rewrite map data model

This commit is contained in:
Martin Michelsen
2024-12-30 09:20:42 -08:00
parent 69f7bb3db9
commit 72ac20e574
95 changed files with 7596 additions and 5125 deletions
+1 -1
View File
@@ -92,7 +92,7 @@ public:
private:
struct File {
std::shared_ptr<const std::string> data;
const Table* table;
const Table* table = nullptr;
};
// Indexed as [online/offline][episode]
+27 -20
View File
@@ -465,7 +465,7 @@ static void server_command_swsetall(shared_ptr<Client> c, const std::string&) {
auto& cmd = cmds[z];
cmd.header.subcommand = 0x05;
cmd.header.size = 0x03;
cmd.header.object_id = 0xFFFF;
cmd.header.entity_id = 0xFFFF;
cmd.switch_flag_floor = c->floor;
cmd.switch_flag_num = z;
cmd.flags = 0x01;
@@ -485,7 +485,7 @@ static void proxy_command_swsetall(shared_ptr<ProxyServer::LinkedSession> ses, c
auto& cmd = cmds[z];
cmd.header.subcommand = 0x05;
cmd.header.size = 0x03;
cmd.header.object_id = 0xFFFF;
cmd.header.entity_id = 0xFFFF;
cmd.switch_flag_floor = ses->floor;
cmd.switch_flag_num = z;
cmd.flags = 0x01;
@@ -1117,7 +1117,7 @@ static void server_command_cheat(shared_ptr<Client> c, const std::string&) {
if (!l->check_flag(Lobby::Flag::CHEATS_ENABLED) &&
!c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE) &&
s->cheat_flags.insufficient_minimum_level) {
size_t default_min_level = s->default_min_level_for_game(l->base_version, l->episode, l->difficulty);
size_t default_min_level = s->default_min_level_for_game(c->version(), l->episode, l->difficulty);
if (l->min_level < default_min_level) {
l->min_level = default_min_level;
send_text_message_printf(l, "$C6Minimum level set\nto %" PRIu32, l->min_level + 1);
@@ -1312,7 +1312,7 @@ static void server_command_secid(shared_ptr<Client> c, const std::string& args)
c->config.override_section_id = new_override_section_id;
if (l->is_game() && (l->leader_id == c->lobby_client_id)) {
l->override_section_id = new_override_section_id;
l->change_section_id();
l->create_item_creator();
}
}
@@ -1343,11 +1343,17 @@ static void server_command_variations(shared_ptr<Client> c, const std::string& a
check_is_game(l, false);
check_cheats_allowed(s, c, s->cheat_flags.override_variations);
c->override_variations = make_unique<parray<le_uint32_t, 0x20>>();
c->override_variations->clear(0);
for (size_t z = 0; z < min<size_t>(c->override_variations->size(), args.size()); z++) {
c->override_variations->at(z) = args[z] - '0';
c->override_variations = make_unique<Variations>();
for (size_t z = 0; z < min<size_t>(c->override_variations->entries.size() * 2, args.size()); z++) {
auto& entry = c->override_variations->entries.at(z / 2);
if (z & 1) {
entry.entities = args[z] - '0';
} else {
entry.layout = args[z] - '0';
}
}
auto vars_str = c->override_variations->str();
c->log.info("Override variations set to %s", vars_str.c_str());
}
static void server_command_rand(shared_ptr<Client> c, const std::string& args) {
@@ -1441,7 +1447,7 @@ static void server_command_min_level(shared_ptr<Client> c, const std::string& ar
bool cheats_allowed = (l->check_flag(Lobby::Flag::CHEATS_ENABLED) ||
c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE));
if (!cheats_allowed && s->cheat_flags.insufficient_minimum_level) {
size_t default_min_level = s->default_min_level_for_game(l->base_version, l->episode, l->difficulty);
size_t default_min_level = s->default_min_level_for_game(c->version(), l->episode, l->difficulty);
if (new_min_level < default_min_level) {
send_text_message_printf(c, "$C6Cannot set minimum\nlevel below %zu", default_min_level + 1);
return;
@@ -2084,7 +2090,10 @@ static void proxy_command_next(shared_ptr<ProxyServer::LinkedSession> ses, const
static void server_command_where(shared_ptr<Client> c, const std::string&) {
auto l = c->require_lobby();
send_text_message_printf(c, "$C7%01" PRIX32 ":%s X:%" PRId32 " Z:%" PRId32,
c->floor, short_name_for_floor(l->episode, c->floor), static_cast<int32_t>(c->x), static_cast<int32_t>(c->z));
c->floor,
short_name_for_floor(l->episode, c->floor),
static_cast<int32_t>(c->pos.x.load()),
static_cast<int32_t>(c->pos.z.load()));
for (auto lc : l->clients) {
if (lc && (lc != c)) {
string name = lc->character()->disp.name.decode(lc->language());
@@ -2108,9 +2117,7 @@ static void server_command_what(shared_ptr<Client> c, const std::string&) {
if (!it.second->visible_to_client(c->lobby_client_id)) {
continue;
}
float dx = it.second->x - c->x;
float dz = it.second->z - c->z;
float dist2 = (dx * dx) + (dz * dz);
float dist2 = (it.second->pos - c->pos).norm2();
if (!nearest_fi || (dist2 < min_dist2)) {
nearest_fi = it.second;
min_dist2 = dist2;
@@ -2281,7 +2288,7 @@ static void server_command_dropmode(shared_ptr<Client> c, const std::string& arg
return;
}
l->set_drop_mode(new_mode);
l->drop_mode = new_mode;
switch (l->drop_mode) {
case Lobby::DropMode::DISABLED:
send_text_message(l, "Item drops disabled");
@@ -2358,11 +2365,11 @@ static void server_command_item(shared_ptr<Client> c, const std::string& args) {
item.id = l->generate_item_id(c->lobby_client_id);
if ((l->drop_mode == Lobby::DropMode::SERVER_PRIVATE) || (l->drop_mode == Lobby::DropMode::SERVER_DUPLICATE)) {
l->add_item(c->floor, item, c->x, c->z, (1 << c->lobby_client_id));
send_drop_stacked_item_to_channel(s, c->channel, item, c->floor, c->x, c->z);
l->add_item(c->floor, item, c->pos, nullptr, nullptr, (1 << c->lobby_client_id));
send_drop_stacked_item_to_channel(s, c->channel, item, c->floor, c->pos);
} else {
l->add_item(c->floor, item, c->x, c->z, 0x00F);
send_drop_stacked_item_to_lobby(l, item, c->floor, c->x, c->z);
l->add_item(c->floor, item, c->pos, nullptr, nullptr, 0x00F);
send_drop_stacked_item_to_lobby(l, item, c->floor, c->pos);
}
string name = s->describe_item(c->version(), item, true);
@@ -2397,8 +2404,8 @@ static void proxy_command_item(shared_ptr<ProxyServer::LinkedSession> ses, const
send_text_message(ses->client_channel, "$C7Next drop:\n" + name);
} else {
send_drop_stacked_item_to_channel(s, ses->client_channel, item, ses->floor, ses->x, ses->z);
send_drop_stacked_item_to_channel(s, ses->server_channel, item, ses->floor, ses->x, ses->z);
send_drop_stacked_item_to_channel(s, ses->client_channel, item, ses->floor, ses->pos);
send_drop_stacked_item_to_channel(s, ses->server_channel, item, ses->floor, ses->pos);
string name = s->describe_item(ses->version(), item, true);
send_text_message(ses->client_channel, "$C7Item created:\n" + name);
+6 -2
View File
@@ -195,8 +195,6 @@ Client::Client(
bb_connection_phase(0xFF),
ping_start_time(0),
sub_version(-1),
x(0.0f),
z(0.0f),
floor(0),
lobby_client_id(0),
lobby_arrow_color(0),
@@ -407,6 +405,9 @@ bool Client::can_see_quest(
uint8_t difficulty,
size_t num_players,
bool v1_present) const {
if (!q->has_version_any_language(this->version())) {
return false;
}
return this->evaluate_quest_availability_expression(q->available_expression, game, event, difficulty, num_players, v1_present);
}
@@ -417,6 +418,9 @@ bool Client::can_play_quest(
uint8_t difficulty,
size_t num_players,
bool v1_present) const {
if (!q->has_version_any_language(this->version())) {
return false;
}
return this->evaluate_quest_availability_expression(q->enabled_expression, game, event, difficulty, num_players, v1_present);
}
+2 -3
View File
@@ -206,10 +206,9 @@ public:
// Lobby/positioning
Config config;
Config synced_config;
std::unique_ptr<parray<le_uint32_t, 0x20>> override_variations;
std::unique_ptr<Variations> override_variations;
int32_t sub_version;
float x;
float z;
VectorXZF pos;
uint32_t floor;
std::weak_ptr<Lobby> lobby;
uint8_t lobby_client_id;
+131 -215
View File
@@ -9,6 +9,7 @@
#include "Episode3/DeckState.hh"
#include "Episode3/MapState.hh"
#include "Episode3/PlayerStateSubordinates.hh"
#include "Map.hh"
#include "PSOProtocol.hh"
#include "PlayerSubordinates.hh"
#include "SaveFileFormats.hh"
@@ -1233,7 +1234,7 @@ struct S_JoinGameT_DC_PC {
// field when sending this command to start an Episode 3 tournament game. This
// can be misleading when reading old logs from those days, but the Episode 3
// client really does ignore it.
/* 0004 */ parray<le_uint32_t, 0x20> variations;
/* 0004 */ Variations variations;
// Unlike lobby join commands, these are filled in in their slot positions.
// That is, if there's only one player in a game with ID 2, then the first two
// of these are blank and the player's data is in the third entry here.
@@ -1246,7 +1247,7 @@ struct S_JoinGameT_DC_PC {
/* 0109 */ uint8_t event = 0;
/* 010A */ uint8_t section_id = 0;
/* 010B */ uint8_t challenge_mode = 0;
/* 010C */ le_uint32_t rare_seed = 0;
/* 010C */ le_uint32_t random_seed = 0;
/* 0110 */
} __packed__;
@@ -1255,7 +1256,7 @@ struct S_JoinGame_DCNTE_64 {
uint8_t leader_id = 0;
uint8_t disable_udp = 1;
uint8_t unused = 0;
parray<le_uint32_t, 0x20> variations;
Variations variations;
parray<PlayerLobbyDataDCGC, 4> lobby_data;
} __packed_ws__(S_JoinGame_DCNTE_64, 0x104);
using S_JoinGame_DC_64 = S_JoinGameT_DC_PC<PlayerLobbyDataDCGC>;
@@ -2238,50 +2239,19 @@ struct S_ServerTime_B1 {
struct S_ExecuteCode_B2 {
// If code_size == 0, no code is executed, but checksumming may still occur.
// In that case, this structure is the entire body of the command (no footer
// is sent).
// In that case, this structure is the entire body of the command (no code
// or footer is sent).
le_uint32_t code_size = 0; // Size of code (following this struct) and footer
le_uint32_t checksum_start = 0; // May be null if size is zero
le_uint32_t checksum_size = 0; // If zero, no checksum is computed
// The code immediately follows, ending with an S_ExecuteCode_Footer_B2
// The code immediately follows, ending with a RELFileFooter. The REL's root
// offset is relative to the start of the code object here, so an offset of 0
// refers to the byte after checksum_size. The root offset points to the
// offset ot the entrypoint (that is, the entrypoint offset is doubly
// indirect). This is presumably done so the entrypoint can be optionally
// relocated.
} __packed_ws__(S_ExecuteCode_B2, 0x0C);
template <bool BE>
struct S_ExecuteCode_FooterT_B2 {
static constexpr bool IsBE = BE; // Needed by generate_client_command_t
// Relocations is a list of words (le_uint16_t on DC/PC/XB/BB, be_uint16_t on
// GC) containing the number of doublewords (uint32_t) to skip for each
// relocation. The relocation pointer starts immediately after the
// checksum_size field in the header, and advances by the value of one
// relocation word (times 4) before each relocation. At each relocated
// doubleword, the address of the first byte of the code (after checksum_size)
// is added to the existing value.
// For example, if the code segment contains the following data (where R
// specifies doublewords to relocate):
// RR RR RR RR ?? ?? ?? ?? ?? ?? ?? ?? RR RR RR RR
// RR RR RR RR ?? ?? ?? ?? RR RR RR RR
// then the relocation words should be 0000, 0003, 0001, and 0002.
// If there is a small number of relocations, they may be placed in the unused
// fields of this structure to save space and/or confuse reverse engineers.
// The game never accesses the last 12 bytes of this structure unless
// relocations_offset points there, so those 12 bytes may also be omitted from
// the command entirely (without changing code_size - so code_size would
// technically extend beyond the end of the B2 command).
U32T<BE> relocations_offset = 0; // Relative to code base (after checksum_size)
U32T<BE> num_relocations = 0;
parray<U32T<BE>, 2> unused1;
// entrypoint_offset is doubly indirect - it points to a pointer to a 32-bit
// value that itself is the actual entrypoint. This is presumably done so the
// entrypoint can be optionally relocated.
U32T<BE> entrypoint_addr_offset = 0; // Relative to code base (after checksum_size).
parray<U32T<BE>, 3> unused2;
} __packed__;
using S_ExecuteCode_Footer_GC_B2 = S_ExecuteCode_FooterT_B2<true>;
using S_ExecuteCode_Footer_DC_PC_XB_BB_B2 = S_ExecuteCode_FooterT_B2<false>;
check_struct_size(S_ExecuteCode_Footer_GC_B2, 0x20);
check_struct_size(S_ExecuteCode_Footer_DC_PC_XB_BB_B2, 0x20);
// B3 (C->S): Execute code and/or checksum memory result
// Not used on versions that don't support the B2 command (see above).
@@ -3244,7 +3214,7 @@ struct SC_SyncSaveFiles_BB_E7 {
// primary game's players should be the leader (this is what newserv does).
struct S_JoinSpectatorTeam_Ep3_E8 {
/* 0000 */ parray<le_uint32_t, 0x20> variations; // unused
/* 0000 */ Variations variations; // unused
struct PlayerEntry {
/* 0000 */ PlayerLobbyDataDCGC lobby_data;
/* 0020 */ PlayerInventory inventory;
@@ -3260,7 +3230,7 @@ struct S_JoinSpectatorTeam_Ep3_E8 {
/* 1175 */ uint8_t event = 0;
/* 1176 */ uint8_t section_id = 0;
/* 1177 */ uint8_t challenge_mode = 0;
/* 1178 */ le_uint32_t rare_seed = 0;
/* 1178 */ le_uint32_t random_seed = 0;
/* 117C */ uint8_t episode = 0;
/* 117D */ parray<uint8_t, 3> unused;
struct SpectatorEntry {
@@ -3867,16 +3837,11 @@ struct G_ClientIDHeader {
uint8_t size = 0;
le_uint16_t client_id = 0; // <= 12
} __packed_ws__(G_ClientIDHeader, 4);
struct G_EnemyIDHeader {
struct G_EntityIDHeader {
uint8_t subcommand = 0;
uint8_t size = 0;
le_uint16_t enemy_id = 0; // In [0x1000, 0x4000); not the same as enemy_index!
} __packed_ws__(G_EnemyIDHeader, 4);
struct G_ObjectIDHeader {
uint8_t subcommand = 0;
uint8_t size = 0;
le_uint16_t object_id = 0; // >= 0x4000, != 0xFFFF
} __packed_ws__(G_ObjectIDHeader, 4);
le_uint16_t entity_id = 0; // 0-B=client, 1000-3FFF=enemy, 4000-FFFF=object
} __packed_ws__(G_EntityIDHeader, 4);
struct G_ParameterHeader {
uint8_t subcommand = 0;
uint8_t size = 0;
@@ -3916,9 +3881,9 @@ struct G_Unknown_6x02_6x03 {
// TODO: This does something with TObjDoorKey objects
struct G_Unknown_6x04 {
G_ClientIDHeader header;
le_uint16_t door_key_token = 0;
le_uint16_t unused = 0;
G_ParameterHeader header; // param = door token (NOT entity ID or index)
le_uint16_t unused1 = 0;
le_uint16_t unused2 = 0;
} __packed_ws__(G_Unknown_6x04, 8);
// 6x05: Switch state changed
@@ -3930,7 +3895,7 @@ struct G_Unknown_6x04 {
struct G_SwitchStateChanged_6x05 {
// Note: header.object_id is 0xFFFF for room clear when all enemies defeated
G_ObjectIDHeader header;
G_EntityIDHeader header;
// TODO: Some of these might be big-endian on GC; it only byteswaps
// switch_flag_num. Are the others actually uint16, or are they uint8[2]?
le_uint16_t client_id = 0;
@@ -3996,14 +3961,14 @@ struct G_SymbolChat_6x07 {
// 6x09: Unknown
struct G_Unknown_6x09 {
G_EnemyIDHeader header;
G_EntityIDHeader header;
} __packed_ws__(G_Unknown_6x09, 4);
// 6x0A: Update enemy state
template <bool BE>
struct G_UpdateEnemyStateT_6x0A {
G_EnemyIDHeader header;
G_EntityIDHeader header;
le_uint16_t enemy_index = 0; // [0, 0xB50)
le_uint16_t total_damage = 0;
// Flags:
@@ -4019,7 +3984,7 @@ check_struct_size(G_UpdateEnemyState_DC_PC_XB_BB_6x0A, 0x0C);
// 6x0B: Update object state
struct G_UpdateObjectState_6x0B {
G_ClientIDHeader header;
G_EntityIDHeader header;
le_uint32_t flags = 0;
le_uint32_t object_index = 0;
} __packed_ws__(G_UpdateObjectState_6x0B, 0x0C);
@@ -4069,12 +4034,12 @@ struct G_Unknown_6x0E {
// 6x10: Unknown (not valid on Episode 3)
struct G_Unknown_6x10_6x11_6x12_6x14 {
G_EnemyIDHeader header;
struct G_Unknown_6x10_6x11_6x14 {
G_EntityIDHeader header;
le_uint16_t unknown_a2 = 0;
le_uint16_t unknown_a3 = 0;
le_uint32_t unknown_a4 = 0;
} __packed_ws__(G_Unknown_6x10_6x11_6x12_6x14, 0x0C);
} __packed_ws__(G_Unknown_6x10_6x11_6x14, 0x0C);
// 6x11: Unknown (not valid on Episode 3)
// Same format as 6x10
@@ -4083,7 +4048,7 @@ struct G_Unknown_6x10_6x11_6x12_6x14 {
template <bool BE>
struct G_DragonBossActionsT_6x12 {
G_EnemyIDHeader header;
G_EntityIDHeader header;
le_uint16_t unknown_a2 = 0;
le_uint16_t unknown_a3 = 0;
le_uint32_t unknown_a4 = 0;
@@ -4098,7 +4063,7 @@ check_struct_size(G_DragonBossActions_GC_6x12, 0x14);
// 6x13: De Rol Le boss actions (not valid on Episode 3)
struct G_DeRolLeBossActions_6x13 {
G_EnemyIDHeader header;
G_EntityIDHeader header;
le_uint16_t unknown_a2 = 0;
le_uint16_t unknown_a3 = 0;
} __packed_ws__(G_DeRolLeBossActions_6x13, 8);
@@ -4109,7 +4074,7 @@ struct G_DeRolLeBossActions_6x13 {
// 6x15: Vol Opt boss actions (not valid on Episode 3)
struct G_VolOptBossActions_6x15 {
G_EnemyIDHeader header;
G_EntityIDHeader header;
le_uint16_t unknown_a2 = 0;
le_uint16_t unknown_a3 = 0;
le_uint16_t unknown_a4 = 0;
@@ -4119,7 +4084,7 @@ struct G_VolOptBossActions_6x15 {
// 6x16: Vol Opt boss actions (not valid on Episode 3)
struct G_VolOptBossActions_6x16 {
G_UnusedHeader header;
G_EntityIDHeader header;
parray<uint8_t, 6> unknown_a2;
le_uint16_t unknown_a3 = 0;
} __packed_ws__(G_VolOptBossActions_6x16, 0x0C);
@@ -4127,7 +4092,7 @@ struct G_VolOptBossActions_6x16 {
// 6x17: Vol Opt phase 2 boss actions (not valid on Episode 3)
struct G_VolOpt2BossActions_6x17 {
G_ClientIDHeader header;
G_EntityIDHeader header;
le_float unknown_a2 = 0.0f;
le_float unknown_a3 = 0.0f;
le_float unknown_a4 = 0.0f;
@@ -4137,14 +4102,14 @@ struct G_VolOpt2BossActions_6x17 {
// 6x18: Vol Opt phase 2 boss actions (not valid on Episode 3)
struct G_VolOpt2BossActions_6x18 {
G_ClientIDHeader header;
G_EntityIDHeader header;
parray<le_uint16_t, 4> unknown_a2;
} __packed_ws__(G_VolOpt2BossActions_6x18, 0x0C);
// 6x19: Dark Falz boss actions (not valid on Episode 3)
struct G_DarkFalzActions_6x19 {
G_EnemyIDHeader header;
G_EntityIDHeader header;
le_uint16_t unknown_a2 = 0;
le_uint16_t unknown_a3 = 0;
le_uint32_t unknown_a4 = 0;
@@ -4186,9 +4151,7 @@ struct G_SetPlayerFloor_6x1F {
struct G_SetPosition_6x20 {
G_ClientIDHeader header;
le_int32_t floor = 0;
le_float x = 0.0f;
le_float y = 0.0f;
le_float z = 0.0f;
VectorXYZF pos;
le_uint32_t unknown_a1 = 0;
} __packed_ws__(G_SetPosition_6x20, 0x18);
@@ -4212,9 +4175,7 @@ struct G_SetPlayerVisibility_6x22_6x23 {
struct G_TeleportPlayer_6x24 {
G_ClientIDHeader header;
le_uint32_t unknown_a1 = 0;
le_float x = 0.0f;
le_float y = 0.0f;
le_float z = 0.0f;
VectorXYZF pos;
} __packed_ws__(G_TeleportPlayer_6x24, 0x14);
// 6x25: Equip item (protected on V3/V4)
@@ -4266,9 +4227,7 @@ struct G_DropItem_6x2A {
le_uint16_t amount = 0;
le_uint16_t floor = 0;
le_uint32_t item_id = 0;
le_float x = 0.0f;
le_float y = 0.0f;
le_float z = 0.0f;
VectorXYZF pos;
} __packed_ws__(G_DropItem_6x2A, 0x18);
// 6x2B: Create item in inventory (e.g. via tekker or bank withdraw) (protected on V3/V4)
@@ -4377,11 +4336,11 @@ struct G_PhotonBlast_6x37 {
// 6x38: Donate to photon blast (protected on V3/V4)
struct G_Unknown_6x38 {
struct G_DonateToPhotonBlast_6x38 {
G_ClientIDHeader header;
le_uint16_t unknown_a1 = 0;
le_uint16_t unused = 0;
} __packed_ws__(G_Unknown_6x38, 8);
} __packed_ws__(G_DonateToPhotonBlast_6x38, 8);
// 6x39: Photon blast ready (protected on V3/V4)
@@ -4414,9 +4373,7 @@ struct G_StopAtPosition_6x3E {
le_uint16_t angle = 0;
le_int16_t floor = 0;
le_int16_t room = 0;
le_float x = 0.0f;
le_float y = 0.0f;
le_float z = 0.0f;
VectorXYZF pos;
} __packed_ws__(G_StopAtPosition_6x3E, 0x18);
// 6x3F: Set position (protected on V3/V4)
@@ -4427,9 +4384,7 @@ struct G_SetPosition_6x3F {
le_uint16_t angle = 0;
le_int16_t floor = 0;
le_int16_t room = 0;
le_float x = 0.0f;
le_float y = 0.0f;
le_float z = 0.0f;
VectorXYZF pos;
} __packed_ws__(G_SetPosition_6x3F, 0x18);
// 6x40: Walk (protected on V3/V4)
@@ -4437,28 +4392,19 @@ struct G_SetPosition_6x3F {
struct G_WalkToPosition_6x40 {
G_ClientIDHeader header;
le_float x = 0.0f;
le_float z = 0.0f;
VectorXZF pos;
le_uint32_t action = 0;
} __packed_ws__(G_WalkToPosition_6x40, 0x10);
// 6x41: Unknown
// This subcommand is completely ignored by v2 and later.
struct G_Unknown_6x41 {
G_ClientIDHeader header;
le_float x = 0.0f;
le_float z = 0.0f;
} __packed_ws__(G_Unknown_6x41, 0x0C);
// 6x41: Move to position (v1)
// 6x42: Run (protected on V3/V4)
// This subcommand is completely ignored by v2 and later.
// If UDP mode is enabled, this command is sent via UDP.
struct G_RunToPosition_6x42 {
struct G_MoveToPosition_6x41_6x42 {
G_ClientIDHeader header;
le_float x = 0.0f;
le_float z = 0.0f;
} __packed_ws__(G_RunToPosition_6x42, 0x0C);
VectorXZF pos;
} __packed_ws__(G_MoveToPosition_6x41_6x42, 0x0C);
// 6x43: First attack (protected on V3/V4)
// 6x44: Second attack (protected on V3/V4)
@@ -4484,7 +4430,7 @@ struct TargetEntry {
struct G_AttackFinished_6x46 {
G_ClientIDHeader header;
le_uint32_t count = 0;
le_uint32_t target_count = 0;
// The client may send a shorter command if not all of these are used.
parray<TargetEntry, 10> targets;
} __packed_ws__(G_AttackFinished_6x46, 0x30);
@@ -4544,8 +4490,7 @@ struct G_HitByEnemy_6x4B_6x4C {
G_ClientIDHeader header;
le_uint16_t angle = 0;
le_uint16_t current_hp = 0;
le_float x_velocity = 0.0f;
le_float z_velocity = 0.0f;
VectorXZF velocity;
} __packed_ws__(G_HitByEnemy_6x4B_6x4C, 0x10);
// 6x4D: Player died (protected on V3/V4)
@@ -4603,7 +4548,9 @@ struct G_Unknown_6x53 {
} __packed_ws__(G_Unknown_6x53, 4);
// 6x54: Unknown
// This subcommand is completely ignored (at least, by PSO GC).
// This subcommand is completely ignored by DCv2 and later. On DC NTE, 11/2000,
// and DCv1, the handler has some logic in it and it calls a virtual function
// on TObjPlayer, but that codepath ends up doing nothing.
struct G_Unknown_6x54 {
G_ClientIDHeader header;
@@ -4614,23 +4561,17 @@ struct G_Unknown_6x54 {
struct G_IntraMapWarp_6x55 {
G_ClientIDHeader header;
le_uint32_t unknown_a1 = 0;
le_float x1 = 0.0f;
le_float y1 = 0.0f;
le_float z1 = 0.0f;
le_float x2 = 0.0f;
le_float y2 = 0.0f;
le_float z2 = 0.0f;
VectorXYZF pos1;
VectorXYZF pos2;
} __packed_ws__(G_IntraMapWarp_6x55, 0x20);
// 6x56: Unknown (supported; game) (protected on V3/V4)
// 6x56: Set player position and angle (protected on V3/V4)
struct G_Unknown_6x56 {
struct G_SetPlayerPositionAndAngle_6x56 {
G_ClientIDHeader header;
le_uint32_t unknown_a1 = 0;
le_float x = 0.0f;
le_float y = 0.0f;
le_float z = 0.0f;
} __packed_ws__(G_Unknown_6x56, 0x14);
le_uint32_t angle_y = 0;
VectorXYZF pos;
} __packed_ws__(G_SetPlayerPositionAndAngle_6x56, 0x14);
// 6x57: Unknown (supported; lobby & game) (protected on V3/V4)
@@ -4680,8 +4621,7 @@ struct G_DropStackedItem_DC_6x5D {
G_ClientIDHeader header;
le_uint16_t floor = 0;
le_uint16_t unknown_a2 = 0; // Corresponds to FloorItem::unknown_a2
le_float x = 0.0f;
le_float z = 0.0f;
VectorXZF pos;
ItemData item_data;
} __packed_ws__(G_DropStackedItem_DC_6x5D, 0x24);
@@ -4700,10 +4640,9 @@ struct G_BuyShopItem_6x5E {
struct FloorItem {
/* 00 */ uint8_t floor = 0;
/* 01 */ uint8_t from_enemy = 0;
/* 02 */ le_uint16_t entity_id = 0; // < 0x0B50 if from_enemy != 0; otherwise < 0x0BA0
/* 04 */ le_float x = 0.0f;
/* 08 */ le_float z = 0.0f;
/* 01 */ uint8_t source_type = 0; // 1 = enemy, 2 = box
/* 02 */ le_uint16_t entity_index = 0; // < 0x0B50 if source_type == 1; otherwise < 0x0BA0
/* 04 */ VectorXZF pos;
/* 0C */ le_uint16_t unknown_a2 = 0;
// The drop number is scoped to the floor and increments by 1 each time an
// item is dropped. The last item dropped in each floor has drop_number equal
@@ -4728,9 +4667,8 @@ struct G_StandardDropItemRequest_DC_6x60 {
/* 00 */ G_UnusedHeader header;
/* 04 */ uint8_t floor = 0;
/* 05 */ uint8_t rt_index = 0;
/* 06 */ le_uint16_t entity_id = 0;
/* 08 */ le_float x = 0.0f;
/* 0C */ le_float z = 0.0f;
/* 06 */ le_uint16_t entity_index = 0;
/* 08 */ VectorXZF pos;
/* 10 */ le_uint16_t section = 0;
/* 12 */ le_uint16_t ignore_def = 0;
/* 14 */
@@ -4794,9 +4732,7 @@ struct G_SetTelepipeState_6x68 {
le_uint16_t unknown_b1 = 0;
uint8_t unknown_b2 = 0;
uint8_t unknown_b3 = 0;
le_float x = 0.0f;
le_float y = 0.0f;
le_float z = 0.0f;
VectorXYZF pos;
le_uint32_t unknown_a3 = 0;
} __packed_ws__(G_SetTelepipeState_6x68, 0x1C);
@@ -4829,16 +4765,21 @@ struct G_NPCControl_6x69 {
le_uint16_t param3; // Commands 0/3: npc_template_index; commands 1/2: unused
} __packed_ws__(G_NPCControl_6x69, 0x0C);
// 6x6A: Use boss warp (not valid on Episode 3)
// 6x6A: Set boss warp flags (not valid on Episode 3)
struct G_UseBossWarp_6x6A {
G_ClientIDHeader header;
le_uint16_t unknown_a1 = 0;
le_uint16_t unused = 0;
} __packed_ws__(G_UseBossWarp_6x6A, 8);
struct G_SetBossWarpFlags_6x6A {
G_EntityIDHeader header; // entity_id = boss warp object ID
le_uint16_t flags;
le_uint16_t unused;
} __packed_ws__(G_SetBossWarpFlags_6x6A, 8);
// 6x6B: Sync enemy state (used while loading into game)
// Note that DC NTE doesn't send the decompressed size in the header here. This
// is a bug that can cause the client to write more entries than it should when
// it receives one of these commands. All later versions, including 11/2000,
// use the second header structure here, which prevents this issue.
struct G_SyncGameStateHeader_DCNTE_6x6B_6x6C_6x6D_6x6E {
G_ExtendedHeaderT<G_UnusedHeader> header;
le_uint32_t decompressed_size = 0;
@@ -4852,25 +4793,11 @@ struct G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E {
// BC0-compressed data follows here (see bc0_decompress)
} __packed_ws__(G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E, 0x10);
// Decompressed format is a list of these
struct G_SyncEnemyState_6x6B_Entry_Decompressed {
le_uint32_t flags = 0; // Same as flags in 6x0A
le_uint16_t item_drop_id = 0;
le_uint16_t total_damage = 0; // Same as in 6x0A
uint8_t red_buff_type = 0;
uint8_t red_buff_level = 0;
uint8_t blue_buff_type = 0;
uint8_t blue_buff_level = 0;
} __packed_ws__(G_SyncEnemyState_6x6B_Entry_Decompressed, 0x0C);
// Decompressed format is a list of SyncEnemyStateEntry (from Map.hh).
// 6x6C: Sync object state (used while loading into game)
// Compressed format is the same as 6x6B.
// Decompressed format is a list of these
struct G_SyncObjectState_6x6C_Entry_Decompressed {
le_uint16_t flags = 0;
le_uint16_t item_drop_id = 0;
} __packed_ws__(G_SyncObjectState_6x6C_Entry_Decompressed, 4);
// Decompressed format is a list of SyncObjectStateEntry (from Map.hh).
// 6x6D: Sync item state (used while loading into game)
// Internal name: RcvItemCondition
@@ -4967,9 +4894,7 @@ struct G_6x70_Sub_Telepipe {
/* 00 */ le_uint16_t owner_client_id = 0xFFFF;
/* 02 */ le_uint16_t floor = 0;
/* 04 */ le_uint32_t unknown_a1 = 0;
/* 08 */ le_float x = 0.0f;
/* 0C */ le_float y = 0.0f;
/* 10 */ le_float z = 0.0f;
/* 08 */ VectorXYZF pos;
/* 14 */ le_uint32_t unknown_a3 = 0;
/* 18 */ le_uint32_t unknown_a4 = 0x0000FFFF;
/* 1C */
@@ -4997,9 +4922,7 @@ struct G_6x70_Base_DCNTE {
/* 0000 */ le_uint16_t client_id = 0;
/* 0002 */ le_uint16_t room_id = 0;
/* 0004 */ le_uint32_t flags1 = 0;
/* 0008 */ le_float x = 0.0f;
/* 000C */ le_float y = 0.0f;
/* 0010 */ le_float z = 0.0f;
/* 0008 */ VectorXYZF pos;
/* 0014 */ le_uint32_t angle_x = 0;
/* 0018 */ le_uint32_t angle_y = 0;
/* 001C */ le_uint32_t angle_z = 0;
@@ -5174,7 +5097,7 @@ struct G_UpdateQuestFlag_V3_BB_6x75 : G_UpdateQuestFlag_DC_PC_6x75 {
// bitwise OR operation instead of a simple assignment.
struct G_SetEntitySetFlags_6x76 {
G_EnemyIDHeader header; // 1000-3FFF = enemy, 4000-FFFF = object
G_EntityIDHeader header; // 1000-3FFF = enemy, 4000-FFFF = object
le_uint16_t floor = 0;
le_uint16_t flags = 0;
} __packed_ws__(G_SetEntitySetFlags_6x76, 8);
@@ -5289,8 +5212,10 @@ check_struct_size(G_BattleScoresBE_6x7F, 0x24);
struct G_TriggerTrap_6x80 {
G_ClientIDHeader header;
le_uint16_t unknown_a1 = 0;
le_uint16_t unknown_a2 = 0;
// Traps set by players are numbered according to their type and who set
// them. The trap number is computed as (client_id * 80) + (trap_type * 20).
le_uint16_t trap_number = 0;
le_uint16_t what = 0; // Must be 0, 1, or 2
} __packed_ws__(G_TriggerTrap_6x80, 8);
// 6x81: Disable drop weapon on death (protected on V3/V4)
@@ -5334,10 +5259,7 @@ struct G_Unknown_6x85 {
// 6x86: Hit destructible object (not valid on Episode 3)
struct G_HitDestructibleObject_6x86 {
G_ObjectIDHeader header;
le_uint32_t unknown_a1 = 0;
le_uint32_t unknown_a2 = 0;
struct G_HitDestructibleObject_6x86 : G_UpdateObjectState_6x0B {
le_uint16_t unknown_a3 = 0;
le_uint16_t unknown_a4 = 0;
} __packed_ws__(G_HitDestructibleObject_6x86, 0x10);
@@ -5355,13 +5277,14 @@ struct G_RestoreShrunkenPlayer_6x88 {
G_ClientIDHeader header;
} __packed_ws__(G_RestoreShrunkenPlayer_6x88, 4);
// 6x89: Player killed by monster (protected on V3/V4)
// 6x89: Set killer entity ID (protected on V3/V4)
// This is used to specify which enemy killed a player.
struct G_PlayerKilledByMonster_6x89 {
struct G_SetKillerEntityID_6x89 {
G_ClientIDHeader header;
le_uint16_t unknown_a1 = 0;
le_uint16_t killer_entity_id = 0;
le_uint16_t unused = 0;
} __packed_ws__(G_PlayerKilledByMonster_6x89, 8);
} __packed_ws__(G_SetKillerEntityID_6x89, 8);
// 6x8A: Show Challenge time records window (not valid on Episode 3)
// The leader sends this command to tell other clients to show, hide, or update
@@ -5400,34 +5323,31 @@ struct G_SetTechniqueLevelOverride_6x8D {
// 6x8E: Unknown (not valid on Episode 3)
// This command has a handler, but it does nothing.
// 6x8F: Unknown (not valid on Episode 3)
// 6x8F: Add battle damage scores (not valid on Episode 3)
struct G_Unknown_6x8F {
struct G_AddBattleDamageScores_6x8F {
G_ClientIDHeader header; // client_id is the attacking player
le_uint16_t target_entity_id = 0;
le_uint16_t amount = 0;
} __packed_ws__(G_AddBattleDamageScores_6x8F, 8);
// 6x90: Set player battle team (not valid on Episode 3) (protected on V3/V4)
struct G_SetPlayerBattleTeam_6x90 {
G_ClientIDHeader header;
le_uint16_t client_id2 = 0;
le_uint16_t unknown_a1 = 0;
} __packed_ws__(G_Unknown_6x8F, 8);
// 6x90: Unknown (not valid on Episode 3) (protected on V3/V4)
struct G_Unknown_6x90 {
G_ClientIDHeader header;
le_uint32_t unknown_a1 = 0;
} __packed_ws__(G_Unknown_6x90, 8);
le_uint32_t team_number = 0; // 0 or 1
} __packed_ws__(G_SetPlayerBattleTeam_6x90, 8);
// 6x91: Unknown (supported; game only)
// TODO: Deals with TOAttackableCol objects. Figoure out exactly what it does.
// TODO: Deals with TOAttackableCol objects. Figure out exactly what it does.
struct G_Unknown_6x91 {
G_ObjectIDHeader header;
le_uint32_t object_flags = 0;
le_uint32_t object_index = 0; // < 0xBA0
struct G_UpdateAttackableColState_6x91 : G_UpdateObjectState_6x0B {
le_uint16_t unknown_a3 = 0;
le_uint16_t unknown_a4 = 0;
le_uint16_t switch_flag_num = 0;
uint8_t should_set = 0; // The switch flag is only set if this is equal to 1; otherwise it's cleared
uint8_t switch_flag_floor = 0;
} __packed_ws__(G_Unknown_6x91, 0x14);
uint8_t floor = 0;
} __packed_ws__(G_UpdateAttackableColState_6x91, 0x14);
// 6x92: Unknown (not valid on Episode 3)
// This does something with the TObjOnlineEndingHexMove object. TODO: Figure
@@ -5522,7 +5442,7 @@ struct G_LevelUpAllTechniques_6x9B {
// TODO: Figure out what this does.
struct G_Unknown_6x9C {
G_EnemyIDHeader header;
G_EntityIDHeader header;
le_uint32_t unknown_a1 = 0;
} __packed_ws__(G_Unknown_6x9C, 8);
@@ -5547,7 +5467,7 @@ struct G_PlayerCameraShutterSound_6x9E {
// 6x9F: Gal Gryphon boss actions (not valid on pre-V3 or Episode 3)
struct G_GalGryphonBossActions_6x9F {
G_EnemyIDHeader header;
G_EntityIDHeader header;
le_uint32_t unknown_a1 = 0;
le_float unknown_a2 = 0.0f;
le_float unknown_a3 = 0.0f;
@@ -5556,10 +5476,8 @@ struct G_GalGryphonBossActions_6x9F {
// 6xA0: Gal Gryphon boss actions (not valid on pre-V3 or Episode 3)
struct G_GalGryphonBossActions_6xA0 {
G_EnemyIDHeader header;
le_float x = 0.0f;
le_float y = 0.0f;
le_float z = 0.0f;
G_EntityIDHeader header;
VectorXYZF pos;
le_uint32_t unknown_a1 = 0;
le_uint16_t unknown_a2 = 0;
le_uint16_t unknown_a3 = 0;
@@ -5586,7 +5504,7 @@ struct G_SpecializableItemDropRequest_6xA2 : G_StandardDropItemRequest_PC_V3_BB_
// 6xA3: Olga Flow boss actions (not valid on pre-V3 or Episode 3)
struct G_OlgaFlowBossActions_6xA3 {
G_EnemyIDHeader header;
G_EntityIDHeader header;
uint8_t unknown_a1 = 0;
uint8_t unknown_a2 = 0;
parray<uint8_t, 2> unknown_a3;
@@ -5595,7 +5513,7 @@ struct G_OlgaFlowBossActions_6xA3 {
// 6xA4: Olga Flow phase 1 boss actions (not valid on pre-V3 or Episode 3)
struct G_OlgaFlowPhase1BossActions_6xA4 {
G_EnemyIDHeader header;
G_EntityIDHeader header;
uint8_t what = 0;
parray<uint8_t, 3> unknown_a3;
} __packed_ws__(G_OlgaFlowPhase1BossActions_6xA4, 8);
@@ -5603,7 +5521,7 @@ struct G_OlgaFlowPhase1BossActions_6xA4 {
// 6xA5: Olga Flow phase 2 boss actions (not valid on pre-V3 or Episode 3)
struct G_OlgaFlowPhase2BossActions_6xA5 {
G_EnemyIDHeader header;
G_EntityIDHeader header;
uint8_t what = 0;
parray<uint8_t, 3> unknown_a3;
} __packed_ws__(G_OlgaFlowPhase2BossActions_6xA5, 8);
@@ -5637,7 +5555,7 @@ struct G_ModifyTradeProposal_6xA6 {
template <bool BE>
struct G_GolDragonBossActionsT_6xA8 {
G_EnemyIDHeader header;
G_EntityIDHeader header;
le_uint16_t unknown_a2 = 0;
le_uint16_t unknown_a3 = 0;
le_uint32_t unknown_a4 = 0;
@@ -5654,7 +5572,7 @@ check_struct_size(G_GolDragonBossActions_GC_6xA8, 0x18);
// 6xA9: Barba Ray boss actions (not valid on pre-V3 or Episode 3)
struct G_BarbaRayBossActions_6xA9 {
G_EnemyIDHeader header;
G_EntityIDHeader header;
le_uint16_t unknown_a1 = 0;
le_uint16_t unknown_a2 = 0;
} __packed_ws__(G_BarbaRayBossActions_6xA9, 8);
@@ -5662,7 +5580,7 @@ struct G_BarbaRayBossActions_6xA9 {
// 6xAA: Barba Ray boss actions (not valid on pre-V3 or Episode 3)
struct G_BarbaRayBossActions_6xAA {
G_EnemyIDHeader header;
G_EntityIDHeader header;
le_uint16_t unknown_a1 = 0;
le_uint16_t unknown_a2 = 0;
le_uint32_t unknown_a3 = 0;
@@ -5673,7 +5591,7 @@ struct G_BarbaRayBossActions_6xAA {
// It's not known what it does on GC NTE.
struct G_Unknown_GCNTE_6xAB {
G_EnemyIDHeader header;
G_EntityIDHeader header;
le_uint16_t unknown_a1 = 0;
le_uint16_t unknown_a2 = 0;
} __packed_ws__(G_Unknown_GCNTE_6xAB, 8);
@@ -5685,12 +5603,11 @@ struct G_CreateLobbyChair_6xAB {
} __packed_ws__(G_CreateLobbyChair_6xAB, 8);
// 6xAC: Unknown (not valid on pre-V3) (protected on V3/V4)
// This command's appears to be different on GC NTE than on any other version.
// It also seems that no version (other than perhaps GC NTE) ever sends this
// command.
// This command appears to be different on GC NTE than on any other version. It
// also seems that no version (except perhaps GC NTE) ever sends this command.
struct G_Unknown_GCNTE_6xAC {
G_EnemyIDHeader header;
G_EntityIDHeader header;
le_uint16_t unknown_a1 = 0;
le_uint16_t unknown_a2 = 0;
le_uint32_t unknown_a3 = 0;
@@ -6079,8 +5996,7 @@ struct G_SplitStackedItem_BB_6xC3 {
G_ClientIDHeader header;
le_uint16_t floor = 0;
le_uint16_t unused2 = 0;
le_float x = 0.0f;
le_float z = 0.0f;
VectorXZF pos;
le_uint32_t item_id = 0;
le_uint32_t amount = 0;
} __packed_ws__(G_SplitStackedItem_BB_6xC3, 0x18);
@@ -6098,7 +6014,7 @@ struct G_MedicalCenterUsed_BB_6xC5 {
G_ClientIDHeader header;
} __packed_ws__(G_MedicalCenterUsed_BB_6xC5, 4);
// 6xC6: Steal experience (BB)
// 6xC6: Steal experience (BB; handled by the server)
struct G_StealEXP_BB_6xC6 {
G_ClientIDHeader header;
@@ -6118,7 +6034,7 @@ struct G_ChargeAttack_BB_6xC7 {
// 6xC8: Enemy EXP request (BB; handled by the server)
struct G_EnemyEXPRequest_BB_6xC8 {
G_EnemyIDHeader header;
G_EntityIDHeader header;
le_uint16_t enemy_index = 0;
le_uint16_t requesting_client_id = 0;
uint8_t is_killer = 0;
@@ -6182,8 +6098,7 @@ struct G_ChallengeModeGraveRecoveryItemRequest_BB_6xD1 {
G_ClientIDHeader header;
le_uint16_t floor = 0;
le_uint16_t unknown_a1 = 0;
le_float x = 0;
le_float z = 0;
VectorXZF pos;
le_uint32_t item_type = 0; // Should be < 6
} __packed_ws__(G_ChallengeModeGraveRecoveryItemRequest_BB_6xD1, 0x14);
@@ -6281,6 +6196,9 @@ struct G_UpgradeWeaponAttribute_BB_6xDA {
struct G_ExchangeItemInQuest_BB_6xDB {
G_ClientIDHeader header;
// If this is 0, the command is identical to 6x29. If this is 1, a function
// similar to find_item_by_id is called instead of find_item_by_id, but I
// don't yet know what exactly the logic differences are.
le_uint32_t unknown_a1 = 0;
le_uint32_t item_id = 0;
le_uint32_t amount = 0;
@@ -6333,8 +6251,7 @@ struct G_RequestItemDropFromQuest_BB_6xE0 {
uint8_t type = 0; // valueA
uint8_t unknown_a3 = 0;
uint8_t unused = 0;
le_float x = 0.0f; // valueB
le_float z = 0.0f; // valueC
VectorXZF pos; // x = valueB, z = valueC
} __packed_ws__(G_RequestItemDropFromQuest_BB_6xE0, 0x10);
// 6xE1: Exchange Photon Tickets (BB; handled by server)
@@ -6359,8 +6276,7 @@ struct G_GetMesetaSlotPrize_BB_6xE2 {
uint8_t floor;
uint8_t unknown_a2;
uint8_t unused;
le_float x; // TODO: Verify this guess
le_float z; // TODO: Verify this guess
VectorXZF pos; // TODO: Verify this guess
} __packed_ws__(G_GetMesetaSlotPrize_BB_6xE2, 0x10);
// 6xE3: Set Meseta slot prize result (BB)
+136
View File
@@ -0,0 +1,136 @@
#pragma once
#include <phosg/Encoding.hh>
#include <phosg/Vector.hh>
#include "Text.hh"
struct VectorXZF {
le_float x = 0.0;
le_float z = 0.0;
inline VectorXZF operator-() const {
return VectorXZF{-this->x, -this->z};
}
inline VectorXZF operator+(const VectorXZF& other) const {
return VectorXZF{this->x + other.x, this->z + other.z};
}
inline VectorXZF operator-(const VectorXZF& other) const {
return VectorXZF{this->x - other.x, this->z - other.z};
}
inline bool operator==(const VectorXZF& other) const {
return ((this->x == other.x) && (this->z == other.z));
}
inline bool operator!=(const VectorXZF& other) const {
return !this->operator==(other);
}
inline double norm() const {
return sqrt(this->norm2());
}
inline double norm2() const {
return ((this->x * this->x) + (this->z * this->z));
}
inline std::string str() const {
return phosg::string_printf("[VectorXZF x=%g z=%g]", this->x.load(), this->z.load());
}
} __packed_ws__(VectorXZF, 0x08);
struct VectorXYZF {
le_float x = 0.0;
le_float y = 0.0;
le_float z = 0.0;
inline operator VectorXZF() const {
return VectorXZF{this->x, this->z};
}
inline VectorXYZF operator-() const {
return VectorXYZF{-this->x, -this->y, -this->z};
}
inline VectorXYZF operator+(const VectorXYZF& other) const {
return VectorXYZF{this->x + other.x, this->y + other.y, this->z + other.z};
}
inline VectorXYZF operator-(const VectorXYZF& other) const {
return VectorXYZF{this->x - other.x, this->y - other.y, this->z - other.z};
}
inline bool operator==(const VectorXYZF& other) const {
return ((this->x == other.x) && (this->y == other.y) && (this->z == other.z));
}
inline bool operator!=(const VectorXYZF& other) const {
return !this->operator==(other);
}
inline double norm() const {
return sqrt(this->norm2());
}
inline double norm2() const {
return ((this->x * this->x) + (this->y * this->y) + (this->z * this->z));
}
inline std::string str() const {
return phosg::string_printf("[VectorXYZF x=%g y=%g z=%g]", this->x.load(), this->y.load(), this->z.load());
}
} __packed_ws__(VectorXYZF, 0x0C);
struct VectorXYZTF {
le_float x = 0.0;
le_float y = 0.0;
le_float z = 0.0;
le_float t = 0.0;
} __packed_ws__(VectorXYZTF, 0x10);
struct VectorXYZI {
le_uint32_t x = 0;
le_uint32_t y = 0;
le_uint32_t z = 0;
} __packed_ws__(VectorXYZI, 0x0C);
template <bool BE>
struct ArrayRefT {
static constexpr bool IsBE = BE;
/* 00 */ U32T<BE> count;
/* 04 */ U32T<BE> offset;
/* 08 */
} __packed__;
using ArrayRef = ArrayRefT<false>;
using ArrayRefBE = ArrayRefT<true>;
check_struct_size(ArrayRef, 8);
check_struct_size(ArrayRefBE, 8);
template <bool BE>
struct RELFileFooterT {
static constexpr bool IsBE = BE;
// Relocations is a list of words (le_uint16_t on DC/PC/XB/BB, be_uint16_t on
// GC) containing the number of doublewords (uint32_t) to skip for each
// relocation. The relocation pointer starts immediately after the
// checksum_size field in the header, and advances by the value of one
// relocation word (times 4) before each relocation. At each relocated
// doubleword, the address of the first byte of the code (after checksum_size)
// is added to the existing value.
// For example, if the code segment contains the following data (where R
// specifies doublewords to relocate):
// RR RR RR RR ?? ?? ?? ?? ?? ?? ?? ?? RR RR RR RR
// RR RR RR RR ?? ?? ?? ?? RR RR RR RR
// then the relocation words should be 0000, 0003, 0001, and 0002.
// If there is a small number of relocations, they may be placed in the unused
// fields of this structure to save space and/or confuse reverse engineers.
// The game never accesses the last 12 bytes of this structure unless
// relocations_offset points there, so those 12 bytes may also be omitted
// entirely in situations (e.g. in the B2 command, without changing code_size,
// so code_size would technically extend beyond the end of the B2 command).
U32T<BE> relocations_offset = 0;
U32T<BE> num_relocations = 0;
parray<U32T<BE>, 2> unused1;
U32T<BE> root_offset = 0;
parray<U32T<BE>, 3> unused2;
} __attribute__((packed));
using RELFileFooter = RELFileFooterT<false>;
using RELFileFooterBE = RELFileFooterT<true>;
check_struct_size(RELFileFooter, 0x20);
check_struct_size(RELFileFooterBE, 0x20);
+48
View File
@@ -1121,3 +1121,51 @@ bool enemy_type_is_rare(EnemyType type) {
(type == EnemyType::DORPHON_ECLAIR) ||
(type == EnemyType::KONDRIEU));
}
EnemyType rare_type_for_enemy_type(EnemyType base_type, Episode episode, uint8_t event, uint8_t floor) {
switch (base_type) {
case EnemyType::HILDEBEAR:
return EnemyType::HILDEBLUE;
case EnemyType::RAG_RAPPY:
switch (episode) {
case Episode::EP1:
return EnemyType::AL_RAPPY;
case Episode::EP2:
switch (event) {
case 0x01: // rappy_type 1
return EnemyType::SAINT_RAPPY;
case 0x04: // rappy_type 2
return EnemyType::EGG_RAPPY;
case 0x05: // rappy_type 3
return EnemyType::HALLO_RAPPY;
default:
return EnemyType::LOVE_RAPPY;
}
case Episode::EP4:
return (floor > 0x05) ? EnemyType::DEL_RAPPY_ALT : EnemyType::DEL_RAPPY;
default:
throw logic_error("invalid episode");
}
case EnemyType::POISON_LILY:
return EnemyType::NAR_LILY;
case EnemyType::POFUILLY_SLIME:
return EnemyType::POUILLY_SLIME;
case EnemyType::SAND_RAPPY:
return EnemyType::DEL_RAPPY;
case EnemyType::SAND_RAPPY_ALT:
return EnemyType::DEL_RAPPY_ALT;
case EnemyType::MERISSA_A:
return EnemyType::MERISSA_AA;
case EnemyType::ZU:
return EnemyType::PAZUZU;
case EnemyType::ZU_ALT:
return EnemyType::PAZUZU_ALT;
case EnemyType::DORPHON:
return EnemyType::DORPHON_ECLAIR;
case EnemyType::SAINT_MILLION:
case EnemyType::SHAMBERTIN:
return EnemyType::KONDRIEU;
default:
return base_type;
}
}
+1
View File
@@ -148,3 +148,4 @@ uint8_t battle_param_index_for_enemy_type(Episode episode, EnemyType enemy_type)
uint8_t rare_table_index_for_enemy_type(EnemyType enemy_type);
const std::vector<EnemyType>& enemy_types_for_rare_table_index(Episode episode, uint8_t rt_index);
bool enemy_type_is_rare(EnemyType type);
EnemyType rare_type_for_enemy_type(EnemyType base_type, Episode episode, uint8_t event, uint8_t floor);
+22 -15
View File
@@ -8,6 +8,7 @@
#include <phosg/Random.hh>
#include <phosg/Time.hh>
#include "../CommonFileFormats.hh"
#include "../Compression.hh"
#include "../Loggers.hh"
#include "../PSOEncryption.hh"
@@ -2452,28 +2453,34 @@ CardIndex::CardIndex(
this->compressed_card_definitions = phosg::load_file(filename);
decompressed_data = prs_decompress(this->compressed_card_definitions);
}
// The client can't handle files larger than this
if (decompressed_data.size() > 0x36EC0) {
throw runtime_error("decompressed card list data is too long");
}
// There's a footer after the card definitions (it's a standard-format REL
// file), but we ignore it
if (decompressed_data.size() % sizeof(CardDefinition) != sizeof(CardDefinitionsFooter)) {
throw runtime_error(phosg::string_printf(
"decompressed card update file size %zX is not aligned with card definition size %zX (%zX extra bytes)",
decompressed_data.size(), sizeof(CardDefinition), decompressed_data.size() % sizeof(CardDefinition)));
// The card definitions file is a standard REL file; the root offset points
// to an ArrayRef which specifies an array of CardDefinition structs
phosg::StringReader r(decompressed_data);
const auto& footer = r.pget<RELFileFooterBE>(r.size() - sizeof(RELFileFooterBE));
uint32_t offset = r.pget_u32b(footer.root_offset);
uint32_t count = r.pget_u32b(footer.root_offset + 4);
if (offset > decompressed_data.size() ||
((offset + count * sizeof(CardDefinition)) > decompressed_data.size())) {
throw runtime_error("definitions array reference out of bounds");
}
auto* defs = reinterpret_cast<CardDefinition*>(decompressed_data.data());
size_t max_cards = decompressed_data.size() / sizeof(CardDefinition);
for (size_t x = 0; x < max_cards; x++) {
CardDefinition* defs = reinterpret_cast<CardDefinition*>(decompressed_data.data() + offset);
for (size_t x = 0; x < count; x++) {
auto& def = defs[x];
// The last card entry has the build date and some other metadata (and
// isn't a real card, obviously), so skip it. The game detects this by
// checking for a negative value in type, which we also do here.
if (static_cast<int8_t>(defs[x].type) < 0) {
if (static_cast<int8_t>(def.type) < 0) {
continue;
}
auto entry = make_shared<CardEntry>(CardEntry{defs[x], "", "", "", {}});
auto entry = make_shared<CardEntry>(CardEntry{def, "", "", "", {}});
if (!this->card_definitions.emplace(entry->def.card_id, entry).second) {
throw runtime_error(phosg::string_printf(
"duplicate card id: %08" PRIX32, entry->def.card_id.load()));
@@ -2493,17 +2500,17 @@ CardIndex::CardIndex(
if (!text_filename.empty() || !decompressed_text_filename.empty()) {
try {
entry->text = std::move(card_text.at(defs[x].card_id));
entry->text = std::move(card_text.at(def.card_id));
} catch (const out_of_range&) {
}
try {
entry->debug_tags = std::move(card_tags.at(defs[x].card_id));
entry->debug_tags = std::move(card_tags.at(def.card_id));
} catch (const out_of_range&) {
}
}
if (!dice_text_filename.empty() || !decompressed_dice_text_filename.empty()) {
try {
auto& dice_text_it = card_dice_text.at(defs[x].card_id);
auto& dice_text_it = card_dice_text.at(def.card_id);
entry->dice_caption = std::move(dice_text_it.first);
entry->dice_text = std::move(dice_text_it.second);
} catch (const out_of_range&) {
@@ -2523,7 +2530,7 @@ CardIndex::CardIndex(
if (this->compressed_card_definitions.size() > 0x7BF8) {
// Try to reduce the compressed size by clearing out text
static_game_data_log.info("Compressed card list data is too long (0x%zX bytes); removing text", this->compressed_card_definitions.size());
for (size_t x = 0; x < max_cards; x++) {
for (size_t x = 0; x < count; x++) {
if (static_cast<int8_t>(defs[x].type) < 0) {
continue;
}
+2 -7
View File
@@ -13,6 +13,7 @@
#include <string>
#include <unordered_map>
#include "../CommonFileFormats.hh"
#include "../PlayerSubordinates.hh"
#include "../Text.hh"
#include "../TextIndex.hh"
@@ -814,18 +815,12 @@ struct CardDefinition {
} __packed_ws__(CardDefinition, 0x128);
struct CardDefinitionsFooter {
// Technically the card definitions file is a REL file, so the last 0x20 bytes
// here should be a separate structure.
/* 00 */ be_uint32_t num_cards1;
/* 04 */ be_uint32_t cards_offset; // == 0
/* 08 */ be_uint32_t num_cards2;
/* 0C */ parray<be_uint32_t, 3> unknown_a2;
/* 18 */ parray<be_uint16_t, 0x10> relocations;
/* 38 */ be_uint32_t relocations_offset;
/* 3C */ be_uint32_t num_relocations;
/* 40 */ parray<be_uint32_t, 2> unused1;
/* 48 */ be_uint32_t footer_offset;
/* 4C */ parray<be_uint32_t, 3> unused2;
/* 38 */ RELFileFooterBE rel_footer;
/* 58 */
} __packed_ws__(CardDefinitionsFooter, 0x58);
+1 -1
View File
@@ -2335,7 +2335,7 @@ void Server::handle_CAx1D_start_battle(shared_ptr<Client>, const string& data) {
if (l) {
// Note: Sega's implementation doesn't set EX results values here; they
// did it at game join time instead. We do it here for code simplicity.
if ((l->base_version != Version::GC_EP3_NTE) && l->ep3_ex_result_values) {
if (!this->options.is_nte() && l->ep3_ex_result_values) {
this->send(*l->ep3_ex_result_values);
}
}
+7 -4
View File
@@ -15,6 +15,7 @@
#endif
#include "CommandFormats.hh"
#include "CommonFileFormats.hh"
#include "Compression.hh"
#include "Loggers.hh"
@@ -47,16 +48,18 @@ const char* name_for_architecture(CompiledFunctionCode::Architecture arch) {
}
}
template <typename FooterT>
template <bool BE>
string CompiledFunctionCode::generate_client_command_t(
const unordered_map<string, uint32_t>& label_writes,
const void* suffix_data,
size_t suffix_size,
uint32_t override_relocations_offset) const {
using FooterT = RELFileFooterT<BE>;
FooterT footer;
footer.num_relocations = this->relocation_deltas.size();
footer.unused1.clear(0);
footer.entrypoint_addr_offset = this->entrypoint_offset_offset;
footer.root_offset = this->entrypoint_offset_offset;
footer.unused2.clear(0);
phosg::StringWriter w;
@@ -108,10 +111,10 @@ string CompiledFunctionCode::generate_client_command(
size_t suffix_size,
uint32_t override_relocations_offset) const {
if (this->arch == Architecture::POWERPC) {
return this->generate_client_command_t<S_ExecuteCode_Footer_GC_B2>(
return this->generate_client_command_t<true>(
label_writes, suffix_data, suffix_size, override_relocations_offset);
} else if ((this->arch == Architecture::X86) || (this->arch == Architecture::SH4)) {
return this->generate_client_command_t<S_ExecuteCode_Footer_DC_PC_XB_BB_B2>(
return this->generate_client_command_t<false>(
label_writes, suffix_data, suffix_size, override_relocations_offset);
} else {
throw logic_error("invalid architecture");
+1 -1
View File
@@ -40,7 +40,7 @@ struct CompiledFunctionCode {
bool is_big_endian() const;
template <typename FooterT>
template <bool BE>
std::string generate_client_command_t(
const std::unordered_map<std::string, uint32_t>& label_writes,
const void* suffix_data = nullptr,
+10 -14
View File
@@ -550,8 +550,8 @@ phosg::JSON HTTPServer::generate_game_client_json_st(shared_ptr<const Client> c,
{"SubVersion", c->sub_version},
{"Config", HTTPServer::generate_client_config_json_st(c->config)},
{"Language", name_for_language_code(c->language())},
{"LocationX", c->x},
{"LocationZ", c->z},
{"LocationX", c->pos.x.load()},
{"LocationZ", c->pos.z.load()},
{"LocationFloor", c->floor},
{"CanChat", c->can_chat},
});
@@ -735,8 +735,8 @@ phosg::JSON HTTPServer::generate_proxy_client_json_st(shared_ptr<const ProxyServ
{"Language", name_for_language_code(ses->language())},
{"LobbyClientID", ses->lobby_client_id},
{"LeaderClientID", ses->leader_client_id},
{"LocationX", ses->x},
{"LocationZ", ses->z},
{"LocationX", ses->pos.x.load()},
{"LocationZ", ses->pos.z.load()},
{"LocationFloor", ses->floor},
{"IsInGame", ses->is_in_game},
{"IsInQuest", ses->is_in_quest},
@@ -787,7 +787,6 @@ phosg::JSON HTTPServer::generate_lobby_json_st(shared_ptr<const Lobby> l, shared
ret.emplace("CheatsEnabled", l->check_flag(Lobby::Flag::CHEATS_ENABLED));
ret.emplace("MinLevel", l->min_level + 1);
ret.emplace("MaxLevel", l->max_level + 1);
ret.emplace("BaseVersion", l->base_version);
ret.emplace("Episode", name_for_episode(l->episode));
ret.emplace("HasPassword", !l->password.empty());
ret.emplace("Name", l->name);
@@ -796,11 +795,7 @@ phosg::JSON HTTPServer::generate_lobby_json_st(shared_ptr<const Lobby> l, shared
ret.emplace("QuestSelectionInProgress", l->check_flag(Lobby::Flag::QUEST_SELECTION_IN_PROGRESS));
ret.emplace("QuestInProgress", l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS));
ret.emplace("JoinableQuestInProgress", l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS));
auto variations_json = phosg::JSON::list();
for (size_t z = 0; z < l->variations.size(); z++) {
variations_json.emplace_back(l->variations[z].load());
}
ret.emplace("Variations", std::move(variations_json));
ret.emplace("Variations", l->variations.json());
ret.emplace("SectionID", name_for_section_id(l->effective_section_id()));
ret.emplace("Mode", name_for_mode(l->mode));
ret.emplace("Difficulty", name_for_difficulty(l->difficulty));
@@ -845,8 +840,8 @@ phosg::JSON HTTPServer::generate_lobby_json_st(shared_ptr<const Lobby> l, shared
const auto& item = it.second;
auto item_dict = phosg::JSON::dict({
{"LocationFloor", floor},
{"LocationX", item->x},
{"LocationZ", item->z},
{"LocationX", item->pos.x.load()},
{"LocationZ", item->pos.z.load()},
{"DropNumber", item->drop_number},
{"Flags", item->flags},
{"Data", item->data.hex()},
@@ -1016,7 +1011,9 @@ phosg::JSON HTTPServer::generate_lobbies_json() const {
return call_on_event_thread<phosg::JSON>(this->state->base, [&]() {
phosg::JSON res = phosg::JSON::list();
for (const auto& it : this->state->id_to_lobby) {
res.emplace_back(this->generate_lobby_json_st(it.second, this->state->item_name_index_opt(it.second->base_version)));
auto leader = it.second->clients[it.second->leader_id];
Version v = leader ? leader->version() : Version::BB_V4;
res.emplace_back(this->generate_lobby_json_st(it.second, this->state->item_name_index_opt(v)));
}
return res;
});
@@ -1061,7 +1058,6 @@ phosg::JSON HTTPServer::generate_summary_json() const {
auto game_json = phosg::JSON::dict({
{"ID", l->lobby_id},
{"Name", l->name},
{"BaseVersion", phosg::name_for_enum(l->base_version)},
{"Players", l->count_clients()},
{"CheatsEnabled", l->check_flag(Lobby::Flag::CHEATS_ENABLED)},
{"Episode", name_for_episode(l->episode)},
+4 -5
View File
@@ -1,5 +1,7 @@
#include "ItemParameterTable.hh"
#include "CommonFileFormats.hh"
using namespace std;
ItemParameterTable::ItemParameterTable(shared_ptr<const string> data, Version version)
@@ -416,15 +418,12 @@ ItemParameterTable::ToolV4 ItemParameterTable::ToolV3T<BE>::to_v4() const {
template <bool BE>
size_t indirect_lookup_2d_count(const phosg::StringReader& r, size_t root_offset, size_t co_index) {
using ArrayRefT = typename std::conditional_t<BE, ItemParameterTable::ArrayRefBE, ItemParameterTable::ArrayRef>;
return r.pget<ArrayRefT>(root_offset + sizeof(ArrayRefT) * co_index).count;
return r.pget<ArrayRefT<BE>>(root_offset + sizeof(ArrayRefT<BE>) * co_index).count;
}
template <typename T, bool BE>
const T& indirect_lookup_2d(const phosg::StringReader& r, size_t root_offset, size_t co_index, size_t item_index) {
using ArrayRefT = typename std::conditional_t<BE, ItemParameterTable::ArrayRefBE, ItemParameterTable::ArrayRef>;
const auto& co = r.pget<ArrayRefT>(root_offset + sizeof(ArrayRefT) * co_index);
const auto& co = r.pget<ArrayRefT<BE>>(root_offset + sizeof(ArrayRefT<BE>) * co_index);
if (item_index >= co.count) {
throw out_of_range("item ID out of range");
}
-12
View File
@@ -19,18 +19,6 @@ public:
// functions instead of manually branching on various offset table pointers
// being null or not in each public function. Rewrite this and make it better.
template <bool BE>
struct ArrayRefT {
/* 00 */ U32T<BE> count;
/* 04 */ U32T<BE> offset;
/* 08 */
} __packed__;
using ArrayRef = ArrayRefT<false>;
using ArrayRefBE = ArrayRefT<true>;
check_struct_size(ArrayRef, 8);
check_struct_size(ArrayRefBE, 8);
template <bool BE>
struct ItemBaseV2T {
// id specifies several things; notably, it doubles as the index of the
+7 -2
View File
@@ -189,9 +189,14 @@ void player_use_item(shared_ptr<Client> c, size_t item_index, shared_ptr<PSOLFGE
item.data.data1.clear_after(3);
should_delete_item = false;
// TODO: It seems that on non-BB, clients don't synchronize this at all
// so they could end up thinking the unwrapped item is something
// completely different. How does this actually work on console PSO?
auto l = c->require_lobby();
if (l->base_version == Version::BB_V4) {
send_create_inventory_item_to_lobby(c, c->lobby_client_id, item.data);
for (const auto& lc : l->clients) {
if (lc && (lc->version() == Version::BB_V4)) {
send_create_inventory_item_to_client(c, c->lobby_client_id, item.data);
}
}
break;
}
+7 -3
View File
@@ -4,6 +4,7 @@
#include <phosg/Filesystem.hh>
#include "CommonFileFormats.hh"
#include "Compression.hh"
#include "PSOEncryption.hh"
@@ -63,7 +64,8 @@ LevelTableV2::LevelTableV2(const string& data, bool compressed) {
r = phosg::StringReader(data);
}
const auto& offsets = r.pget<Offsets>(r.pget_u32l(r.size() - 0x10));
const auto& footer = r.pget<RELFileFooter>(r.size() - sizeof(RELFileFooter));
const auto& offsets = r.pget<Offsets>(footer.root_offset);
const auto& level_deltas_offsets = r.pget<parray<le_uint32_t, 9>>(offsets.level_deltas);
const auto& base_stats_offsets = r.pget<parray<le_uint32_t, 9>>(offsets.base_stats);
for (size_t char_class = 0; char_class < 9; char_class++) {
@@ -112,7 +114,8 @@ LevelTableV3BE::LevelTableV3BE(const string& data, bool encrypted) {
// u32 offset:
// u32[12] offsets:
// LevelStatsDeltaBE[200] level_deltas
const auto& offsets = r.pget<parray<be_uint32_t, 12>>(r.pget_u32b(r.pget_u32b(r.size() - 0x10)));
const auto& footer = r.pget<RELFileFooterBE>(r.size() - sizeof(RELFileFooterBE));
const auto& offsets = r.pget<parray<be_uint32_t, 12>>(r.pget_u32b(footer.root_offset));
for (size_t char_class = 0; char_class < 12; char_class++) {
const auto& src_deltas = r.pget<parray<LevelStatsDeltaBE, 200>>(offsets[char_class]);
for (size_t level = 0; level < 200; level++) {
@@ -189,7 +192,8 @@ LevelTableV4::LevelTableV4(const string& data, bool compressed) {
r = phosg::StringReader(data);
}
const auto& offsets = r.pget<Offsets>(r.pget_u32l(r.size() - 0x10));
const auto& footer = r.pget<RELFileFooter>(r.size() - sizeof(RELFileFooter));
const auto& offsets = r.pget<Offsets>(footer.root_offset);
const auto& level_deltas_offsets = r.pget<parray<le_uint32_t, 12>>(offsets.level_deltas);
const auto& base_stats_offsets = r.pget<parray<le_uint32_t, 12>>(offsets.base_stats);
for (size_t char_class = 0; char_class < 12; char_class++) {
+70 -218
View File
@@ -27,12 +27,18 @@ shared_ptr<Lobby::FloorItem> Lobby::FloorItemManager::find(uint32_t item_id) con
return this->items.at(item_id);
}
void Lobby::FloorItemManager::add(const ItemData& item, float x, float z, uint16_t flags) {
void Lobby::FloorItemManager::add(
const ItemData& item,
const VectorXZF& pos,
shared_ptr<const MapState::ObjectState> from_obj,
shared_ptr<const MapState::EnemyState> from_ene,
uint16_t flags) {
auto fi = make_shared<FloorItem>();
fi->data = item;
fi->x = x;
fi->z = z;
fi->pos = pos;
fi->drop_number = this->next_drop_number++;
fi->from_obj = from_obj;
fi->from_ene = from_ene;
fi->flags = flags;
this->add(fi);
}
@@ -52,7 +58,7 @@ void Lobby::FloorItemManager::add(shared_ptr<Lobby::FloorItem> fi) {
}
}
this->log.info("Added floor item %08" PRIX32 " at %g, %g with drop number %" PRIu64 " with flags %03hX",
fi->data.id.load(), fi->x, fi->z, fi->drop_number, fi->flags);
fi->data.id.load(), fi->pos.x.load(), fi->pos.z.load(), fi->drop_number, fi->flags);
}
std::shared_ptr<Lobby::FloorItem> Lobby::FloorItemManager::remove(uint32_t item_id, uint8_t client_id) {
@@ -71,7 +77,7 @@ std::shared_ptr<Lobby::FloorItem> Lobby::FloorItemManager::remove(uint32_t item_
}
this->items.erase(item_it);
this->log.info("Removed floor item %08" PRIX32 " at %g, %g with drop number %" PRIu64 " with flags %03hX",
fi->data.id.load(), fi->x, fi->z, fi->drop_number, fi->flags);
fi->data.id.load(), fi->pos.x.load(), fi->pos.z.load(), fi->drop_number, fi->flags);
return fi;
}
@@ -142,7 +148,6 @@ Lobby::Lobby(shared_ptr<ServerState> s, uint32_t id, bool is_game)
min_level(0),
max_level(0xFFFFFFFF),
next_game_item_id(0xCC000000),
base_version(Version::GC_V3),
allowed_versions(0x0000),
override_section_id(0xFF),
episode(Episode::NONE),
@@ -196,24 +201,22 @@ shared_ptr<Lobby::ChallengeParameters> Lobby::require_challenge_params() const {
return this->challenge_params;
}
void Lobby::set_drop_mode(DropMode new_mode) {
this->drop_mode = new_mode;
bool should_have_item_creator = (this->base_version == Version::BB_V4) ||
((new_mode != DropMode::DISABLED) && (new_mode != DropMode::CLIENT));
if (should_have_item_creator && !this->item_creator) {
this->create_item_creator();
} else if (!should_have_item_creator && this->item_creator) {
void Lobby::create_item_creator(Version logic_version) {
if (!this->is_game() || this->episode == Episode::EP3) {
this->item_creator.reset();
return;
}
}
void Lobby::create_item_creator() {
auto s = this->require_server_state();
if (logic_version == Version::UNKNOWN) {
auto leader_c = this->clients[this->leader_id];
logic_version = leader_c ? leader_c->version() : Version::BB_V4;
}
shared_ptr<const RareItemSet> rare_item_set;
shared_ptr<const CommonItemSet> common_item_set;
switch (this->base_version) {
switch (logic_version) {
case Version::PC_PATCH:
case Version::BB_PATCH:
case Version::GC_EP3_NTE:
@@ -245,6 +248,7 @@ void Lobby::create_item_creator() {
default:
throw logic_error("invalid lobby base version");
}
this->item_creator = make_shared<ItemCreator>(
common_item_set,
rare_item_set,
@@ -252,8 +256,8 @@ void Lobby::create_item_creator() {
s->tool_random_set,
s->weapon_random_sets.at(this->difficulty),
s->tekker_adjustment_set,
s->item_parameter_table(this->base_version),
s->item_stack_limits(this->base_version),
s->item_parameter_table(logic_version),
s->item_stack_limits(logic_version),
this->episode,
(this->mode == GameMode::SOLO) ? GameMode::NORMAL : this->mode,
this->difficulty,
@@ -262,21 +266,6 @@ void Lobby::create_item_creator() {
this->quest ? this->quest->battle_rules : nullptr);
}
void Lobby::change_section_id() {
if (this->item_creator) {
uint8_t new_section_id = this->effective_section_id();
if (this->item_creator->get_section_id() != new_section_id) {
this->log.info("Changing section ID to %s", name_for_section_id(new_section_id));
this->item_creator->set_section_id(new_section_id);
for (const auto& c : this->clients) {
if (c && c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
send_text_message_printf(c, "$C5Section ID changed\nto %s (%hhu)", name_for_section_id(new_section_id), new_section_id);
}
}
}
}
}
uint8_t Lobby::effective_section_id() const {
if (this->override_section_id != 0xFF) {
return this->override_section_id;
@@ -291,199 +280,55 @@ uint8_t Lobby::effective_section_id() const {
return 0;
}
shared_ptr<Map> Lobby::load_maps(
Version version,
Episode episode,
uint8_t difficulty,
uint8_t event,
uint32_t lobby_id,
shared_ptr<const Map::RareEnemyRates> rare_rates,
uint32_t random_seed,
shared_ptr<PSOLFGEncryption> opt_rand_crypt,
shared_ptr<const string> quest_dat_contents_decompressed) {
auto map = make_shared<Map>(version, lobby_id, random_seed, opt_rand_crypt);
map->add_entities_from_quest_data(episode, difficulty, event, quest_dat_contents_decompressed, rare_rates);
return map;
}
shared_ptr<Map> Lobby::load_maps(
Version version,
Episode episode,
GameMode mode,
uint8_t difficulty,
uint8_t event,
uint32_t lobby_id,
shared_ptr<const SetDataTableBase> sdt,
function<shared_ptr<const string>(Version, const string&)> get_file_data,
shared_ptr<const Map::RareEnemyRates> rare_rates,
uint32_t random_seed,
shared_ptr<PSOLFGEncryption> opt_rand_crypt,
const parray<le_uint32_t, 0x20>& variations,
const phosg::PrefixedLogger* log) {
auto enemy_filenames = sdt->map_filenames_for_variations(variations, episode, mode, SetDataTable::FilenameType::ENEMIES);
auto object_filenames = sdt->map_filenames_for_variations(variations, episode, mode, SetDataTable::FilenameType::OBJECTS);
auto event_filenames = sdt->map_filenames_for_variations(variations, episode, mode, SetDataTable::FilenameType::EVENTS);
return Lobby::load_maps(
enemy_filenames,
object_filenames,
event_filenames,
version,
episode,
mode,
difficulty,
event,
lobby_id,
get_file_data,
rare_rates,
random_seed,
opt_rand_crypt,
log);
}
shared_ptr<Map> Lobby::load_maps(
const vector<string>& enemy_filenames,
const vector<string>& object_filenames,
const vector<string>& event_filenames,
Version version,
Episode episode,
GameMode mode,
uint8_t difficulty,
uint8_t event,
uint32_t lobby_id,
function<shared_ptr<const string>(Version, const string&)> get_file_data,
shared_ptr<const Map::RareEnemyRates> rare_rates,
uint32_t rare_seed,
shared_ptr<PSOLFGEncryption> opt_rand_crypt,
const phosg::PrefixedLogger* log) {
auto map = make_shared<Map>(version, lobby_id, rare_seed, opt_rand_crypt);
// Don't load free-roam maps in Challenge mode, since players can't go to
// Ragol without a quest loaded
if (mode == GameMode::CHALLENGE) {
return map;
}
for (size_t floor = 0; floor < 0x12; floor++) {
const auto& floor_enemy_filename = enemy_filenames.at(floor);
if (!floor_enemy_filename.empty()) {
auto map_data = get_file_data(version, floor_enemy_filename);
if (map_data) {
map->add_enemies_from_map_data(
episode,
difficulty,
event,
floor,
map_data->data(),
map_data->size(),
rare_rates);
if (log) {
log->info("Loaded enemies map %s for floor %02zX", floor_enemy_filename.c_str(), floor);
}
} else if (log) {
log->info("Enemies map %s for floor %02zX cannot be used; skipping", floor_enemy_filename.c_str(), floor);
}
} else if (log) {
log->info("No enemies to load for floor %02zX", floor);
}
const auto& floor_object_filename = object_filenames.at(floor);
if (!floor_object_filename.empty()) {
auto map_data = get_file_data(version, floor_object_filename);
if (map_data) {
map->add_objects_from_map_data(floor, map_data);
if (log) {
log->info("Loaded objects map %s for floor %02zX", floor_object_filename.c_str(), floor);
}
} else if (log) {
log->info("Objects map %s for floor %02zX cannot be used; skipping", floor_object_filename.c_str(), floor);
}
} else if (log) {
log->info("No objects to load for floor %02zX", floor);
}
const auto& floor_event_filename = event_filenames.at(floor);
if (!floor_event_filename.empty()) {
auto map_data = get_file_data(version, floor_event_filename);
if (map_data) {
map->add_events_from_map_data(floor, map_data->data(), map_data->size());
if (log) {
log->info("Loaded events map %s for floor %02zX", floor_event_filename.c_str(), floor);
}
} else if (log) {
log->info("Events map %s for floor %02zX cannot be used; skipping", floor_event_filename.c_str(), floor);
}
} else if (log) {
log->info("No events to load for floor %02zX", floor);
uint16_t Lobby::quest_version_flags() const {
uint16_t ret = 0;
for (auto lc : this->clients) {
if (lc) {
ret |= (1 << static_cast<size_t>(lc->version()));
}
}
return map;
return ret;
}
void Lobby::load_maps() {
auto rare_rates = ((this->base_version == Version::BB_V4) && this->rare_enemy_rates)
? this->rare_enemy_rates
: Map::DEFAULT_RARE_ENEMIES;
auto rare_rates = this->rare_enemy_rates ? this->rare_enemy_rates : MapState::DEFAULT_RARE_ENEMIES;
if (this->quest) {
auto leader_c = this->clients.at(this->leader_id);
if (!leader_c) {
throw logic_error("lobby leader is missing");
}
auto vq = this->quest->version(this->base_version, leader_c->language());
if (!vq->dat_contents_decompressed) {
throw runtime_error("quest does not have DAT data");
}
this->map = this->load_maps(
this->base_version,
this->episode,
if (this->episode == Episode::EP3) {
this->map_state = make_shared<MapState>();
} else if (this->quest) {
this->map_state = make_shared<MapState>(
this->lobby_id,
this->difficulty,
this->event,
this->lobby_id,
rare_rates,
this->random_seed,
this->rare_enemy_rates,
this->opt_rand_crypt,
vq->dat_contents_decompressed);
} else if (this->mode != GameMode::CHALLENGE) {
auto s = this->require_server_state();
this->map = this->load_maps(
this->base_version,
this->episode,
this->mode,
this->difficulty,
this->event,
this->lobby_id,
s->set_data_table(this->base_version, this->episode, this->mode, this->difficulty),
bind(&ServerState::load_map_file, s.get(), placeholders::_1, placeholders::_2),
rare_rates,
this->random_seed,
this->opt_rand_crypt,
this->variations,
&this->log);
this->quest->get_supermap(this->random_seed));
} else {
this->map = make_shared<Map>(this->base_version, this->lobby_id, this->random_seed, this->opt_rand_crypt);
auto s = this->require_server_state();
this->map_state = make_shared<MapState>(
this->lobby_id,
this->difficulty,
this->event,
this->random_seed,
this->rare_enemy_rates,
this->opt_rand_crypt,
s->supermaps_for_variations(this->episode, this->mode, this->difficulty, this->variations));
}
this->log.info("Generated objects list (%zu entries):", this->map->objects.size());
for (size_t z = 0; z < this->map->objects.size(); z++) {
string o_str = this->map->objects[z].str();
this->log.info("(K-%zX) %s", z, o_str.c_str());
if (this->check_flag(Lobby::Flag::DEBUG)) {
this->log.info("Generated map state:");
this->map_state->print(stderr);
}
this->log.info("Generated enemies list (%zu entries):", this->map->enemies.size());
for (size_t z = 0; z < this->map->enemies.size(); z++) {
string e_str = this->map->enemies[z].str();
this->log.info("(E-%zX) %s", z, e_str.c_str());
}
[[nodiscard]] bool Lobby::is_ep3_nte() const {
for (const auto& lc : this->clients) {
if (lc && (lc->version() != Version::GC_EP3_NTE)) {
return false;
}
}
this->log.info("Generated events list (%zu entries):", this->map->events.size());
for (size_t z = 0; z < this->map->events.size(); z++) {
string e_str = this->map->events[z].str();
this->log.info("%s", e_str.c_str());
}
this->log.info("Loaded maps contain %zu object entries and %zu enemy entries overall (%zu as rares)",
this->map->objects.size(), this->map->enemies.size(), this->map->rare_enemy_indexes.size());
return true;
}
void Lobby::create_ep3_server() {
@@ -494,7 +339,8 @@ void Lobby::create_ep3_server() {
this->log.info("Recreating Episode 3 server state");
}
auto tourn = this->tournament_match ? this->tournament_match->tournament.lock() : nullptr;
bool is_nte = this->base_version == Version::GC_EP3_NTE;
bool is_nte = this->is_ep3_nte();
Episode3::Server::Options options = {
.card_index = is_nte ? s->ep3_card_index_trial : s->ep3_card_index,
.map_index = s->ep3_map_index,
@@ -504,7 +350,7 @@ void Lobby::create_ep3_server() {
.tournament = tourn,
.trap_card_ids = s->ep3_trap_card_ids,
};
if (this->base_version == Version::GC_EP3_NTE) {
if (is_nte) {
options.behavior_flags |= Episode3::BehaviorFlag::IS_TRIAL_EDITION;
} else {
options.behavior_flags &= (~Episode3::BehaviorFlag::IS_TRIAL_EDITION);
@@ -520,7 +366,7 @@ void Lobby::reassign_leader_on_client_departure(size_t leaving_client_index) {
}
if (this->clients[x]) {
this->leader_id = x;
this->change_section_id();
this->create_item_creator();
return;
}
}
@@ -611,7 +457,7 @@ void Lobby::add_client(shared_ptr<Client> c, ssize_t required_client_id) {
}
if (leader_index >= this->max_clients) {
this->leader_id = c->lobby_client_id;
this->change_section_id();
this->create_item_creator();
}
// If this is a lobby or no one was here before this, reassign all the floor
@@ -850,9 +696,15 @@ shared_ptr<Lobby::FloorItem> Lobby::find_item(uint8_t floor, uint32_t item_id) c
return this->floor_item_managers.at(floor).find(item_id);
}
void Lobby::add_item(uint8_t floor, const ItemData& data, float x, float z, uint16_t flags) {
void Lobby::add_item(
uint8_t floor,
const ItemData& data,
const VectorXZF& pos,
std::shared_ptr<const MapState::ObjectState> from_obj,
std::shared_ptr<const MapState::EnemyState> from_ene,
uint16_t flags) {
auto& m = this->floor_item_managers.at(floor);
m.add(data, x, z, flags);
m.add(data, pos, from_obj, from_ene, flags);
this->evict_items_from_floor(floor);
}
+28 -51
View File
@@ -26,9 +26,11 @@ struct ServerState;
struct Lobby : public std::enable_shared_from_this<Lobby> {
struct FloorItem {
ItemData data;
float x;
float z;
VectorXZF pos;
uint64_t drop_number;
// At most one of the following will be non-null
std::shared_ptr<const MapState::ObjectState> from_obj;
std::shared_ptr<const MapState::EnemyState> from_ene;
// The low 12 bits of flags are visibility flags, specifying which clients
// can see the item. (In practice, only the lowest 4 of these bits are used,
// but the game has fields for 12 players so we do too.)
@@ -52,7 +54,12 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
bool exists(uint32_t item_id) const;
std::shared_ptr<FloorItem> find(uint32_t item_id) const;
void add(const ItemData& item, float x, float z, uint16_t flags);
void add(
const ItemData& item,
const VectorXZF& pos,
std::shared_ptr<const MapState::ObjectState> from_obj,
std::shared_ptr<const MapState::EnemyState> from_ene,
uint16_t flags);
void add(std::shared_ptr<FloorItem> fi);
std::shared_ptr<FloorItem> remove(uint32_t item_id, uint8_t client_id);
std::unordered_set<std::shared_ptr<FloorItem>> evict();
@@ -65,6 +72,7 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
// clang-format off
GAME = 0x00000001,
PERSISTENT = 0x00000002,
DEBUG = 0x00000004,
// Flags used only for games
CHEATS_ENABLED = 0x00000100,
QUEST_SELECTION_IN_PROGRESS = 0x00000200,
@@ -103,15 +111,14 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
std::array<uint32_t, 12> next_item_id_for_client;
uint32_t next_game_item_id;
std::vector<FloorItemManager> floor_item_managers;
std::shared_ptr<const Map::RareEnemyRates> rare_enemy_rates;
std::shared_ptr<Map> map;
parray<le_uint32_t, 0x20> variations;
std::shared_ptr<const MapState::RareEnemyRates> rare_enemy_rates;
std::shared_ptr<MapState> map_state; // Always null for lobbies, never null for games
Variations variations;
std::unique_ptr<QuestFlags> quest_flags_known; // If null, ALL quest flags are known
std::unique_ptr<QuestFlags> quest_flag_values;
std::unique_ptr<SwitchFlags> switch_flags;
// Game config
Version base_version;
// Bits in allowed_versions specify who is allowed to join this game. The
// bits are indexed as (1 << version), where version is a value from the
// Version enum.
@@ -131,7 +138,7 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
std::shared_ptr<PSOLFGEncryption> opt_rand_crypt;
uint8_t allowed_drop_modes;
DropMode drop_mode;
std::shared_ptr<ItemCreator> item_creator;
std::shared_ptr<ItemCreator> item_creator; // Always null for lobbies, never null for games
struct ChallengeParameters {
uint8_t stage_number = 0;
@@ -204,55 +211,16 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
std::shared_ptr<ServerState> require_server_state() const;
std::shared_ptr<ChallengeParameters> require_challenge_params() const;
void set_drop_mode(DropMode new_mode);
void create_item_creator();
void change_section_id();
void create_item_creator(Version logic_version = Version::UNKNOWN);
uint8_t effective_section_id() const;
static std::shared_ptr<Map> load_maps(
Version version,
Episode episode,
uint8_t difficulty,
uint8_t event,
uint32_t lobby_id,
std::shared_ptr<const Map::RareEnemyRates> rare_rates,
uint32_t random_seed,
std::shared_ptr<PSOLFGEncryption> opt_rand_crypt,
std::shared_ptr<const std::string> quest_dat_contents_decompressed);
static std::shared_ptr<Map> load_maps(
Version version,
Episode episode,
GameMode mode,
uint8_t difficulty,
uint8_t event,
uint32_t lobby_id,
std::shared_ptr<const SetDataTableBase> sdt,
std::function<std::shared_ptr<const std::string>(Version, const std::string&)> get_file_data,
std::shared_ptr<const Map::RareEnemyRates> rare_rates,
uint32_t random_seed,
std::shared_ptr<PSOLFGEncryption> opt_rand_crypt,
const parray<le_uint32_t, 0x20>& variations,
const phosg::PrefixedLogger* log = nullptr);
static std::shared_ptr<Map> load_maps(
const std::vector<std::string>& enemy_filenames,
const std::vector<std::string>& object_filenames,
const std::vector<std::string>& event_filenames,
Version version,
Episode episode,
GameMode mode,
uint8_t difficulty,
uint8_t event,
uint32_t lobby_id,
std::function<std::shared_ptr<const std::string>(Version, const std::string&)> get_file_data,
std::shared_ptr<const Map::RareEnemyRates> rare_rates,
uint32_t random_seed,
std::shared_ptr<PSOLFGEncryption> opt_rand_crypt,
const phosg::PrefixedLogger* log = nullptr);
uint16_t quest_version_flags() const;
void load_maps();
void create_ep3_server();
[[nodiscard]] inline bool is_game() const {
return this->check_flag(Flag::GAME);
}
[[nodiscard]] bool is_ep3_nte() const;
[[nodiscard]] inline bool is_ep3() const {
return this->episode == Episode::EP3;
}
@@ -263,6 +231,9 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
inline void allow_version(Version v) {
this->allowed_versions |= (1 << static_cast<size_t>(v));
}
inline void forbid_version(Version v) {
this->allowed_versions &= ~(1 << static_cast<size_t>(v));
}
void reassign_leader_on_client_departure(size_t leaving_client_id);
size_t count_clients() const;
@@ -297,7 +268,13 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
bool item_exists(uint8_t floor, uint32_t item_id) const;
std::shared_ptr<FloorItem> find_item(uint8_t floor, uint32_t item_id) const;
void add_item(uint8_t floor, const ItemData& item, float x, float z, uint16_t flags);
void add_item(
uint8_t floor,
const ItemData& item,
const VectorXZF& pos,
std::shared_ptr<const MapState::ObjectState> from_obj,
std::shared_ptr<const MapState::EnemyState> from_ene,
uint16_t flags);
void add_item(uint8_t floor, std::shared_ptr<FloorItem>);
void evict_items_from_floor(uint8_t floor);
std::shared_ptr<FloorItem> remove_item(uint8_t floor, uint32_t item_id, uint8_t requesting_client_id);
+113 -233
View File
@@ -1407,7 +1407,8 @@ Action a_encode_qst(
pvr_data = make_shared<string>(phosg::load_file(pvr_filename));
} catch (const phosg::cannot_open_file&) {
}
auto vq = make_shared<VersionedQuest>(0, 0, version, 0, bin_data, dat_data, pvr_data);
auto vq = make_shared<VersionedQuest>(0, 0, version, 0, bin_data, dat_data, nullptr, pvr_data);
if (download) {
vq = vq->create_download_quest();
}
@@ -1446,21 +1447,22 @@ Action a_disassemble_quest_script(
});
Action a_disassemble_quest_map(
"disassemble-quest-map", "\
disassemble-quest-map [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
disassemble-quest-map [OPTIONS] [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
Disassemble the input quest map (.dat file) into a text representation of\n\
the data it contains.\n",
the data it contains. If --decompressed is given, don\'t decompress before\n\
disassembling.\n",
+[](phosg::Arguments& args) {
string data = read_input_data(args);
auto data = make_shared<string>(read_input_data(args));
if (!args.get<bool>("decompressed")) {
data = prs_decompress(data);
*data = prs_decompress(*data);
}
string result = Map::disassemble_quest_data(data.data(), data.size());
string result = MapFile(data).disassemble();
write_output_data(args, result.data(), result.size(), "txt");
});
Action a_disassemble_free_map(
"disassemble-free-map", "\
disassemble-free-map INPUT-FILENAME [OUTPUT-FILENAME]\n\
Disassemble the input free-roam map (.dat or .evt file) into a text\n\
Disassemble the input free-play map (.dat or .evt file) into a text\n\
representation of the data it contains. Unlike othe disassembly actions,\n\
this action expects its input to be already decompressed. If the input is\n\
compressed, use the --compressed option. Also unlike other options, the\n\
@@ -1475,18 +1477,19 @@ Action a_disassemble_free_map(
throw runtime_error("cannot determine input file type");
}
string data = read_input_data(args);
auto data = make_shared<string>(read_input_data(args));
if (args.get<bool>("compressed")) {
data = prs_decompress(data);
*data = prs_decompress(*data);
}
uint8_t floor = args.get<uint8_t>("floor", 0);
string result;
if (is_objects) {
result = Map::disassemble_objects_data(data.data(), data.size());
result = MapFile(floor, data, nullptr, nullptr).disassemble();
} else if (is_enemies) {
result = Map::disassemble_enemies_data(data.data(), data.size());
result = MapFile(floor, nullptr, data, nullptr).disassemble();
} else if (is_events) {
result = Map::disassemble_wave_events_data(data.data(), data.size());
result = MapFile(floor, nullptr, nullptr, data).disassemble();
} else {
throw logic_error("unhandled input type");
}
@@ -1912,7 +1915,7 @@ Action a_convert_rare_item_set(
auto data = make_shared<string>(read_input_data(args));
shared_ptr<RareItemSet> rs;
if (phosg::ends_with(input_filename, ".json")) {
rs = make_shared<RareItemSet>(phosg::JSON::parse(*data), s->item_name_index(version));
rs = make_shared<RareItemSet>(phosg::JSON::parse(*data), s->item_name_index_opt(version));
} else if (phosg::ends_with(input_filename, ".gsl")) {
rs = make_shared<RareItemSet>(GSLArchive(data, false), false);
} else if (phosg::ends_with(input_filename, ".gslb")) {
@@ -1931,9 +1934,9 @@ Action a_convert_rare_item_set(
string output_filename = args.get<string>(2, false);
if (output_filename.empty() || (output_filename == "-")) {
rs->print_all_collections(stdout, s->item_name_index(version));
rs->print_all_collections(stdout, s->item_name_index_opt(version));
} else if (phosg::ends_with(output_filename, ".json")) {
auto json = rs->json(s->item_name_index(version));
auto json = rs->json(s->item_name_index_opt(version));
string data = json.serialize(phosg::JSON::SerializeOption::FORMAT | phosg::JSON::SerializeOption::HEX_INTEGERS | phosg::JSON::SerializeOption::SORT_DICT_KEYS);
write_output_data(args, data.data(), data.size(), nullptr);
} else if (phosg::ends_with(output_filename, ".gsl")) {
@@ -2088,23 +2091,21 @@ Action a_name_all_items(
}
fprintf(stderr, "IDENT :");
for (size_t v_s = 0; v_s < NUM_VERSIONS; v_s++) {
Version version = static_cast<Version>(v_s);
const auto& index = s->item_name_indexes.at(v_s);
for (Version v : ALL_VERSIONS) {
const auto& index = s->item_name_index_opt(v);
if (index) {
fprintf(stderr, " %30s ", phosg::name_for_enum(version));
fprintf(stderr, " %30s ", phosg::name_for_enum(v));
}
}
fputc('\n', stderr);
for (uint32_t primary_identifier : all_primary_identifiers) {
fprintf(stderr, "%08" PRIX32 ":", primary_identifier);
for (size_t v_s = 0; v_s < NUM_VERSIONS; v_s++) {
const auto& index = s->item_name_indexes.at(v_s);
for (Version v : ALL_VERSIONS) {
const auto& index = s->item_name_index_opt(v);
if (index) {
Version version = static_cast<Version>(v_s);
auto pmt = s->item_parameter_table(version);
ItemData item = ItemData::from_primary_identifier(*s->item_stack_limits(version), primary_identifier);
auto pmt = s->item_parameter_table(v);
ItemData item = ItemData::from_primary_identifier(*s->item_stack_limits(v), primary_identifier);
string name = index->describe_item(item);
try {
bool is_rare = pmt->is_item_rare(item);
@@ -2212,10 +2213,9 @@ Action a_print_item_parameter_tables(
s->load_text_index(false);
s->load_item_definitions(false);
s->load_item_name_indexes(false);
for (size_t v_s = 0; v_s < NUM_VERSIONS; v_s++) {
const auto& index = s->item_name_indexes.at(v_s);
for (Version v : ALL_VERSIONS) {
const auto& index = s->item_name_index_opt(v);
if (index) {
Version v = static_cast<Version>(v_s);
fprintf(stdout, "======== %s\n", phosg::name_for_enum(v));
index->print_table(stdout);
}
@@ -2569,224 +2569,104 @@ Action a_show_battle_params(
s->battle_params->get_table(true, Episode::EP4).print(stdout);
});
Action a_find_rare_enemy_seeds(
"find-rare-enemy-seeds", "\
find-rare-enemy-seeds OPTIONS...\n\
Search all possible rare seeds to find those that produce one or more rare\n\
enemies in any set of variations. A version option (e.g. --gc) is required;\n\
an episode option (--ep1, --ep2, or --ep4) is also required. A difficulty\n\
option (--normal, --hard, --very-hard, or --ultimate) may be given; this\n\
affects which rare rates from config.json are used if --bb was given.\n\
Similarly, --battle, --challenge, or --solo may also be given; this affects\n\
which variations are used on all versions and which rare rates to use for\n\
BB. --threads=COUNT controls the number of threads to use for the search\n\
by default, one thread per CPU core is used. --min-count specifies how many\n\
rare enemies must be found to output the seed. Finally, --quest=NAME may be\n\
given to use that quest\'s map instead of the free-roam maps.\n",
+[](phosg::Arguments& args) {
auto version = get_cli_version(args);
auto episode = get_cli_episode(args);
auto difficulty = get_cli_difficulty(args);
auto mode = get_cli_game_mode(args);
size_t num_threads = args.get<size_t>("threads", 0);
size_t min_count = args.get<size_t>("min-count", 1);
string quest_name = args.get<string>("quest", false);
auto s = make_shared<ServerState>(get_config_filename(args));
shared_ptr<const VersionedQuest> vq;
if (!quest_name.empty()) {
s->load_config_early();
s->load_quest_index(false);
auto q = s->quest_index(version)->get(quest_name);
if (!q) {
throw runtime_error("quest does not exist");
}
vq = q->version(version, 1);
if (!vq) {
throw runtime_error("quest version does not exist");
}
} else if (version == Version::BB_V4) {
s->load_config_early();
} else if (version == Version::PC_V2) {
s->load_patch_indexes(false);
} else {
s->clear_file_caches(false);
}
shared_ptr<const Map::RareEnemyRates> rare_rates;
if (version != Version::BB_V4) {
rare_rates = Map::DEFAULT_RARE_ENEMIES;
} else if (mode == GameMode::CHALLENGE) {
rare_rates = s->rare_enemy_rates_challenge;
} else {
rare_rates = s->rare_enemy_rates_by_difficulty[difficulty];
}
mutex output_lock;
auto thread_fn = [&](uint64_t seed, size_t) -> bool {
auto random_crypt = make_shared<PSOV2Encryption>(seed);
parray<le_uint32_t, 0x20> variations;
shared_ptr<Map> map;
if (vq) {
if (!vq->dat_contents_decompressed) {
throw runtime_error("quest does not have DAT data");
}
map = Lobby::load_maps(
version, episode, difficulty, 0, 0, rare_rates, seed, random_crypt, vq->dat_contents_decompressed);
} else {
generate_variations_deprecated(variations, random_crypt, version, episode, (mode == GameMode::SOLO));
map = Lobby::load_maps(
version,
episode,
mode,
difficulty,
0,
0,
s->set_data_table(version, episode, mode, difficulty),
bind(&ServerState::load_map_file, s.get(), placeholders::_1, placeholders::_2),
rare_rates,
seed,
random_crypt,
variations);
}
vector<size_t> rare_indexes;
for (size_t z = 0; z < map->enemies.size(); z++) {
if (enemy_type_is_rare(map->enemies[z].type)) {
rare_indexes.emplace_back(z);
}
}
if (rare_indexes.size() >= min_count) {
lock_guard g(output_lock);
fprintf(stdout, "%08" PRIX64 ":", seed);
for (size_t index : rare_indexes) {
fprintf(stdout, " E-%zX:%s", index, phosg::name_for_enum(map->enemies[index].type));
}
fprintf(stdout, "\n");
}
return false;
};
phosg::parallel_range_blocks<uint64_t>(thread_fn, 0, 0x100000000, 0x1000, num_threads, nullptr);
});
Action a_load_maps_test(
"load-maps-test", nullptr, +[](phosg::Arguments& args) {
using SDT = SetDataTable;
bool save_disassembly = args.get<bool>("disassemble");
auto s = make_shared<ServerState>(get_config_filename(args));
s->load_config_early();
s->clear_file_caches(false);
s->load_patch_indexes(false);
s->load_set_data_tables(false);
s->load_quest_index(false);
for (size_t v_s = NUM_PATCH_VERSIONS; v_s < NUM_VERSIONS; v_s++) {
Version v = static_cast<Version>(v_s);
if (is_ep3(v)) {
continue;
}
const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
for (Episode episode : episodes) {
if (episode == Episode::EP4 && !is_v4(v)) {
continue;
}
if (episode == Episode::EP2 && is_v1_or_v2(v) && (v != Version::GC_NTE)) {
continue;
}
const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
for (GameMode mode : modes) {
if ((mode == GameMode::BATTLE || mode == GameMode::CHALLENGE) && is_v1(v)) {
continue;
}
if (mode == GameMode::SOLO && !is_v4(v)) {
continue;
}
for (uint8_t difficulty = 0; difficulty < 4; difficulty++) {
if (difficulty == 3 && is_v1(v)) {
continue;
}
auto sdt = s->set_data_table(v, episode, mode, difficulty);
for (uint8_t floor = 0; floor < 0x12; floor++) {
auto variation_maxes = sdt->num_free_roam_variations_for_floor(episode, mode == GameMode::SOLO, floor);
for (size_t var1 = 0; var1 < variation_maxes.first; var1++) {
for (size_t var2 = 0; var2 < variation_maxes.second; var2++) {
auto enemies_filename = sdt->map_filename_for_variation(
floor, var1, var2, episode, mode, SDT::FilenameType::ENEMIES);
auto objects_filename = sdt->map_filename_for_variation(
floor, var1, var2, episode, mode, SDT::FilenameType::OBJECTS);
auto events_filename = sdt->map_filename_for_variation(
floor, var1, var2, episode, mode, SDT::FilenameType::EVENTS);
s->load_maps(false);
fprintf(stderr, "... %s %s %s %s %02hhX %zX %zX",
phosg::name_for_enum(v), name_for_episode(episode), name_for_mode(mode), name_for_difficulty(difficulty), floor, var1, var2);
auto map = make_shared<Map>(v, 0, 0, nullptr);
if (!enemies_filename.empty()) {
fprintf(stderr, " [%s => ", enemies_filename.c_str());
auto map_data = s->load_map_file(v, enemies_filename);
if (map_data) {
map->add_enemies_from_map_data(
episode, difficulty, 0, 0, map_data->data(), map_data->size(), Map::DEFAULT_RARE_ENEMIES);
fprintf(stderr, "%zu enemies, %zu sets]", map->enemies.size(), map->enemy_set_flags.size());
} else {
fprintf(stderr, "__MISSING__]");
}
}
if (!objects_filename.empty()) {
fprintf(stderr, " [%s => ", objects_filename.c_str());
auto map_data = s->load_map_file(v, objects_filename);
if (map_data) {
map->add_objects_from_map_data(floor, map_data);
fprintf(stderr, "%zu objects]", map->objects.size());
} else {
fprintf(stderr, "__MISSING__]");
}
}
if (!events_filename.empty()) {
fprintf(stderr, " [%s => ", events_filename.c_str());
auto map_data = s->load_map_file(v, events_filename);
if (map_data) {
map->add_events_from_map_data(floor, map_data->data(), map_data->size());
fprintf(stderr, "%zu events, %zu action bytes]", map->events.size(), map->event_action_stream.size());
} else {
fprintf(stderr, "__MISSING__]");
}
}
fputc('\n', stderr);
}
}
}
}
}
for (const auto& it : s->supermaps) {
auto episode = static_cast<Episode>((it.first >> 28) & 7);
auto mode = static_cast<GameMode>((it.first >> 26) & 3);
uint8_t difficulty = (it.first >> 24) & 3;
uint8_t floor = (it.first >> 16) & 0xFF;
uint8_t layout = (it.first >> 8) & 0xFF;
uint8_t entities = (it.first >> 0) & 0xFF;
fprintf(stderr, "FREE MAP: %08" PRIX32 " => %s %s %c floor=%02hhX layout=%02hhX entities=%02hhX\n",
it.first,
abbreviation_for_episode(episode),
abbreviation_for_mode(mode),
abbreviation_for_difficulty(difficulty),
floor, layout, entities);
if (save_disassembly) {
string filename = phosg::string_printf(
"supermap_%s_%s_%c_%02hhX_%02hhx_%02hhX.txt",
abbreviation_for_episode(episode),
abbreviation_for_mode(mode),
abbreviation_for_difficulty(difficulty),
floor, layout, entities);
auto f = phosg::fopen_unique(filename, "wt");
it.second->print(f.get());
}
}
for (const auto& q_it : s->default_quest_index->quests_by_number) {
for (const auto& vq_it : q_it.second->versions) {
auto vq = vq_it.second;
shared_ptr<const Map> map = Lobby::load_maps(
vq->version,
vq->episode,
0,
0,
0,
Map::DEFAULT_RARE_ENEMIES,
0,
nullptr,
vq->dat_contents_decompressed);
fprintf(stderr, "... %" PRIu32 " (%s) %s %s %s => %zu enemies (%zu sets), %zu objects, %zu events\n",
vq->quest_number,
vq->name.c_str(),
name_for_episode(vq->episode),
phosg::name_for_enum(vq->version),
name_for_language_code(vq->language),
map->enemies.size(),
map->enemy_set_flags.size(),
map->objects.size(),
map->events.size());
// Generate MapStates for a few random variations
for (size_t z = 0; z < 0x20; z++) {
static const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
static const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
Episode episode = episodes[phosg::random_object<uint32_t>() % episodes.size()];
GameMode mode = modes[phosg::random_object<uint32_t>() % modes.size()];
uint8_t difficulty = phosg::random_object<uint32_t>() % 4;
uint8_t event = phosg::random_object<uint32_t>() % 8;
uint32_t random_seed = phosg::random_object<uint32_t>();
fprintf(stderr, "FREE MAP STATE TEST: %s %s %c\n",
abbreviation_for_episode(episode),
abbreviation_for_mode(mode),
abbreviation_for_difficulty(difficulty));
auto sdt = s->set_data_table(Version::BB_V4, episode, mode, difficulty);
auto variations = sdt->generate_variations(episode, (mode == GameMode::SOLO), nullptr);
auto supermaps = s->supermaps_for_variations(episode, mode, difficulty, variations);
auto map_state = make_shared<MapState>(
0, difficulty, event, random_seed, MapState::DEFAULT_RARE_ENEMIES, nullptr, supermaps);
map_state->verify();
fprintf(stderr, " map state ok: 0x%zX objects, 0x%zX enemies, 0x%zX enemy sets, 0x%zX events\n",
map_state->object_states.size(),
map_state->enemy_states.size(),
map_state->enemy_set_states.size(),
map_state->event_states.size());
}
s->load_quest_index(false);
uint32_t random_seed = args.get<uint32_t>("random-seed", 0, phosg::Arguments::IntFormat::HEX);
for (const auto& it : s->default_quest_index->quests_by_number) {
auto supermap = it.second->get_supermap(random_seed);
if (!supermap) {
fprintf(stderr, "QUEST MAP: %08" PRIX32 " => (no supermap)\n", it.first);
} else {
string filename = phosg::string_printf("supermap_quest_%" PRIu32 "_%08" PRIX32 ".txt", it.first, random_seed);
fprintf(stderr, "QUEST MAP: %08" PRIX32 " => %s\n", it.first, filename.c_str());
if (save_disassembly) {
auto f = phosg::fopen_unique(filename, "wt");
fprintf(f.get(), "QUEST %" PRIu32 " (%s)\n", it.first, it.second->name.c_str());
supermap->print(f.get());
}
}
auto map_state = make_shared<MapState>(
0,
phosg::random_object<uint8_t>() & 3,
0,
phosg::random_object<uint32_t>(),
MapState::DEFAULT_RARE_ENEMIES,
nullptr,
supermap);
map_state->verify();
fprintf(stderr, " map state ok: 0x%zX objects, 0x%zX enemies, 0x%zX enemy sets, 0x%zX events\n",
map_state->object_states.size(),
map_state->enemy_states.size(),
map_state->enemy_set_states.size(),
map_state->event_states.size());
}
});
+3598 -2140
View File
File diff suppressed because it is too large Load Diff
+806 -406
View File
File diff suppressed because it is too large Load Diff
+66 -54
View File
@@ -746,7 +746,7 @@ static HandlerResult S_B2(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t,
ses->log.info("Wrote code from server to file %s", output_filename.c_str());
#ifdef HAVE_RESOURCE_FILE
using FooterT = S_ExecuteCode_FooterT_B2<BE>;
using FooterT = RELFileFooterT<BE>;
// TODO: Support SH-4 disassembly too
bool is_ppc = ::is_ppc(ses->version());
@@ -770,9 +770,9 @@ static HandlerResult S_B2(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t,
reloc_offset += (r.get<U16T<BE>>() * 4);
labels.emplace(reloc_offset, phosg::string_printf("reloc%zu", x));
}
labels.emplace(footer.entrypoint_addr_offset.load(), "entry_ptr");
labels.emplace(footer.root_offset.load(), "entry_ptr");
labels.emplace(footer_offset, "footer");
labels.emplace(r.pget<U32T<BE>>(footer.entrypoint_addr_offset), "start");
labels.emplace(r.pget<U32T<BE>>(footer.root_offset), "start");
string disassembly;
if (is_ppc) {
@@ -1002,9 +1002,9 @@ static HandlerResult SC_6x60_6xA2(shared_ptr<ProxyServer::LinkedSession> ses, co
G_SpecializableItemDropRequest_6xA2 cmd = normalize_drop_request(data.data(), data.size());
auto s = ses->require_server_state();
ses->next_drop_item.id = ses->next_item_id++;
bool is_box = (cmd.rt_index == 0x30);
send_drop_item_to_channel(s, ses->server_channel, ses->next_drop_item, !is_box, cmd.floor, cmd.x, cmd.z, cmd.entity_id);
send_drop_item_to_channel(s, ses->client_channel, ses->next_drop_item, !is_box, cmd.floor, cmd.x, cmd.z, cmd.entity_id);
uint8_t source_type = (cmd.rt_index == 0x30) ? 2 : 1;
send_drop_item_to_channel(s, ses->server_channel, ses->next_drop_item, source_type, cmd.floor, cmd.pos, cmd.entity_index);
send_drop_item_to_channel(s, ses->client_channel, ses->next_drop_item, source_type, cmd.floor, cmd.pos, cmd.entity_index);
ses->next_drop_item.clear();
return HandlerResult::Type::SUPPRESS;
}
@@ -1025,27 +1025,34 @@ static HandlerResult SC_6x60_6xA2(shared_ptr<ProxyServer::LinkedSession> ses, co
ses->log.warning("Session is in INTERCEPT drop mode, but item creator is missing");
return HandlerResult::Type::FORWARD;
}
if (!ses->map) {
ses->log.warning("Session is in INTERCEPT drop mode, but map is missing");
if (!ses->map_state) {
ses->log.warning("Session is in INTERCEPT drop mode, but map state is missing");
return HandlerResult::Type::FORWARD;
}
G_SpecializableItemDropRequest_6xA2 cmd = normalize_drop_request(data.data(), data.size());
auto rec = reconcile_drop_request_with_map(
ses->log, ses->client_channel, cmd, ses->version(), ses->lobby_episode, ses->config, ses->map, false);
ses->log,
ses->client_channel,
cmd,
ses->lobby_episode,
ses->lobby_event,
ses->config,
ses->map_state,
false);
ItemCreator::DropResult res;
if (rec.is_box) {
if (rec.obj_st) {
if (rec.ignore_def) {
ses->log.info("Creating item from box %04hX (area %02hX)", cmd.entity_id.load(), cmd.effective_area);
ses->log.info("Creating item from box %04hX (area %02hX)", cmd.entity_index.load(), cmd.effective_area);
res = ses->item_creator->on_box_item_drop(cmd.effective_area);
} else {
ses->log.info("Creating item from box %04hX (area %02hX; specialized with %g %08" PRIX32 " %08" PRIX32 " %08" PRIX32 ")",
cmd.entity_id.load(), cmd.effective_area, cmd.param3.load(), cmd.param4.load(), cmd.param5.load(), cmd.param6.load());
cmd.entity_index.load(), cmd.effective_area, cmd.param3.load(), cmd.param4.load(), cmd.param5.load(), cmd.param6.load());
res = ses->item_creator->on_specialized_box_item_drop(cmd.effective_area, cmd.param3, cmd.param4, cmd.param5, cmd.param6);
}
} else {
ses->log.info("Creating item from enemy %04hX (area %02hX)", cmd.entity_id.load(), cmd.effective_area);
ses->log.info("Creating item from enemy %04hX (area %02hX)", cmd.entity_index.load(), cmd.effective_area);
res = ses->item_creator->on_monster_item_drop(rec.effective_rt_index, cmd.effective_area);
}
@@ -1054,12 +1061,12 @@ static HandlerResult SC_6x60_6xA2(shared_ptr<ProxyServer::LinkedSession> ses, co
} else {
auto s = ses->require_server_state();
string name = s->describe_item(ses->version(), res.item, false);
ses->log.info("Entity %04hX (area %02hX) created item %s", cmd.entity_id.load(), cmd.effective_area, name.c_str());
ses->log.info("Entity %04hX (area %02hX) created item %s", cmd.entity_index.load(), cmd.effective_area, name.c_str());
res.item.id = ses->next_item_id++;
ses->log.info("Creating item %08" PRIX32 " at %02hhX:%g,%g for all clients",
res.item.id.load(), cmd.floor, cmd.x.load(), cmd.z.load());
send_drop_item_to_channel(s, ses->client_channel, res.item, !rec.is_box, cmd.floor, cmd.x, cmd.z, cmd.entity_id);
send_drop_item_to_channel(s, ses->server_channel, res.item, !rec.is_box, cmd.floor, cmd.x, cmd.z, cmd.entity_id);
res.item.id.load(), cmd.floor, cmd.pos.x.load(), cmd.pos.z.load());
send_drop_item_to_channel(s, ses->client_channel, res.item, rec.obj_st ? 2 : 1, cmd.floor, cmd.pos, cmd.entity_index);
send_drop_item_to_channel(s, ses->server_channel, res.item, rec.obj_st ? 2 : 1, cmd.floor, cmd.pos, cmd.entity_index);
send_item_notification_if_needed(s, ses->client_channel, ses->config, res.item, res.is_from_rare_table);
}
return HandlerResult::Type::SUPPRESS;
@@ -1144,7 +1151,7 @@ static HandlerResult S_6x(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t,
const auto& cmd = check_size_t<G_AttackFinished_6x46>(data,
offsetof(G_AttackFinished_6x46, targets),
sizeof(G_AttackFinished_6x46));
if (cmd.count > min<size_t>(cmd.header.size - 2, cmd.targets.size())) {
if (cmd.target_count > min<size_t>(cmd.header.size - 2, cmd.targets.size())) {
ses->log.warning("Blocking subcommand 6x46 with invalid count");
return HandlerResult::Type::SUPPRESS;
}
@@ -1426,18 +1433,25 @@ static HandlerResult S_13_A7(shared_ptr<ProxyServer::LinkedSession> ses, uint16_
if (!sf->is_download && phosg::ends_with(sf->basename, ".dat")) {
auto quest_dat_data = make_shared<std::string>(prs_decompress(sf->data));
try {
ses->map = Lobby::load_maps(
ses->version(),
ses->lobby_episode,
auto map_file = make_shared<MapFile>(quest_dat_data);
auto materialized_map_file = map_file->materialize_random_sections(ses->lobby_random_seed);
array<shared_ptr<const MapFile>, NUM_VERSIONS> map_files;
map_files.at(static_cast<size_t>(ses->version())) = materialized_map_file;
auto supermap = make_shared<SuperMap>(ses->lobby_episode, map_files);
ses->map_state = make_shared<MapState>(
ses->id,
ses->lobby_difficulty,
ses->lobby_event,
ses->id,
Map::DEFAULT_RARE_ENEMIES,
ses->lobby_random_seed,
MapState::DEFAULT_RARE_ENEMIES,
make_shared<PSOV2Encryption>(ses->lobby_random_seed),
quest_dat_data);
supermap);
} catch (const exception& e) {
ses->log.warning("Failed to load quest map: %s", e.what());
ses->map_state.reset();
}
}
@@ -1578,7 +1592,7 @@ static HandlerResult S_65_67_68_EB(shared_ptr<ProxyServer::LinkedSession> ses, u
ses->lobby_episode = Episode::EP1;
ses->lobby_random_seed = 0;
ses->item_creator.reset();
ses->map.reset();
ses->map_state.reset();
// This command can cause the client to no longer send D6 responses when
// 1A/D5 large message boxes are closed. newserv keeps track of this
@@ -1720,7 +1734,7 @@ static HandlerResult S_64(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t,
} else {
ses->lobby_mode = GameMode::NORMAL;
}
ses->lobby_random_seed = cmd->rare_seed;
ses->lobby_random_seed = cmd->random_seed;
if (cmd_ep3) {
ses->lobby_episode = Episode::EP3;
} else {
@@ -1730,27 +1744,24 @@ static HandlerResult S_64(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t,
if (ses->version() == Version::GC_NTE) {
// GC NTE ignores the variations field entirely, so clear the array to
// ensure we'll load the correct maps
cmd->variations.clear(0);
cmd->variations = Variations();
}
// Recreate the item creator if needed, and load maps
auto s = ses->require_server_state();
ses->set_drop_mode(ses->drop_mode);
if (!is_ep3(ses->version())) {
ses->map = Lobby::load_maps(
ses->version(),
ses->lobby_episode,
ses->lobby_mode,
if (!is_ep3(ses->version()) && (ses->lobby_mode != GameMode::CHALLENGE)) {
auto s = ses->require_server_state();
ses->map_state = make_shared<MapState>(
ses->id,
ses->lobby_difficulty,
ses->lobby_event,
ses->id,
s->set_data_table(ses->version(), ses->lobby_episode, ses->lobby_mode, ses->lobby_difficulty),
bind(&ServerState::load_map_file, s.get(), placeholders::_1, placeholders::_2),
Map::DEFAULT_RARE_ENEMIES,
ses->lobby_random_seed,
MapState::DEFAULT_RARE_ENEMIES,
make_shared<PSOV2Encryption>(ses->lobby_random_seed),
cmd->variations,
&ses->log);
s->supermaps_for_variations(ses->lobby_episode, ses->lobby_mode, ses->lobby_difficulty, cmd->variations));
} else {
ses->map_state.reset();
}
bool modified = false;
@@ -1785,7 +1796,7 @@ static HandlerResult S_64(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t,
modified = true;
}
if (ses->config.check_flag(Client::Flag::USE_OVERRIDE_RANDOM_SEED)) {
cmd->rare_seed = ses->config.override_random_seed;
cmd->random_seed = ses->config.override_random_seed;
modified = true;
}
@@ -1812,7 +1823,7 @@ static HandlerResult S_E8(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t,
ses->lobby_random_seed = 0;
ses->lobby_episode = Episode::EP3;
ses->item_creator.reset();
ses->map.reset();
ses->map_state.reset();
bool modified = false;
@@ -1850,7 +1861,7 @@ static HandlerResult S_E8(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t,
modified = true;
}
if (ses->config.check_flag(Client::Flag::USE_OVERRIDE_RANDOM_SEED)) {
cmd.rare_seed = ses->config.override_random_seed;
cmd.random_seed = ses->config.override_random_seed;
modified = true;
}
@@ -1899,7 +1910,7 @@ static HandlerResult C_98(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t c
ses->lobby_mode = GameMode::NORMAL;
ses->lobby_random_seed = 0;
ses->item_creator.reset();
ses->map.reset();
ses->map_state.reset();
if (is_v3(ses->version()) || is_v4(ses->version())) {
return C_GXB_61(ses, command, flag, data);
@@ -1999,9 +2010,7 @@ constexpr on_command_t C_B_81 = &C_81<SC_SimpleMail_BB_81>;
template <typename CmdT>
void C_6x_movement(shared_ptr<ProxyServer::LinkedSession> ses, const string& data) {
const auto& cmd = check_size_t<CmdT>(data);
ses->x = cmd.x;
ses->z = cmd.z;
ses->pos = check_size_t<CmdT>(data).pos;
}
template <typename SendGuildCardCmdT>
@@ -2030,19 +2039,22 @@ HandlerResult C_6x<void>(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, u
if (!data.empty()) {
if ((data[0] == 0x05) && ses->config.check_flag(Client::Flag::SWITCH_ASSIST_ENABLED)) {
auto& cmd = check_size_t<G_SwitchStateChanged_6x05>(data);
if (ses->map && (cmd.flags & 1) && (cmd.header.object_id != 0xFFFF)) {
for (auto* door : ses->map->doors_for_switch_flag(cmd.switch_flag_floor, cmd.switch_flag_num)) {
if (door->game_flags & 0x0001) {
if (ses->map_state && (cmd.flags & 1) && (cmd.header.entity_id != 0xFFFF)) {
auto door_states = ses->map_state->door_states_for_switch_flag(
ses->version(), cmd.switch_flag_floor, cmd.switch_flag_num);
for (auto& door_state : door_states) {
if (door_state->game_flags & 0x0001) {
continue;
}
door->game_flags |= 1;
door_state->game_flags |= 1;
uint16_t object_index = ses->map_state->index_for_object_state(ses->version(), door_state);
G_UpdateObjectState_6x0B cmd0B;
cmd0B.header.subcommand = 0x0B;
cmd0B.header.size = sizeof(cmd0B) / 4;
cmd0B.header.client_id = door->object_id | 0x4000;
cmd0B.flags = door->game_flags;
cmd0B.object_index = door->object_id;
cmd0B.header.entity_id = object_index | 0x4000;
cmd0B.flags = door_state->game_flags;
cmd0B.object_index = object_index;
ses->client_channel.send(0x60, 0x00, &cmd0B, sizeof(cmd0B));
ses->server_channel.send(0x60, 0x00, &cmd0B, sizeof(cmd0B));
}
@@ -2075,8 +2087,8 @@ HandlerResult C_6x<void>(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, u
} else if (data[0] == 0x40) {
C_6x_movement<G_WalkToPosition_6x40>(ses, data);
} else if (data[0] == 0x42) {
C_6x_movement<G_RunToPosition_6x42>(ses, data);
} else if ((data[0] == 0x41) || (data[0] == 0x42)) {
C_6x_movement<G_MoveToPosition_6x41_6x42>(ses, data);
} else if (data[0] == 0x48) {
if (ses->config.check_flag(Client::Flag::INFINITE_TP_ENABLED)) {
-2
View File
@@ -547,8 +547,6 @@ ProxyServer::LinkedSession::LinkedSession(
lobby_client_id(0),
leader_client_id(0),
floor(0),
x(0.0),
z(0.0),
is_in_game(false),
is_in_quest(false),
lobby_event(0),
+2 -3
View File
@@ -81,7 +81,7 @@ public:
DropMode drop_mode;
std::shared_ptr<std::string> quest_dat_data;
std::shared_ptr<ItemCreator> item_creator;
std::shared_ptr<Map> map;
std::shared_ptr<MapState> map_state;
struct LobbyPlayer {
uint32_t guild_card_number = 0;
@@ -95,8 +95,7 @@ public:
size_t lobby_client_id;
size_t leader_client_id;
uint16_t floor;
float x;
float z;
VectorXZF pos;
bool is_in_game;
bool is_in_quest;
uint8_t lobby_event;
+98 -23
View File
@@ -199,6 +199,7 @@ VersionedQuest::VersionedQuest(
uint8_t language,
std::shared_ptr<const std::string> bin_contents,
std::shared_ptr<const std::string> dat_contents,
std::shared_ptr<const MapFile> map_file,
std::shared_ptr<const std::string> pvr_contents,
std::shared_ptr<const BattleRules> battle_rules,
ssize_t challenge_template_index,
@@ -219,6 +220,7 @@ VersionedQuest::VersionedQuest(
is_dlq_encoded(false),
bin_contents(bin_contents),
dat_contents(dat_contents),
map_file(map_file),
pvr_contents(pvr_contents),
battle_rules(battle_rules),
challenge_template_index(challenge_template_index),
@@ -226,10 +228,6 @@ VersionedQuest::VersionedQuest(
available_expression(available_expression),
enabled_expression(enabled_expression) {
if (this->dat_contents) {
this->dat_contents_decompressed = make_shared<string>(prs_decompress(*this->dat_contents));
}
auto bin_decompressed = prs_decompress(*this->bin_contents);
switch (this->version) {
@@ -390,12 +388,13 @@ Quest::Quest(shared_ptr<const VersionedQuest> initial_version)
joinable(initial_version->joinable),
lock_status_register(initial_version->lock_status_register),
name(initial_version->name),
supermap(nullptr),
battle_rules(initial_version->battle_rules),
challenge_template_index(initial_version->challenge_template_index),
description_flag(initial_version->description_flag),
available_expression(initial_version->available_expression),
enabled_expression(initial_version->enabled_expression) {
this->versions.emplace(this->versions_key(initial_version->version, initial_version->language), initial_version);
this->add_version(initial_version);
}
uint32_t Quest::versions_key(Version v, uint8_t language) {
@@ -449,6 +448,44 @@ void Quest::add_version(shared_ptr<const VersionedQuest> vq) {
this->versions.emplace(this->versions_key(vq->version, vq->language), vq);
}
std::shared_ptr<const SuperMap> Quest::get_supermap(int64_t random_seed) const {
if (this->supermap) {
return this->supermap;
}
bool save_to_cache = true;
bool any_map_file_present = false;
array<shared_ptr<const MapFile>, NUM_VERSIONS> map_files;
for (Version v : ALL_ARPG_SEMANTIC_VERSIONS) {
auto vq = this->version(v, 1);
if (vq && vq->map_file) {
auto map_file = vq->map_file;
if (map_file->has_random_sections()) {
if (random_seed < 0) {
return nullptr;
}
save_to_cache = false;
map_file = map_file->materialize_random_sections(random_seed);
}
map_files.at(static_cast<size_t>(v)) = map_file;
any_map_file_present = true;
}
}
if (!any_map_file_present) {
return nullptr;
}
auto supermap = make_shared<SuperMap>(this->episode, map_files);
if (save_to_cache) {
this->supermap = supermap;
}
static_game_data_log.info("Constructed %s supermap for quest %" PRIu32 " (%s)",
save_to_cache ? "cacheable" : "temporary", this->quest_number, this->name.c_str());
return supermap;
}
bool Quest::has_version(Version v, uint8_t language) const {
return this->versions.count(this->versions_key(v, language));
}
@@ -482,17 +519,22 @@ shared_ptr<const VersionedQuest> Quest::version(Version v, uint8_t language) con
QuestIndex::QuestIndex(
const string& directory,
std::shared_ptr<const QuestCategoryIndex> category_index,
shared_ptr<const QuestCategoryIndex> category_index,
bool is_ep3)
: directory(directory),
category_index(category_index) {
struct FileData {
std::string filename;
string filename;
shared_ptr<const string> data;
};
struct DATFileData {
string filename;
shared_ptr<const string> data;
shared_ptr<const MapFile> map_file;
};
map<string, FileData> bin_files;
map<string, FileData> dat_files;
map<string, DATFileData> dat_files;
map<string, FileData> pvr_files;
map<string, FileData> json_files;
map<string, uint32_t> categories;
@@ -519,6 +561,23 @@ QuestIndex::QuestIndex(
}
};
auto add_dat_file = [&](const string& basename, const string& filename, string&& value) {
if (categories.emplace(basename, cat->category_id).first->second != cat->category_id) {
throw runtime_error("file " + basename + " exists in multiple categories");
}
auto data_ptr = make_shared<string>(std::move(value));
auto map_file = make_shared<MapFile>(make_shared<string>(prs_decompress(*data_ptr)));
if (!dat_files.emplace(basename, DATFileData{filename, data_ptr, map_file}).second) {
throw runtime_error("file " + basename + " already exists");
}
// There is a bug in the client that prevents quests from loading properly
// if any file's size is a multiple of 0x400. See the comments on the 13
// command in CommandFormats.hh for more details.
if (!(data_ptr->size() & 0x3FF)) {
data_ptr->push_back(0x00);
}
};
string cat_path = directory + "/" + cat->directory_name;
if (!phosg::isdir(cat_path)) {
static_game_data_log.warning("Quest category directory %s is missing; skipping it", cat_path.c_str());
@@ -570,9 +629,9 @@ QuestIndex::QuestIndex(
} else if (extension == "bind" || extension == "mnmd") {
add_file(bin_files, file_basename, orig_filename, prs_compress_optimal(file_data), true);
} else if (extension == "dat") {
add_file(dat_files, file_basename, orig_filename, std::move(file_data), true);
add_dat_file(file_basename, orig_filename, std::move(file_data));
} else if (extension == "datd") {
add_file(dat_files, file_basename, orig_filename, prs_compress_optimal(file_data), true);
add_dat_file(file_basename, orig_filename, prs_compress_optimal(file_data));
} else if (extension == "pvr") {
add_file(pvr_files, file_basename, orig_filename, std::move(file_data), true);
} else if (extension == "qst") {
@@ -581,7 +640,7 @@ QuestIndex::QuestIndex(
if (phosg::ends_with(it.first, ".bin")) {
add_file(bin_files, file_basename, orig_filename, std::move(it.second), true);
} else if (phosg::ends_with(it.first, ".dat")) {
add_file(dat_files, file_basename, orig_filename, std::move(it.second), true);
add_dat_file(file_basename, orig_filename, std::move(it.second));
} else if (phosg::ends_with(it.first, ".pvr")) {
add_file(pvr_files, file_basename, orig_filename, std::move(it.second), true);
} else {
@@ -655,7 +714,7 @@ QuestIndex::QuestIndex(
uint8_t language = language_code_for_char(language_token[0]);
// Find the corresponding dat and pvr files
const FileData* dat_filedata = nullptr;
const DATFileData* dat_filedata = nullptr;
const FileData* pvr_filedata = nullptr;
if (!::is_ep3(version)) {
// Look for dat and pvr files with the same basename as the bin file; if
@@ -746,6 +805,7 @@ QuestIndex::QuestIndex(
language,
bin_filedata->data,
dat_filedata ? dat_filedata->data : nullptr,
dat_filedata ? dat_filedata->map_file : nullptr,
pvr_filedata ? pvr_filedata->data : nullptr,
battle_rules,
challenge_template_index,
@@ -796,6 +856,11 @@ QuestIndex::QuestIndex(
static_game_data_log.warning("(%s) Failed to index quest file: (%s)", basename.c_str(), e.what());
}
}
// Create supermaps for all quests that need them (all non-Ep3 quests)
for (const auto& it : this->quests_by_number) {
it.second->get_supermap(-1);
}
}
shared_ptr<const Quest> QuestIndex::get(uint32_t quest_number) const {
@@ -817,11 +882,11 @@ shared_ptr<const Quest> QuestIndex::get(const std::string& name) const {
vector<shared_ptr<const QuestCategoryIndex::Category>> QuestIndex::categories(
QuestMenuType menu_type,
Episode episode,
Version version,
uint16_t version_flags,
IncludeCondition include_condition) const {
vector<shared_ptr<const QuestCategoryIndex::Category>> ret;
for (const auto& cat : this->category_index->categories) {
if (cat->check_flag(menu_type) && !this->filter(episode, version, cat->category_id, include_condition, 1).empty()) {
if (cat->check_flag(menu_type) && !this->filter(episode, version_flags, cat->category_id, include_condition, 1).empty()) {
ret.emplace_back(cat);
}
}
@@ -830,7 +895,7 @@ vector<shared_ptr<const QuestCategoryIndex::Category>> QuestIndex::categories(
vector<pair<QuestIndex::IncludeState, shared_ptr<const Quest>>> QuestIndex::filter(
Episode episode,
Version version,
uint16_t version_flags,
uint32_t category_id,
IncludeCondition include_condition,
size_t limit) const {
@@ -843,17 +908,27 @@ vector<pair<QuestIndex::IncludeState, shared_ptr<const Quest>>> QuestIndex::filt
return ret;
}
for (auto it : category_it->second) {
if (((effective_episode == Episode::NONE) || (it.second->episode == effective_episode)) &&
it.second->has_version_any_language(version)) {
IncludeState state = include_condition ? include_condition(it.second) : IncludeState::AVAILABLE;
if (state == IncludeState::HIDDEN) {
continue;
}
ret.emplace_back(make_pair(state, it.second));
if (limit && (ret.size() >= limit)) {
if ((effective_episode != Episode::NONE) && (it.second->episode != effective_episode)) {
continue;
}
bool all_required_versions_present = true;
for (size_t v_s = 0; v_s < NUM_VERSIONS; v_s++) {
if ((version_flags & (1 << v_s)) && !it.second->has_version_any_language(static_cast<Version>(v_s))) {
all_required_versions_present = false;
break;
}
}
if (!all_required_versions_present) {
continue;
}
IncludeState state = include_condition ? include_condition(it.second) : IncludeState::AVAILABLE;
if (state == IncludeState::HIDDEN) {
continue;
}
ret.emplace_back(make_pair(state, it.second));
if (limit && (ret.size() >= limit)) {
break;
}
}
return ret;
}
+19 -14
View File
@@ -9,6 +9,7 @@
#include <vector>
#include "IntegralExpression.hh"
#include "Map.hh"
#include "PlayerSubordinates.hh"
#include "QuestScript.hh"
#include "StaticGameData.hh"
@@ -63,25 +64,25 @@ struct QuestCategoryIndex {
};
struct VersionedQuest {
uint32_t quest_number;
uint32_t category_id;
Episode episode;
bool allow_start_from_chat_command;
bool joinable;
int16_t lock_status_register;
uint32_t quest_number = 0;
uint32_t category_id = 0;
Episode episode = Episode::NONE;
bool allow_start_from_chat_command = false;
bool joinable = false;
int16_t lock_status_register = -1;
std::string name;
Version version;
uint8_t language;
bool is_dlq_encoded;
Version version = Version::UNKNOWN;
uint8_t language = 1;
bool is_dlq_encoded = false;
std::string short_description;
std::string long_description;
std::shared_ptr<const std::string> bin_contents;
std::shared_ptr<const std::string> dat_contents;
std::shared_ptr<const std::string> dat_contents_decompressed;
std::shared_ptr<const MapFile> map_file;
std::shared_ptr<const std::string> pvr_contents;
std::shared_ptr<const BattleRules> battle_rules;
ssize_t challenge_template_index;
uint8_t description_flag;
ssize_t challenge_template_index = -1;
uint8_t description_flag = 0;
std::shared_ptr<const IntegralExpression> available_expression;
std::shared_ptr<const IntegralExpression> enabled_expression;
@@ -92,6 +93,7 @@ struct VersionedQuest {
uint8_t language,
std::shared_ptr<const std::string> bin_contents,
std::shared_ptr<const std::string> dat_contents,
std::shared_ptr<const MapFile> map_file,
std::shared_ptr<const std::string> pvr_contents,
std::shared_ptr<const BattleRules> battle_rules = nullptr,
ssize_t challenge_template_index = -1,
@@ -120,6 +122,8 @@ public:
Quest& operator=(const Quest&) = default;
Quest& operator=(Quest&&) = default;
std::shared_ptr<const SuperMap> get_supermap(int64_t random_seed) const;
void add_version(std::shared_ptr<const VersionedQuest> vq);
bool has_version(Version v, uint8_t language) const;
bool has_version_any_language(Version v) const;
@@ -134,6 +138,7 @@ public:
bool joinable;
int16_t lock_status_register;
std::string name;
mutable std::shared_ptr<const SuperMap> supermap;
std::shared_ptr<const BattleRules> battle_rules;
ssize_t challenge_template_index;
uint8_t description_flag;
@@ -165,11 +170,11 @@ struct QuestIndex {
std::vector<std::shared_ptr<const QuestCategoryIndex::Category>> categories(
QuestMenuType menu_type,
Episode episode,
Version version,
uint16_t version_flags,
IncludeCondition include_condition = nullptr) const;
std::vector<std::pair<QuestIndex::IncludeState, std::shared_ptr<const Quest>>> filter(
Episode episode,
Version version,
uint16_t version_flags,
uint32_t category_id,
IncludeCondition include_condition = nullptr,
size_t limit = 0) const;
+6 -13
View File
@@ -59,13 +59,6 @@ using AttackData = BattleParamsIndex::AttackData;
using ResistData = BattleParamsIndex::ResistData;
using MovementData = BattleParamsIndex::MovementData;
struct Vector4F {
le_float x;
le_float y;
le_float z;
le_float t;
} __packed_ws__(Vector4F, 0x10);
// bit_cast isn't in the standard place on macOS (it is apparently implicitly
// included by resource_dasm, but newserv can be built without resource_dasm)
// and I'm too lazy to go find the right header to include
@@ -2213,7 +2206,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
// valueD = loop flag (0 = no, 1 = yes)
// regsE[0-2] = result point (x, y, z as floats)
// regsE[3] = the result code (0 = failed, 1 = success)
// labelF = control point entries (array of valueA Vector4F structures)
// labelF = control point entries (array of valueA VectorXYZTF structures)
{0xF8DB, "get_vector_from_path", "unknownF8DB", {INT32, FLOAT32, FLOAT32, INT32, {REG_SET_FIXED, 4}, SCRIPT16}, F_V3_V4 | F_ARGS},
// Same as npc_text, but only applies to a specific player slot.
@@ -2309,7 +2302,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
// valueD = loop flag (0 = no, 1 = yes)
// regsE[0-2] = result point (x, y, z as floats)
// regsE[3] = the result code (0 = failed, 1 = success)
// labelF = control point entries (array of valueA Vector4F structures)
// labelF = control point entries (array of valueA VectorXYZTF structures)
{0xF8F2, "compute_bezier_curve_point", "load_unk_data", {INT32, FLOAT32, FLOAT32, INT32, {REG_SET_FIXED, 4}, {LABEL16, Arg::DataType::BEZIER_CONTROL_POINT_DATA}}, F_V3_V4 | F_ARGS},
// Creates a timed particle effect. Like the particle opcode, but the
@@ -3626,11 +3619,11 @@ std::string disassemble_quest_script(
}
if (l->has_data_type(Arg::DataType::BEZIER_CONTROL_POINT_DATA)) {
phosg::StringReader r = cmd_r.sub(l->offset, size);
lines.emplace_back(" // As Vector4F");
while (r.remaining() >= sizeof(Vector4F)) {
lines.emplace_back(" // As VectorXYZTF");
while (r.remaining() >= sizeof(VectorXYZTF)) {
size_t offset = l->offset + cmd_r.where();
const auto& e = r.get<Vector4F>();
lines.emplace_back(phosg::string_printf(" %04zX vector4f x=%g, y=%g, z=%g, t=%g", offset, e.x.load(), e.y.load(), e.z.load(), e.t.load()));
const auto& e = r.get<VectorXYZTF>();
lines.emplace_back(phosg::string_printf(" %04zX vector x=%g, y=%g, z=%g, t=%g", offset, e.x.load(), e.y.load(), e.z.load(), e.t.load()));
}
if (r.remaining() > 0) {
size_t struct_end_offset = l->offset + r.where();
+10 -10
View File
@@ -5,6 +5,7 @@
#include <phosg/Random.hh>
#include "BattleParamsIndex.hh"
#include "CommonFileFormats.hh"
#include "ItemData.hh"
#include "StaticGameData.hh"
@@ -94,8 +95,8 @@ RareItemSet::ExpandedDrop RareItemSet::ParsedRELData::PackedDrop::expand() const
template <bool BE>
void RareItemSet::ParsedRELData::parse_t(phosg::StringReader r, bool is_v1) {
uint32_t root_offset = r.pget<U32T<BE>>(r.size() - 0x10);
const auto& root = r.pget<OffsetsT<BE>>(root_offset);
const auto& footer = r.pget<RELFileFooterT<BE>>(r.size() - sizeof(RELFileFooterT<BE>));
const auto& root = r.pget<OffsetsT<BE>>(footer.root_offset);
phosg::StringReader monsters_r = r.sub(root.monster_rares_offset);
for (size_t z = 0; z < (is_v1 ? 0x33 : 0x65); z++) {
@@ -160,14 +161,13 @@ std::string RareItemSet::ParsedRELData::serialize_t(bool is_v1) const {
while (w.size() & 0x1F) {
w.put_u8(0);
}
w.put<U32T<BE>>(relocations_offset);
w.put<U32T<BE>>(3); // num_relocations
w.put<U32T<BE>>(1); // TODO: What is this used for?
w.put<U32T<BE>>(0);
w.put<U32T<BE>>(root_offset);
w.put<U32T<BE>>(0);
w.put<U32T<BE>>(0);
w.put<U32T<BE>>(0);
RELFileFooterT<BE> footer;
footer.relocations_offset = relocations_offset;
footer.num_relocations = 3;
footer.unused1[0] = 1; // TODO: What is this used for?
footer.root_offset = root_offset;
w.put<RELFileFooterT<BE>>(footer);
return std::move(w.str());
}
+130 -142
View File
@@ -1393,7 +1393,7 @@ static bool start_ep3_battle_table_game_if_ready(shared_ptr<Lobby> l, int16_t ta
// Figure out which clients are at this table. If any client has declined, we
// never start a match, but we may start a match even if all clients have not
// yet accepted (in case of a tournament match).
Version base_version = Version::UNKNOWN;
Version game_version = Version::UNKNOWN;
unordered_map<size_t, shared_ptr<Client>> table_clients;
bool all_clients_accepted = true;
for (const auto& c : l->clients) {
@@ -1401,9 +1401,9 @@ static bool start_ep3_battle_table_game_if_ready(shared_ptr<Lobby> l, int16_t ta
continue;
}
// Prevent match from starting unless all players are on the same version
if (base_version == Version::UNKNOWN) {
base_version = c->version();
} else if (base_version != c->version()) {
if (game_version == Version::UNKNOWN) {
game_version = c->version();
} else if (game_version != c->version()) {
return false;
}
if (c->card_battle_table_seat_number >= 4) {
@@ -1920,10 +1920,31 @@ static void on_09(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
for (size_t x = 0; x < game->max_clients; x++) {
const auto& game_c = game->clients[x];
if (game_c.get()) {
static_assert(NUM_VERSIONS == 14, "Don\'t forget to update the game player listing version tokens");
static const array<const char*, NUM_VERSIONS> version_tokens = {
" $C4P2$C7",
" $C4P4$C7",
" $C5DCN$C7",
" $C5DCP$C7",
" $C2DC1$C7",
" $C2DC2$C7",
" $C5PCN$C7",
" $C2PC$C7",
" $C5GCN$C7",
" $C2GC$C7",
" $C5Ep3N$C7",
" $C2Ep3$C7",
" $C2XB$C7",
" $C2BB$C7",
};
const char* version_token = (game_c->version() != c->version())
? version_tokens.at(static_cast<size_t>(game_c->version()))
: "";
auto player = game_c->character();
string name = escape_player_name(player->disp.name.decode(game_c->language()));
info += phosg::string_printf("%s\n %s Lv%" PRIu32 " %c\n",
info += phosg::string_printf("%s%s\n %s Lv%" PRIu32 " %c\n",
name.c_str(),
version_token,
name_for_char_class(player->disp.visual.char_class),
player->disp.stats.level + 1,
char_for_language_code(game_c->language()));
@@ -1942,10 +1963,6 @@ static void on_09(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
info += phosg::string_printf("Req. level: %" PRIu32 "+\n", game->min_level + 1);
}
if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
info += phosg::string_printf("%s\n", phosg::name_for_enum(game->base_version));
}
if (game->check_flag(Lobby::Flag::CHEATS_ENABLED)) {
info += "$C6Cheats enabled$C7\n";
}
@@ -2080,15 +2097,10 @@ static void on_quest_loaded(shared_ptr<Lobby> l) {
throw logic_error("on_quest_loaded called without a quest loaded");
}
auto s = l->require_server_state();
// Replace the free-play map with the quest's map
l->load_maps();
// For BB Challenge quests, don't replace the map now - the leader will send
// an 02DF command to create overlays, which also replaces the map. (We do
// this because 02DF is also sent when a challenge is failed and retried,
// which reloads the map and recreates character overlays anyway.)
if ((l->base_version != Version::BB_V4) || (l->quest->challenge_template_index < 0)) {
l->load_maps();
}
auto s = l->require_server_state();
// Delete all floor items
for (auto& m : l->floor_item_managers) {
@@ -2100,26 +2112,20 @@ static void on_quest_loaded(shared_ptr<Lobby> l) {
continue;
}
if ((lc->version() == Version::BB_V4) && l->map) {
send_rare_enemy_index_list(lc, l->map->rare_enemy_indexes);
if (lc->version() == Version::BB_V4) {
send_rare_enemy_index_list(lc, l->map_state->bb_rare_enemy_indexes);
}
// On non-BB versions, overlays are created when the quest starts because
// the server is not informed when the clients have replaced their player
// data. On BB, this is instead done in the 6xCF handler (for battle) or
// the 02DF handler (for challenge).
if (l->base_version != Version::BB_V4) {
lc->delete_overlay();
if (l->quest->battle_rules) {
lc->use_default_bank();
lc->create_battle_overlay(l->quest->battle_rules, s->level_table(lc->version()));
lc->log.info("Created battle overlay");
} else if (l->quest->challenge_template_index >= 0) {
lc->use_default_bank();
lc->create_challenge_overlay(lc->version(), l->quest->challenge_template_index, s->level_table(lc->version()));
lc->log.info("Created challenge overlay");
l->assign_inventory_and_bank_item_ids(lc, true);
}
lc->delete_overlay();
if (l->quest->battle_rules) {
lc->use_default_bank();
lc->create_battle_overlay(l->quest->battle_rules, s->level_table(lc->version()));
lc->log.info("Created battle overlay");
} else if (l->quest->challenge_template_index >= 0) {
lc->use_default_bank();
lc->create_challenge_overlay(lc->version(), l->quest->challenge_template_index, s->level_table(lc->version()));
lc->log.info("Created challenge overlay");
l->assign_inventory_and_bank_item_ids(lc, true);
}
}
}
@@ -2151,12 +2157,10 @@ void set_lobby_quest(shared_ptr<Lobby> l, shared_ptr<const Quest> q, bool substi
l->clear_flag(Lobby::Flag::PERSISTENT);
l->quest = q;
if (!is_ep3(l->base_version)) {
if (l->episode != Episode::EP3) {
l->episode = q->episode;
}
if (l->item_creator) {
l->create_item_creator();
}
l->create_item_creator();
// There is no such thing as command AC on PSO V1 and V2 - quests just start
// immediately when they're done downloading. (This is also the case on V3
@@ -2296,9 +2300,10 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
// online quests, but they're served via a server data request
// instead of the file download paradigm that other versions use.)
auto quest_index = s->quest_index(c->version());
const auto& categories = quest_index->categories(menu_type, Episode::EP3, c->version());
uint16_t version_flags = (1 << static_cast<size_t>(c->version()));
const auto& categories = quest_index->categories(menu_type, Episode::EP3, version_flags);
if (categories.size() == 1) {
auto quests = quest_index->filter(Episode::EP3, c->version(), categories[0]->category_id);
auto quests = quest_index->filter(Episode::EP3, version_flags, categories[0]->category_id);
send_quest_menu(c, quests, true);
break;
}
@@ -2618,12 +2623,13 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
shared_ptr<Lobby> l = c->lobby.lock();
Episode episode = l ? l->episode : Episode::NONE;
uint16_t version_flags = (1 << static_cast<size_t>(c->version())) | (l ? l->quest_version_flags() : 0);
QuestIndex::IncludeCondition include_condition = nullptr;
if (l && !c->login->account->check_flag(Account::Flag::DISABLE_QUEST_REQUIREMENTS)) {
include_condition = l->quest_include_condition();
}
const auto& quests = quest_index->filter(episode, c->version(), item_id, include_condition);
const auto& quests = quest_index->filter(episode, version_flags, item_id, include_condition);
send_quest_menu(c, quests, !l);
break;
}
@@ -3944,7 +3950,7 @@ static void on_DF_BB(shared_ptr<Client> c, uint16_t command, uint32_t, string& d
}
}
l->load_maps();
l->map_state->reset();
break;
}
@@ -4286,7 +4292,7 @@ static void on_C9_XB(shared_ptr<Client> c, uint16_t, uint32_t flag, string& data
shared_ptr<Lobby> create_game_generic(
shared_ptr<ServerState> s,
shared_ptr<Client> c,
shared_ptr<Client> creator_c,
const std::string& name,
const std::string& password,
Episode episode,
@@ -4307,86 +4313,68 @@ shared_ptr<Lobby> create_game_generic(
throw invalid_argument("incorrect difficulty level");
}
auto current_lobby = c->require_lobby();
auto current_lobby = creator_c->require_lobby();
size_t min_level = s->default_min_level_for_game(c->version(), episode, difficulty);
size_t min_level = s->default_min_level_for_game(creator_c->version(), episode, difficulty);
auto p = c->character();
if (!c->login->account->check_flag(Account::Flag::FREE_JOIN_GAMES) && (min_level > p->disp.stats.level)) {
auto p = creator_c->character();
if (!creator_c->login->account->check_flag(Account::Flag::FREE_JOIN_GAMES) && (min_level > p->disp.stats.level)) {
// Note: We don't throw here because this is a situation players might
// actually encounter while playing the game normally
string msg = phosg::string_printf("You must be level %zu\nor above to play\nthis difficulty.", static_cast<size_t>(min_level + 1));
send_lobby_message_box(c, msg);
send_lobby_message_box(creator_c, msg);
return nullptr;
}
shared_ptr<Lobby> game = s->create_lobby(true);
game->name = name;
game->base_version = c->version();
game->allowed_versions = 0;
switch (game->base_version) {
case Version::DC_NTE:
game->allow_version(Version::DC_NTE);
break;
case Version::DC_V1_11_2000_PROTOTYPE:
game->allow_version(Version::DC_V1_11_2000_PROTOTYPE);
break;
case Version::DC_V1:
game->allow_version(Version::DC_V1);
game->allow_version(Version::DC_V2);
if (s->allow_dc_pc_games) {
game->allow_version(Version::PC_V2);
}
break;
case Version::DC_V2:
if (allow_v1 && (difficulty <= 2) && (mode == GameMode::NORMAL)) {
game->allow_version(Version::DC_V1);
}
game->allow_version(Version::DC_V2);
if (s->allow_dc_pc_games) {
game->allow_version(Version::PC_V2);
}
break;
case Version::PC_NTE:
game->allow_version(Version::PC_NTE);
break;
case Version::PC_V2:
game->allow_version(Version::PC_V2);
if (s->allow_dc_pc_games) {
game->allow_version(Version::DC_V2);
if (allow_v1 && (difficulty <= 2) && (mode == GameMode::NORMAL)) {
game->allow_version(Version::DC_V1);
game->episode = episode;
game->mode = mode;
game->difficulty = difficulty;
game->allowed_versions = s->compatibility_groups.at(static_cast<size_t>(creator_c->version()));
static_assert(NUM_VERSIONS == 14, "Don't forget to update the group compatibility restrictions");
if (!allow_v1 || (difficulty > 2) || (mode != GameMode::NORMAL)) {
game->forbid_version(Version::DC_NTE);
game->forbid_version(Version::DC_V1_11_2000_PROTOTYPE);
game->forbid_version(Version::DC_V1);
}
switch (game->episode) {
case Episode::NONE:
throw logic_error("game episode not set at creation time");
case Episode::EP1:
for (Version v : ALL_VERSIONS) {
if (is_ep3(v)) {
game->forbid_version(v);
}
}
break;
case Version::GC_NTE:
game->allow_version(Version::GC_NTE);
break;
case Version::GC_V3:
game->allow_version(Version::GC_V3);
if (s->allow_gc_xb_games) {
game->allow_version(Version::XB_V3);
case Episode::EP2:
for (Version v : ALL_VERSIONS) {
if (!is_v3(v) || is_ep3(v)) {
game->forbid_version(v);
}
}
break;
case Version::GC_EP3_NTE:
game->allow_version(Version::GC_EP3_NTE);
break;
case Version::GC_EP3:
game->allow_version(Version::GC_EP3);
break;
case Version::XB_V3:
game->allow_version(Version::XB_V3);
if (s->allow_gc_xb_games) {
game->allow_version(Version::GC_V3);
case Episode::EP3:
for (Version v : ALL_VERSIONS) {
if (!is_ep3(v)) {
game->forbid_version(v);
}
}
break;
case Version::BB_V4:
game->allow_version(Version::BB_V4);
case Episode::EP4:
for (Version v : ALL_VERSIONS) {
if (!is_v4(v)) {
game->forbid_version(v);
}
}
break;
default:
throw logic_error("invalid quest script version");
}
if (c->config.check_flag(Client::Flag::IS_CLIENT_CUSTOMIZATION)) {
if (creator_c->login->account->check_flag(Account::Flag::DEBUG)) {
game->set_flag(Lobby::Flag::DEBUG);
}
if (creator_c->config.check_flag(Client::Flag::IS_CLIENT_CUSTOMIZATION)) {
game->set_flag(Lobby::Flag::IS_CLIENT_CUSTOMIZATION);
}
@@ -4409,15 +4397,12 @@ shared_ptr<Lobby> create_game_generic(
game->password = password;
game->creator_section_id = p->disp.visual.section_id;
game->override_section_id = c->config.override_section_id;
game->episode = episode;
game->mode = mode;
game->override_section_id = creator_c->config.override_section_id;
if (game->mode == GameMode::CHALLENGE) {
game->challenge_params = make_shared<Lobby::ChallengeParameters>();
}
game->difficulty = difficulty;
if (c->config.check_flag(Client::Flag::USE_OVERRIDE_RANDOM_SEED)) {
game->random_seed = c->config.override_random_seed;
if (creator_c->config.check_flag(Client::Flag::USE_OVERRIDE_RANDOM_SEED)) {
game->random_seed = creator_c->config.override_random_seed;
game->opt_rand_crypt = make_shared<PSOV2Encryption>(game->random_seed);
}
if (battle_player) {
@@ -4428,7 +4413,7 @@ shared_ptr<Lobby> create_game_generic(
game->exp_share_multiplier = s->exp_share_multiplier;
const unordered_map<uint16_t, IntegralExpression>* quest_flag_rewrites;
switch (game->base_version) {
switch (creator_c->version()) {
case Version::DC_NTE:
case Version::DC_V1_11_2000_PROTOTYPE:
case Version::DC_V1:
@@ -4437,13 +4422,13 @@ shared_ptr<Lobby> create_game_generic(
case Version::PC_V2:
quest_flag_rewrites = &s->quest_flag_rewrites_v1_v2;
if (game->mode == GameMode::BATTLE) {
game->set_drop_mode(s->default_drop_mode_v1_v2_battle);
game->drop_mode = s->default_drop_mode_v1_v2_battle;
game->allowed_drop_modes = s->allowed_drop_modes_v1_v2_battle;
} else if (game->mode == GameMode::CHALLENGE) {
game->set_drop_mode(s->default_drop_mode_v1_v2_challenge);
game->drop_mode = s->default_drop_mode_v1_v2_challenge;
game->allowed_drop_modes = s->allowed_drop_modes_v1_v2_challenge;
} else {
game->set_drop_mode(s->default_drop_mode_v1_v2_normal);
game->drop_mode = s->default_drop_mode_v1_v2_normal;
game->allowed_drop_modes = s->allowed_drop_modes_v1_v2_normal;
}
break;
@@ -4452,32 +4437,32 @@ shared_ptr<Lobby> create_game_generic(
case Version::XB_V3:
quest_flag_rewrites = &s->quest_flag_rewrites_v3;
if (game->mode == GameMode::BATTLE) {
game->set_drop_mode(s->default_drop_mode_v3_battle);
game->drop_mode = s->default_drop_mode_v3_battle;
game->allowed_drop_modes = s->allowed_drop_modes_v3_battle;
} else if (game->mode == GameMode::CHALLENGE) {
game->set_drop_mode(s->default_drop_mode_v3_challenge);
game->drop_mode = s->default_drop_mode_v3_challenge;
game->allowed_drop_modes = s->allowed_drop_modes_v3_challenge;
} else {
game->set_drop_mode(s->default_drop_mode_v3_normal);
game->drop_mode = s->default_drop_mode_v3_normal;
game->allowed_drop_modes = s->allowed_drop_modes_v3_normal;
}
break;
case Version::GC_EP3_NTE:
case Version::GC_EP3:
quest_flag_rewrites = nullptr;
game->set_drop_mode(Lobby::DropMode::DISABLED);
game->drop_mode = Lobby::DropMode::DISABLED;
game->allowed_drop_modes = (1 << static_cast<size_t>(game->drop_mode));
break;
case Version::BB_V4:
quest_flag_rewrites = &s->quest_flag_rewrites_v4;
if (game->mode == GameMode::BATTLE) {
game->set_drop_mode(s->default_drop_mode_v4_battle);
game->drop_mode = s->default_drop_mode_v4_battle;
game->allowed_drop_modes = s->allowed_drop_modes_v4_battle;
} else if (game->mode == GameMode::CHALLENGE) {
game->set_drop_mode(s->default_drop_mode_v4_challenge);
game->drop_mode = s->default_drop_mode_v4_challenge;
game->allowed_drop_modes = s->allowed_drop_modes_v4_challenge;
} else {
game->set_drop_mode(s->default_drop_mode_v4_normal);
game->drop_mode = s->default_drop_mode_v4_normal;
game->allowed_drop_modes = s->allowed_drop_modes_v4_normal;
}
// Disallow CLIENT mode on BB
@@ -4491,6 +4476,7 @@ shared_ptr<Lobby> create_game_generic(
default:
throw logic_error("invalid quest script version");
}
game->create_item_creator(creator_c->version());
game->event = current_lobby->event;
game->block = 0xFF;
@@ -4512,23 +4498,27 @@ shared_ptr<Lobby> create_game_generic(
if (game->episode != Episode::EP3) {
// GC NTE ignores the passed-in variations and always uses all zeroes
if (game->base_version == Version::GC_NTE) {
game->variations.clear(0);
} else if (c->override_variations) {
game->variations = *c->override_variations;
c->override_variations.reset();
if (creator_c->version() == Version::GC_NTE) {
game->variations = Variations();
game->log.info("Base version is GC_NTE; using blank variations");
} else if (creator_c->override_variations) {
game->variations = *creator_c->override_variations;
creator_c->override_variations.reset();
auto vars_str = game->variations.str();
game->log.info("Using variations from client override: %s", vars_str.c_str());
} else {
auto sdt = s->set_data_table(game->base_version, game->episode, game->mode, game->difficulty);
auto sdt = s->set_data_table(creator_c->version(), game->episode, game->mode, game->difficulty);
game->variations = sdt->generate_variations(game->episode, is_solo, game->opt_rand_crypt);
auto vars_str = game->variations.str();
game->log.info("Using random variations: %s", vars_str.c_str());
}
game->load_maps();
} else {
game->variations.clear(0);
game->map = make_shared<Map>(game->base_version, game->lobby_id, game->random_seed, game->opt_rand_crypt);
game->variations = Variations();
}
game->load_maps(); // Load free-play maps
// The game's quest flags are inherited from the creator, if known
if (c->version() == Version::BB_V4) {
if (creator_c->version() == Version::BB_V4) {
game->quest_flag_values = make_unique<QuestFlags>(p->quest_flags);
game->quest_flags_known = nullptr;
} else {
@@ -4540,10 +4530,10 @@ shared_ptr<Lobby> create_game_generic(
IntegralExpression::Env env = {
.flags = &p->quest_flags.data.at(difficulty),
.challenge_records = &p->challenge_records,
.team = c->team(),
.team = creator_c->team(),
.num_players = 1,
.event = game->event,
.v1_present = is_v1(game->base_version),
.v1_present = is_v1(creator_c->version()),
};
for (const auto& it : *quest_flag_rewrites) {
bool should_set = it.second.evaluate(env);
@@ -4557,7 +4547,7 @@ shared_ptr<Lobby> create_game_generic(
game->quest_flags_known->set(game->difficulty, it.first);
}
}
c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_FLAG_STATE);
creator_c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_FLAG_STATE);
}
game->switch_flags = make_unique<SwitchFlags>();
@@ -4761,11 +4751,11 @@ static void on_6F(shared_ptr<Client> c, uint16_t command, uint32_t, string& data
send_server_time(c);
if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
string variations_str;
for (size_t z = 0; z < l->variations.size(); z++) {
variations_str += phosg::string_printf("%" PRIX32, l->variations[z].load());
for (size_t z = 0; z < l->variations.entries.size(); z++) {
const auto& e = l->variations.entries[z];
variations_str += phosg::string_printf(" %" PRIX32 "%" PRIX32, e.layout.load(), e.entities.load());
}
send_text_message_printf(c, "Rare seed: %08" PRIX32 "\nRare enemies: %zu\nVariations: %s\n",
l->random_seed, l->map->rare_enemy_indexes.size(), variations_str.c_str());
send_text_message_printf(c, "Rare seed: %08" PRIX32 "\nVariations:%s\n", l->random_seed, variations_str.c_str());
}
bool should_resume_game = true;
@@ -4800,9 +4790,7 @@ static void on_6F(shared_ptr<Client> c, uint16_t command, uint32_t, string& data
c->config.clear_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST);
c->log.info("LOADING_RUNNING_JOINABLE_QUEST flag cleared");
}
if (l->map) {
send_rare_enemy_index_list(c, l->map->rare_enemy_indexes);
}
send_rare_enemy_index_list(c, l->map_state->bb_rare_enemy_indexes);
}
// We should resume the game if:
+1569 -1270
View File
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -5,11 +5,11 @@
#include "Client.hh"
#include "CommandFormats.hh"
#include "Lobby.hh"
#include "Map.hh"
#include "PSOProtocol.hh"
#include "ServerState.hh"
void on_subcommand_multi(std::shared_ptr<Client> c, uint8_t command, uint8_t flag, std::string& data);
bool subcommand_is_implemented(uint8_t which);
void send_item_notification_if_needed(
std::shared_ptr<ServerState> s,
@@ -21,8 +21,9 @@ void send_item_notification_if_needed(
G_SpecializableItemDropRequest_6xA2 normalize_drop_request(const void* data, size_t size);
struct DropReconcileResult {
std::shared_ptr<MapState::ObjectState> obj_st;
std::shared_ptr<MapState::EnemyState> ene_st;
uint8_t effective_rt_index;
bool is_box;
bool should_drop;
bool ignore_def;
};
@@ -31,10 +32,10 @@ DropReconcileResult reconcile_drop_request_with_map(
phosg::PrefixedLogger& log,
Channel& client_channel,
G_SpecializableItemDropRequest_6xA2& cmd,
Version version,
Episode episode,
uint8_t event,
const Client::Config& config,
std::shared_ptr<Map> map,
std::shared_ptr<MapState> map,
bool mark_drop);
class Parsed6x70Data {
+10 -10
View File
@@ -286,19 +286,19 @@ void ReplaySession::apply_default_mask(shared_ptr<Event> ev) {
case 0x64:
if ((version == Version::PC_NTE) || (version == Version::PC_V2)) {
auto& mask = check_size_t<S_JoinGame_PC_64>(mask_data, mask_size);
mask.variations.clear(0);
mask.rare_seed = 0;
mask.variations = Variations();
mask.random_seed = 0;
} else if (version == Version::XB_V3) {
auto& mask = check_size_t<S_JoinGame_XB_64>(mask_data, mask_size);
mask.variations.clear(0);
mask.rare_seed = 0;
mask.variations = Variations();
mask.random_seed = 0;
} else if (version == Version::DC_NTE || version == Version::DC_V1_11_2000_PROTOTYPE) {
auto& mask = check_size_t<S_JoinGame_DCNTE_64>(mask_data, mask_size);
mask.variations.clear(0);
mask.variations = Variations();
} else {
auto& mask = check_size_t<S_JoinGame_DC_64>(mask_data, mask_size, sizeof(S_JoinGame_Ep3_64));
mask.variations.clear(0);
mask.rare_seed = 0;
mask.variations = Variations();
mask.random_seed = 0;
for (size_t offset = sizeof(S_JoinGame_GC_64) +
offsetof(S_JoinGame_Ep3_64::Ep3PlayerEntry, disp.visual.name_color_checksum);
offset + 4 <= mask_size;
@@ -348,7 +348,7 @@ void ReplaySession::apply_default_mask(shared_ptr<Event> ev) {
case 0xE8:
if (is_gc(version)) {
auto& mask = check_size_t<S_JoinSpectatorTeam_Ep3_E8>(mask_data, mask_size);
mask.rare_seed = 0;
mask.random_seed = 0;
for (size_t z = 0; z < 4; z++) {
mask.players[z].disp.visual.name_color_checksum = 0;
}
@@ -430,8 +430,8 @@ void ReplaySession::apply_default_mask(shared_ptr<Event> ev) {
}
case 0x0064: {
auto& mask = check_size_t<S_JoinGame_BB_64>(mask_data, mask_size);
mask.variations.clear(0);
mask.rare_seed = 0;
mask.variations = Variations();
mask.random_seed = 0;
break;
}
case 0x00B1: {
+166 -97
View File
@@ -520,8 +520,7 @@ bool send_protected_command(std::shared_ptr<Client> c, const void* data, size_t
try {
auto s = c->require_server_state();
auto fn = s->function_code_index->get_patch("CallProtectedHandler", c->config.specific_version);
uint32_t size_label_value = is_big_endian(c->version()) ? data.size() : phosg::bswap32(data.size());
send_function_call(c, fn, {{"size", size_label_value}}, data.data(), data.size());
send_function_call(c, fn, {{"size", data.size()}}, data.data(), data.size());
c->function_call_response_queue.emplace_back(empty_function_call_response_handler);
if (echo_to_lobby) {
auto l = c->lobby.lock();
@@ -1627,8 +1626,14 @@ void send_quest_categories_menu_t(
include_condition = l ? l->quest_include_condition() : nullptr;
}
uint16_t version_flags = (1 << static_cast<size_t>(c->version()));
auto l = c->lobby.lock();
if (l) {
version_flags |= l->quest_version_flags();
}
vector<EntryT> entries;
for (const auto& cat : quest_index->categories(menu_type, episode, c->version(), include_condition)) {
for (const auto& cat : quest_index->categories(menu_type, episode, version_flags, include_condition)) {
auto& e = entries.emplace_back();
e.menu_id = cat->use_ep2_icon() ? MenuID::QUEST_CATEGORIES_EP2 : MenuID::QUEST_CATEGORIES_EP1;
e.item_id = cat->category_id;
@@ -1806,11 +1811,11 @@ static void send_join_spectator_team(shared_ptr<Client> c, shared_ptr<Lobby> l)
S_JoinSpectatorTeam_Ep3_E8 cmd;
cmd.variations.clear(0);
cmd.variations = Variations();
cmd.client_id = c->lobby_client_id;
cmd.event = l->event;
cmd.section_id = l->effective_section_id();
cmd.rare_seed = l->random_seed;
cmd.random_seed = l->random_seed;
cmd.episode = 0xFF;
uint8_t player_count = 0;
@@ -1953,7 +1958,7 @@ void send_join_game(shared_ptr<Client> c, shared_ptr<Lobby> l) {
cmd.event = l->event;
cmd.section_id = l->effective_section_id();
cmd.challenge_mode = (l->mode == GameMode::CHALLENGE) ? 1 : 0;
cmd.rare_seed = l->random_seed;
cmd.random_seed = l->random_seed;
return populate_lobby_data(cmd);
};
auto populate_v3_cmd = [&](auto& cmd) -> size_t {
@@ -2568,7 +2573,6 @@ void send_warp(Channel& ch, uint8_t client_id, uint32_t floor, bool is_private)
void send_warp(shared_ptr<Client> c, uint32_t floor, bool is_private) {
send_warp(c->channel, c->lobby_client_id, floor, is_private);
c->floor = floor;
}
void send_warp(shared_ptr<Lobby> l, uint32_t floor, bool is_private) {
@@ -2587,6 +2591,10 @@ void send_ep3_change_music(Channel& ch, uint32_t song) {
void send_game_join_sync_command(
shared_ptr<Client> c, const void* data, size_t size, uint8_t dc_nte_sc, uint8_t dc_11_2000_sc, uint8_t sc) {
string compressed_data = bc0_compress(data, size);
if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
c->log.info("Compressed sync data from (%zX -> %zX bytes):", size, compressed_data.size());
phosg::print_data(stderr, data, size);
}
send_game_join_sync_command_compressed(c, compressed_data.data(), compressed_data.size(), size, dc_nte_sc, dc_11_2000_sc, sc);
}
@@ -2639,6 +2647,9 @@ void send_game_join_sync_command_compressed(
void send_game_item_state(shared_ptr<Client> c) {
auto l = c->require_lobby();
if (!l->is_game()) {
throw logic_error("cannot send item state in non-game lobby");
}
auto s = c->require_server_state();
phosg::StringWriter floor_items_w;
@@ -2652,7 +2663,7 @@ void send_game_item_state(shared_ptr<Client> c) {
decompressed_header.next_item_id_per_player[2].load(),
decompressed_header.next_item_id_per_player[3].load());
for (size_t floor = 0; floor < 0x10; floor++) {
for (size_t floor = 0; floor < 0x0F; floor++) {
const auto& m = l->floor_item_managers.at(floor);
// It's important that these are added in increasing order of item_id (hence
// why items is a map and not an unordered_map), since the game uses binary
@@ -2667,10 +2678,9 @@ void send_game_item_state(shared_ptr<Client> c) {
FloorItem fi;
fi.floor = floor;
fi.from_enemy = 0;
fi.entity_id = 0xFFFF;
fi.x = item->x;
fi.z = item->z;
fi.source_type = 0;
fi.entity_index = 0xFFFF;
fi.pos = item->pos;
fi.unknown_a2 = 0;
fi.drop_number = (floor == 0) ? 0xFFFF : (decompressed_header.next_drop_number_per_floor.at(floor - 1)++);
fi.item = item->data;
@@ -2686,55 +2696,84 @@ void send_game_item_state(shared_ptr<Client> c) {
decompressed_w.write(floor_items_w.str());
const auto& data = decompressed_w.str();
send_game_join_sync_command(c, data.data(), data.size(), 0x5E, 0x65, 0x6D);
}
void send_game_enemy_state(shared_ptr<Client> c) {
auto l = c->require_lobby();
if (!l->map) {
return;
// Items on floors 0x0F and above can't be sent in the 6x6D command, so we
// manually send 6x5D commands to create them if needed
phosg::StringWriter w;
for (size_t floor = 0x0F; floor < l->floor_item_managers.size(); floor++) {
const auto& m = l->floor_item_managers[floor];
for (const auto& it : m.items) {
const auto& item = it.second;
if (!item->visible_to_client(c->lobby_client_id)) {
continue;
}
uint8_t subcommand = get_pre_v1_subcommand(c->version(), 0x4F, 0x56, 0x5D);
G_DropStackedItem_PC_V3_BB_6x5D cmd = {{{subcommand, 0x0A, 0x0000}, floor, 0, item->pos, item->data}, 0};
cmd.item_data.encode_for_version(c->version(), s->item_parameter_table_for_encode(c->version()));
w.put(cmd);
}
}
auto s = c->require_server_state();
vector<G_SyncEnemyState_6x6B_Entry_Decompressed> entries;
entries.reserve(l->map->enemies.size());
for (size_t z = 0; z < l->map->enemies.size(); z++) {
const auto& enemy = l->map->enemies[z];
auto& entry = entries.emplace_back();
entry.flags = enemy.game_flags;
entry.item_drop_id = (enemy.server_flags & Map::Enemy::Flag::ITEM_DROPPED) ? 0xFFFF : (0xCA0 + z);
entry.total_damage = enemy.total_damage;
if (!w.str().empty()) {
send_command(c, 0x6D, c->lobby_client_id, w.str());
}
send_game_join_sync_command(c, entries.data(), entries.size() * sizeof(entries[0]), 0x5C, 0x63, 0x6B);
}
void send_game_object_state(shared_ptr<Client> c) {
auto l = c->require_lobby();
if (!l->map) {
return;
if (!l->is_game()) {
throw logic_error("cannot send object state in non-game lobby");
}
auto s = c->require_server_state();
vector<G_SyncObjectState_6x6C_Entry_Decompressed> entries;
entries.reserve(l->map->objects.size());
for (size_t z = 0; z < l->map->objects.size(); z++) {
const auto& obj = l->map->objects[z];
vector<SyncObjectStateEntry> entries;
for (auto obj_st : l->map_state->iter_object_states(c->version())) {
auto& entry = entries.emplace_back();
entry.flags = obj.game_flags;
entry.item_drop_id = (obj.item_drop_checked) ? 0xFFFF : (0x100 + z);
entry.flags = obj_st->game_flags;
entry.item_drop_id = (obj_st->item_drop_checked)
? 0xFFFF
: (0x100 + l->map_state->index_for_object_state(c->version(), obj_st));
}
send_game_join_sync_command(c, entries.data(), entries.size() * sizeof(entries[0]), 0x5D, 0x64, 0x6C);
}
void send_game_set_state(shared_ptr<Client> c) {
void send_game_enemy_state(shared_ptr<Client> c) {
auto l = c->require_lobby();
if (!l->map) {
return;
if (!l->is_game()) {
throw logic_error("cannot send enemy state in non-game lobby");
}
auto s = c->require_server_state();
vector<SyncEnemyStateEntry> entries;
for (auto ene_st : l->map_state->iter_enemy_states(c->version())) {
auto& entry = entries.emplace_back();
entry.flags = ene_st->game_flags;
entry.item_drop_id = (ene_st->server_flags & MapState::EnemyState::Flag::ITEM_DROPPED)
? 0xFFFF
: (0xCA0 + l->map_state->index_for_enemy_state(c->version(), ene_st));
entry.total_damage = ene_st->total_damage;
}
size_t num_object_sets = l->map->objects.size();
size_t num_enemy_sets = l->map->enemy_set_flags.size();
send_game_join_sync_command(c, entries.data(), entries.size() * sizeof(entries[0]), 0x5C, 0x63, 0x6B);
}
void send_game_set_state(shared_ptr<Client> c) {
auto l = c->require_lobby();
if (!l->is_game()) {
throw logic_error("cannot send set state in non-game lobby");
}
size_t num_object_sets = 0;
size_t num_enemy_sets = 0;
size_t num_events = 0;
for (const auto& fc : l->map_state->floor_config_entries) {
if (fc.super_map) {
const auto& entities = fc.super_map->version(c->version());
num_object_sets += entities.objects.size();
num_enemy_sets += entities.enemy_sets.size();
num_events += entities.events.size();
}
}
G_SyncSetFlagState_6x6E_Decompressed::EntitySetFlags entity_set_flags_header;
entity_set_flags_header.object_set_flags_offset = sizeof(entity_set_flags_header);
@@ -2744,22 +2783,47 @@ void send_game_set_state(shared_ptr<Client> c) {
G_SyncSetFlagState_6x6E_Decompressed header;
header.entity_set_flags_size = sizeof(entity_set_flags_header) + (num_object_sets + num_enemy_sets) * sizeof(le_uint16_t);
header.event_set_flags_size = sizeof(le_uint16_t) * l->map->events.size();
header.event_set_flags_size = sizeof(le_uint16_t) * num_events;
header.switch_flags_size = is_v1(c->version()) ? 0x200 : 0x240;
header.total_size = header.entity_set_flags_size + header.event_set_flags_size + header.switch_flags_size;
phosg::StringWriter w;
w.put(header);
w.put(entity_set_flags_header);
for (const auto& obj : l->map->objects) {
w.put_u16l(obj.set_flags);
{
size_t size_before = w.size();
for (const auto& obj_st : l->map_state->iter_object_states(c->version())) {
w.put_u16l(obj_st->set_flags);
}
size_t bytes_added = w.size() - size_before;
if (bytes_added != num_object_sets * sizeof(le_uint16_t)) {
throw logic_error("incorrect number of object set flags added");
}
}
for (uint16_t enemy_set_flags : l->map->enemy_set_flags) {
w.put_u16l(enemy_set_flags);
{
size_t size_before = w.size();
for (const auto& ene_st : l->map_state->iter_enemy_set_states(c->version())) {
w.put_u16l(ene_st->set_flags);
}
size_t bytes_added = w.size() - size_before;
if (bytes_added != num_enemy_sets * sizeof(le_uint16_t)) {
throw logic_error("incorrect number of enemy set flags added");
}
}
for (const auto& event : l->map->events) {
w.put_u16l(event.flags);
{
size_t size_before = w.size();
for (const auto& ev_st : l->map_state->iter_event_states(c->version())) {
w.put_u16l(ev_st->flags);
}
size_t bytes_added = w.size() - size_before;
if (bytes_added != num_events * sizeof(le_uint16_t)) {
throw logic_error("incorrect number of event flags added");
}
}
if (l->switch_flags) {
static_assert(sizeof(SwitchFlags) == 0x240, "switch_flags size is incorrect");
w.write(l->switch_flags->data.data(), header.switch_flags_size);
@@ -2869,16 +2933,15 @@ void send_game_player_state(shared_ptr<Client> to_c, shared_ptr<Client> from_c,
to_send.telepipe.owner_client_id = from_c->telepipe_state.client_id2;
to_send.telepipe.floor = from_c->telepipe_state.floor;
to_send.telepipe.unknown_a1 = from_c->telepipe_state.unknown_b3;
to_send.telepipe.x = from_c->telepipe_state.x;
to_send.telepipe.y = from_c->telepipe_state.y;
to_send.telepipe.z = from_c->telepipe_state.z;
to_send.telepipe.pos = from_c->telepipe_state.pos;
to_send.telepipe.unknown_a3 = from_c->telepipe_state.unknown_a3;
}
if (apply_overrides) {
auto from_p = from_c->character();
to_send.base.x = from_c->x;
to_send.base.z = from_c->z;
to_send.base.pos.x = from_c->pos.x;
to_send.base.pos.y = 0.0;
to_send.base.pos.z = from_c->pos.z;
to_send.bonus_hp_from_materials = from_p->inventory.hp_from_materials;
to_send.bonus_tp_from_materials = from_p->inventory.tp_from_materials;
to_send.language = from_c->language();
@@ -2928,41 +2991,44 @@ void send_game_player_state(shared_ptr<Client> to_c, shared_ptr<Client> from_c,
}
}
void send_drop_item_to_channel(shared_ptr<ServerState> s, Channel& ch, const ItemData& item,
bool from_enemy, uint8_t floor, float x, float z, uint16_t entity_id) {
uint8_t subcommand = get_pre_v1_subcommand(ch.version, 0x51, 0x58, 0x5F);
G_DropItem_PC_V3_BB_6x5F cmd = {
{{subcommand, 0x0B, 0x0000}, {floor, from_enemy, entity_id, x, z, 0, 0, item}}, 0};
cmd.item.item.encode_for_version(ch.version, s->item_parameter_table_for_encode(ch.version));
ch.send(0x60, 0x00, &cmd, sizeof(cmd));
}
void send_drop_item_to_lobby(shared_ptr<Lobby> l, const ItemData& item,
bool from_enemy, uint8_t floor, float x, float z, uint16_t entity_id) {
auto s = l->require_server_state();
for (auto& c : l->clients) {
if (!c) {
continue;
}
send_drop_item_to_channel(s, c->channel, item, from_enemy, floor, x, z, entity_id);
void send_drop_item_to_channel(
shared_ptr<ServerState> s,
Channel& ch,
const ItemData& item,
uint8_t source_type,
uint8_t floor,
const VectorXZF& pos,
uint16_t entity_index) {
if (entity_index == 0xFFFF) {
send_drop_stacked_item_to_channel(s, ch, item, floor, pos);
} else {
uint8_t subcommand = get_pre_v1_subcommand(ch.version, 0x51, 0x58, 0x5F);
G_DropItem_PC_V3_BB_6x5F cmd = {
{{subcommand, 0x0B, 0x0000}, {floor, source_type, entity_index, pos, 0, 0, item}}, 0};
cmd.item.item.encode_for_version(ch.version, s->item_parameter_table_for_encode(ch.version));
ch.send(0x60, 0x00, &cmd, sizeof(cmd));
}
}
void send_drop_stacked_item_to_channel(
shared_ptr<ServerState> s, Channel& ch, const ItemData& item, uint8_t floor, float x, float z) {
shared_ptr<ServerState> s,
Channel& ch,
const ItemData& item,
uint8_t floor,
const VectorXZF& pos) {
uint8_t subcommand = get_pre_v1_subcommand(ch.version, 0x4F, 0x56, 0x5D);
G_DropStackedItem_PC_V3_BB_6x5D cmd = {{{subcommand, 0x0A, 0x0000}, floor, 0, x, z, item}, 0};
G_DropStackedItem_PC_V3_BB_6x5D cmd = {{{subcommand, 0x0A, 0x0000}, floor, 0, pos, item}, 0};
cmd.item_data.encode_for_version(ch.version, s->item_parameter_table_for_encode(ch.version));
ch.send(0x60, 0x00, &cmd, sizeof(cmd));
}
void send_drop_stacked_item_to_lobby(shared_ptr<Lobby> l, const ItemData& item, uint8_t floor, float x, float z) {
void send_drop_stacked_item_to_lobby(shared_ptr<Lobby> l, const ItemData& item, uint8_t floor, const VectorXZF& pos) {
auto s = l->require_server_state();
for (auto& c : l->clients) {
if (!c) {
continue;
}
send_drop_stacked_item_to_channel(s, c->channel, item, floor, x, z);
send_drop_stacked_item_to_channel(s, c->channel, item, floor, pos);
}
}
@@ -3644,18 +3710,17 @@ void send_ep3_update_game_metadata(shared_ptr<Lobby> l) {
{
G_SetGameMetadata_Ep3_6xB4x52 cmd;
cmd.total_spectators = total_spectators;
if ((l->base_version != Version::GC_EP3_NTE) &&
!(s->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) {
uint8_t mask_key = (phosg::random_object<uint32_t>() % 0xFF) + 1;
set_mask_for_ep3_game_command(&cmd, sizeof(cmd), mask_key);
}
// Note: We can't use send_command_t(l, ...) here because that would send
// the same command to l and to all watcher lobbies. The commands should
// have different values depending on who's in each watcher lobby, so we
// have to manually send to each client here.
for (auto c : l->clients) {
if (c) {
send_command_t(c, 0xC9, 0x00, cmd);
if ((c->version() == Version::GC_EP3) &&
!(s->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) {
G_SetGameMetadata_Ep3_6xB4x52 masked_cmd = cmd;
uint8_t mask_key = (phosg::random_object<uint32_t>() % 0xFF) + 1;
set_mask_for_ep3_game_command(&masked_cmd, sizeof(masked_cmd), mask_key);
send_command_t(c, 0xC9, 0x00, masked_cmd);
} else {
send_command_t(c, 0xC9, 0x00, cmd);
}
}
}
}
@@ -3683,12 +3748,19 @@ void send_ep3_update_game_metadata(shared_ptr<Lobby> l) {
cmd.total_spectators = total_spectators;
cmd.text_size = text.size();
cmd.text.encode(text, 1);
if ((watcher_l->base_version != Version::GC_EP3_NTE) &&
!(s->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) {
uint8_t mask_key = (phosg::random_object<uint32_t>() % 0xFF) + 1;
set_mask_for_ep3_game_command(&cmd, sizeof(cmd), mask_key);
for (auto c : watcher_l->clients) {
if (c) {
if ((c->version() == Version::GC_EP3) &&
!(s->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) {
G_SetGameMetadata_Ep3_6xB4x52 masked_cmd = cmd;
uint8_t mask_key = (phosg::random_object<uint32_t>() % 0xFF) + 1;
set_mask_for_ep3_game_command(&masked_cmd, sizeof(masked_cmd), mask_key);
send_command_t(c, 0xC9, 0x00, masked_cmd);
} else {
send_command_t(c, 0xC9, 0x00, cmd);
}
}
}
send_command_t(watcher_l, 0xC9, 0x00, cmd);
}
}
}
@@ -3958,15 +4030,13 @@ void send_ep3_card_auction(shared_ptr<Lobby> l) {
}
num_cards = min<uint16_t>(num_cards, 0x14);
auto card_index = l->is_ep3_nte() ? s->ep3_card_index_trial : s->ep3_card_index;
uint64_t distribution_size = 0;
for (const auto& e : s->ep3_card_auction_pool) {
distribution_size += e.probability;
}
auto card_index = (l->base_version == Version::GC_EP3_NTE)
? s->ep3_card_index_trial
: s->ep3_card_index;
S_StartCardAuction_Ep3_EF cmd;
cmd.points_available = s->ep3_card_auction_points;
for (size_t z = 0; z < num_cards; z++) {
@@ -4002,10 +4072,9 @@ void send_server_time(shared_ptr<Client> c) {
gmtime_r(&t_secs, &t_parsed);
string time_str(128, 0);
size_t len = strftime(time_str.data(), time_str.size(),
"%Y:%m:%d: %H:%M:%S.000", &t_parsed);
size_t len = strftime(time_str.data(), time_str.size(), "%Y:%m:%d: %H:%M:%S.000", &t_parsed);
if (len == 0) {
throw runtime_error("phosg::format_time buffer too short");
throw logic_error("strftime buffer too short");
}
time_str.resize(len);
+3 -5
View File
@@ -354,13 +354,11 @@ void send_game_set_state(std::shared_ptr<Client> c);
void send_game_flag_state(std::shared_ptr<Client> c);
void send_game_player_state(std::shared_ptr<Client> to_c, std::shared_ptr<Client> from_c, bool apply_overrides);
void send_drop_item_to_channel(std::shared_ptr<ServerState> s, Channel& ch, const ItemData& item,
bool from_enemy, uint8_t floor, float x, float z, uint16_t request_id);
void send_drop_item_to_lobby(std::shared_ptr<Lobby> l, const ItemData& item,
bool from_enemy, uint8_t floor, float x, float z, uint16_t request_id);
uint8_t source_type, uint8_t floor, const VectorXZF& pos, uint16_t entity_index);
void send_drop_stacked_item_to_channel(
std::shared_ptr<ServerState> s, Channel& ch, const ItemData& item, uint8_t floor, float x, float z);
std::shared_ptr<ServerState> s, Channel& ch, const ItemData& item, uint8_t floor, const VectorXZF& pos);
void send_drop_stacked_item_to_lobby(
std::shared_ptr<Lobby> l, const ItemData& item, uint8_t floor, float x, float z);
std::shared_ptr<Lobby> l, const ItemData& item, uint8_t floor, const VectorXZF& pos);
void send_pick_up_item_to_client(std::shared_ptr<Client> c, uint8_t client_id, uint32_t id, uint8_t floor);
void send_create_inventory_item_to_client(std::shared_ptr<Client> c, uint8_t client_id, const ItemData& item);
void send_create_inventory_item_to_lobby(std::shared_ptr<Client> c, uint8_t client_id, const ItemData& item, bool exclude_c = false);
+4 -2
View File
@@ -256,6 +256,8 @@ CommandDefinition c_reload(
args.s->load_bb_private_keys(true);
} else if (type == "accounts") {
args.s->load_accounts(true);
} else if (type == "maps") {
args.s->load_maps(true);
} else if (type == "caches") {
args.s->clear_file_caches(true);
} else if (type == "patch-files") {
@@ -1175,8 +1177,8 @@ void fn_create_item(CommandArgs& args) {
send_text_message(ses->client_channel, "$C7Next drop:\n" + name);
} else {
send_drop_stacked_item_to_channel(s, ses->client_channel, item, ses->floor, ses->x, ses->z);
send_drop_stacked_item_to_channel(s, ses->server_channel, item, ses->floor, ses->x, ses->z);
send_drop_stacked_item_to_channel(s, ses->client_channel, item, ses->floor, ses->pos);
send_drop_stacked_item_to_channel(s, ses->server_channel, item, ses->floor, ses->pos);
string name = s->describe_item(ses->version(), ses->next_drop_item, true);
send_text_message(ses->client_channel, "$C7Item created:\n" + name);
+200 -17
View File
@@ -3,6 +3,7 @@
#include <string.h>
#include <memory>
#include <phosg/Filesystem.hh>
#include <phosg/Image.hh>
#include <phosg/Network.hh>
#include <phosg/Platform.hh>
@@ -563,11 +564,6 @@ shared_ptr<const string> ServerState::load_bb_file(
}
shared_ptr<const string> ServerState::load_map_file(Version version, const string& filename) const {
auto& cache = this->map_file_caches.at(static_cast<size_t>(version));
return cache->get(filename, bind(&ServerState::load_map_file_uncached, this, version, placeholders::_1));
}
shared_ptr<const string> ServerState::load_map_file_uncached(Version version, const string& filename) const {
if (version == Version::BB_V4) {
try {
return this->load_bb_file(filename);
@@ -1044,8 +1040,32 @@ void ServerState::load_config_early() {
} catch (const out_of_range&) {
}
this->allow_dc_pc_games = this->config_json->get_bool("AllowDCPCGames", true);
this->allow_gc_xb_games = this->config_json->get_bool("AllowGCXBGames", true);
try {
const auto& groups = this->config_json->get_list("CompatibilityGroups");
this->compatibility_groups.fill(0);
for (size_t v_s = 0; v_s < groups.size(); v_s++) {
this->compatibility_groups[v_s] = groups[v_s]->as_int();
}
} catch (const out_of_range&) {
static_assert(NUM_VERSIONS == 14, "Don't forget to update the default compatibility groups");
this->compatibility_groups = {
0x0000, // PC_PATCH
0x0000, // BB_PATCH
0x0004, // DC_NTE compatible only with itself
0x0008, // DC_V1_11_2000_PROTOTYPE compatible only with itself
0x00B0, // DC_V1 compatible with DC_V1, DC_V2, and PC_V2
0x00B0, // DC_V2 compatible with DC_V1, DC_V2, and PC_V2
0x0040, // PC_NTE compatible only with itself
0x00B0, // PC_V2 compatible with DC_V1, DC_V2, and PC_V2
0x0100, // GC_NTE compatible only with itself
0x1200, // GC_V3 compatible with GC_V3 and XB_V3
0x0400, // GC_EP3_NTE compatible only with itself
0x0800, // GC_EP3 compatible only with itself
0x1200, // XB_V3 compatible with GC_V3 and XB_V3
0x2000, // BB_V4 compatible only with itself
};
}
this->enable_chat_commands = this->config_json->get_bool("EnableChatCommands", true);
this->version_name_colors.reset();
@@ -1216,20 +1236,20 @@ void ServerState::load_config_early() {
}
for (size_t z = 0; z < 4; z++) {
shared_ptr<const Map::RareEnemyRates> prev = Map::DEFAULT_RARE_ENEMIES;
shared_ptr<const MapState::RareEnemyRates> prev = MapState::DEFAULT_RARE_ENEMIES;
try {
string key = "RareEnemyRates-";
key += token_name_for_difficulty(z);
this->rare_enemy_rates_by_difficulty[z] = make_shared<Map::RareEnemyRates>(this->config_json->at(key));
this->rare_enemy_rates_by_difficulty[z] = make_shared<MapState::RareEnemyRates>(this->config_json->at(key));
prev = this->rare_enemy_rates_by_difficulty[z];
} catch (const out_of_range&) {
this->rare_enemy_rates_by_difficulty[z] = prev;
}
}
try {
this->rare_enemy_rates_challenge = make_shared<Map::RareEnemyRates>(this->config_json->at("RareEnemyRates-Challenge"));
this->rare_enemy_rates_challenge = make_shared<MapState::RareEnemyRates>(this->config_json->at("RareEnemyRates-Challenge"));
} catch (const out_of_range&) {
this->rare_enemy_rates_challenge = Map::DEFAULT_RARE_ENEMIES;
this->rare_enemy_rates_challenge = MapState::DEFAULT_RARE_ENEMIES;
}
this->min_levels_v4[0] = DEFAULT_MIN_LEVELS_V4_EP1;
@@ -1398,7 +1418,7 @@ void ServerState::load_config_late() {
} catch (const out_of_range&) {
}
auto parse_primary_identifier_list = [&](const char* key, Version base_version) -> unordered_set<uint32_t> {
auto parse_primary_identifier_list = [&](const char* key, Version v) -> unordered_set<uint32_t> {
unordered_set<uint32_t> ret;
try {
for (const auto& pi_json : this->config_json->get_list(key)) {
@@ -1406,7 +1426,7 @@ void ServerState::load_config_late() {
ret.emplace(pi_json->as_int());
} else {
try {
auto item = this->parse_item_description(base_version, pi_json->as_string());
auto item = this->parse_item_description(v, pi_json->as_string());
ret.emplace(item.primary_identifier());
} catch (const exception& e) {
config_log.warning("Cannot parse item description \"%s\": %s (skipping entry)", pi_json->as_string().c_str(), e.what());
@@ -1532,12 +1552,174 @@ void ServerState::load_patch_indexes(bool from_non_event_thread) {
this->forward_or_call(from_non_event_thread, std::move(set));
}
void ServerState::load_maps(bool from_non_event_thread) {
using SDT = SetDataTable;
config_log.info("Loading free play map files");
unordered_map<uint64_t, shared_ptr<const MapFile>> map_file_for_source_hash;
map<uint32_t, array<shared_ptr<const MapFile>, NUM_VERSIONS>> map_files;
for (Version v : ALL_ARPG_SEMANTIC_VERSIONS) {
const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
for (Episode episode : episodes) {
if ((episode == Episode::EP2 && is_v1_or_v2(v) && (v != Version::GC_NTE)) ||
(episode == Episode::EP4 && !is_v4(v))) {
continue;
}
const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
for (GameMode mode : modes) {
if (((mode == GameMode::BATTLE || mode == GameMode::CHALLENGE) && is_v1(v)) ||
(mode == GameMode::SOLO && !is_v4(v))) {
continue;
}
for (uint8_t difficulty = 0; difficulty < 4; difficulty++) {
if ((difficulty == 3) && is_v1(v)) {
continue;
}
auto sdt = this->set_data_table(v, episode, mode, difficulty);
for (uint8_t floor = 0; floor < 0x12; floor++) {
auto variation_maxes = sdt->num_free_play_variations_for_floor(episode, mode == GameMode::SOLO, floor);
for (size_t var_layout = 0; var_layout < variation_maxes.layout; var_layout++) {
for (size_t var_entities = 0; var_entities < variation_maxes.entities; var_entities++) {
uint32_t supermap_key = this->supermap_key(episode, mode, difficulty, floor, var_layout, var_entities);
auto objects_filename = sdt->map_filename_for_variation(
episode, mode, floor, var_layout, var_entities, SDT::FilenameType::OBJECT_SETS);
auto enemies_filename = sdt->map_filename_for_variation(
episode, mode, floor, var_layout, var_entities, SDT::FilenameType::ENEMY_SETS);
auto events_filename = sdt->map_filename_for_variation(
episode, mode, floor, var_layout, var_entities, SDT::FilenameType::EVENTS);
auto objects_data = objects_filename.empty() ? nullptr : this->load_map_file(v, objects_filename);
auto enemies_data = enemies_filename.empty() ? nullptr : this->load_map_file(v, enemies_filename);
auto events_data = enemies_filename.empty() ? nullptr : this->load_map_file(v, events_filename);
if (objects_data || enemies_data || events_data) {
// TODO: This is ugly; the hash computation probably should be factored into MapFile
uint64_t source_hash = ((objects_data ? phosg::fnv1a64(*objects_data) : 0) ^
(enemies_data ? phosg::fnv1a64(*enemies_data) : 0) ^
(events_data ? phosg::fnv1a64(*events_data) : 0));
shared_ptr<const MapFile> map_file;
try {
map_file = map_file_for_source_hash.at(source_hash);
} catch (const out_of_range&) {
map_file = make_shared<MapFile>(floor, objects_data, enemies_data, events_data);
if (map_file->source_hash() != source_hash) {
throw logic_error("incorrect source hash");
}
map_file_for_source_hash.emplace(map_file->source_hash(), map_file);
}
// Uncomment for debugging
// config_log.info("Maps for %s %s %s %s %02hhX %02zu %02zu (%08" PRIX32 " => %016" PRIX64 "): objects=%s(%s)+0x%zX enemies=%s(%s)+0x%zX events=%s(%s)+0x%zX",
// phosg::name_for_enum(v),
// name_for_episode(episode),
// name_for_mode(mode),
// name_for_difficulty(difficulty),
// floor,
// var_layout,
// var_entities,
// supermap_key,
// map_file->source_hash(),
// objects_filename.empty() ? "(none)" : objects_filename.c_str(),
// objects_data ? "present" : "missing",
// map_file->count_object_sets(),
// enemies_filename.empty() ? "(none)" : enemies_filename.c_str(),
// enemies_data ? "present" : "missing",
// map_file->count_enemy_sets(),
// events_filename.empty() ? "(none)" : events_filename.c_str(),
// events_data ? "present" : "missing",
// map_file->count_events());
map_files[supermap_key].at(static_cast<size_t>(v)) = map_file;
}
}
}
}
}
}
}
}
config_log.info("Constructing free play supermaps");
unordered_map<uint64_t, shared_ptr<const SuperMap>> supermap_for_source_hash_sum;
unordered_map<uint32_t, shared_ptr<const SuperMap>> new_supermaps;
for (const auto& it : map_files) {
uint64_t source_hash_sum = 0;
for (auto map_file : it.second) {
source_hash_sum += map_file ? map_file->source_hash() : 0;
}
Episode episode = static_cast<Episode>((it.first >> 28) & 7);
// Uncomment for debugging
// auto mode = static_cast<GameMode>((it.first >> 26) & 3);
// uint8_t difficulty = (it.first >> 24) & 3;
// uint8_t floor = (it.first >> 16) & 0xFF;
// uint8_t layout = (it.first >> 8) & 0xFF;
// uint8_t entities = (it.first >> 0) & 0xFF;
// fprintf(stderr, "SuperMap for %s %s %s %02hhX %02hhX %02hhX (%08" PRIX32 "): %016" PRIX64 " from",
// name_for_episode(episode),
// name_for_mode(mode),
// name_for_difficulty(difficulty),
// floor,
// layout,
// entities,
// it.first,
// source_hash_sum);
// for (const auto& map_file : it.second) {
// if (map_file) {
// fprintf(stderr, " %016" PRIX64, map_file->source_hash());
// } else {
// fprintf(stderr, " ----------------");
// }
// }
// fputc('\n', stderr);
shared_ptr<const SuperMap> supermap;
try {
supermap = supermap_for_source_hash_sum.at(source_hash_sum);
static_game_data_log.info("Linking existing free play supermap %016" PRIX64 " for key %08" PRIX32, source_hash_sum, it.first);
} catch (const out_of_range&) {
supermap = make_shared<SuperMap>(episode, it.second);
supermap_for_source_hash_sum.emplace(source_hash_sum, supermap);
static_game_data_log.info("Constructed free play supermap %016" PRIX64 " for key %08" PRIX32, source_hash_sum, it.first);
}
new_supermaps.emplace(it.first, supermap);
}
auto set = [s = this->shared_from_this(), new_supermaps = std::move(new_supermaps)]() {
s->supermaps = std::move(new_supermaps);
};
this->forward_or_call(from_non_event_thread, std::move(set));
}
vector<shared_ptr<const SuperMap>> ServerState::supermaps_for_variations(
Episode episode,
GameMode mode,
uint8_t difficulty,
const Variations& variations) const {
vector<shared_ptr<const SuperMap>> ret;
for (size_t floor = 0; floor < 0x12; floor++) {
Variations::Entry e;
if (floor < variations.entries.size()) {
e = variations.entries[floor];
}
ret.push_back(this->get_supermap(episode, mode, difficulty, floor, e.layout, e.entities));
if (ret.back()) {
static_game_data_log.info("Using supermap %08" PRIX32 " for floor %02zX layout %" PRIX32 " entities %" PRIX32,
this->supermap_key(episode, mode, difficulty, floor, e.layout, e.entities),
floor, e.layout.load(), e.entities.load());
} else {
static_game_data_log.info("No supermap available for floor %02zX layout %" PRIX32 " entities %" PRIX32,
floor, e.layout.load(), e.entities.load());
}
}
return ret;
}
void ServerState::clear_file_caches(bool from_non_event_thread) {
auto set = [s = this->shared_from_this()]() {
config_log.info("Clearing map file caches");
for (auto& cache : s->map_file_caches) {
cache = make_shared<ThreadSafeFileCache>();
}
config_log.info("Clearing BB stream file cache");
s->bb_stream_files_cache.reset(new FileContentsCache(3600000000ULL));
config_log.info("Clearing BB system cache");
@@ -2041,6 +2223,7 @@ void ServerState::load_all() {
this->load_dol_files(false);
this->create_default_lobbies();
this->load_set_data_tables(false);
this->load_maps(false);
this->load_battle_params(false);
this->load_level_tables(false);
this->load_text_index(false);
+28 -5
View File
@@ -115,8 +115,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
bool allow_unregistered_users = false;
bool allow_pc_nte = false;
bool use_temp_accounts_for_prototypes = true;
bool allow_dc_pc_games = true;
bool allow_gc_xb_games = true;
std::array<uint16_t, NUM_VERSIONS> compatibility_groups = {};
bool enable_chat_commands = true;
std::unique_ptr<std::array<uint32_t, NUM_NON_PATCH_VERSIONS>> version_name_colors;
uint32_t client_customization_name_color = 0x00000000;
@@ -173,7 +172,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
std::shared_ptr<const FunctionCodeIndex> function_code_index;
std::shared_ptr<const PatchFileIndex> pc_patch_file_index;
std::shared_ptr<const PatchFileIndex> bb_patch_file_index;
std::array<std::shared_ptr<ThreadSafeFileCache>, NUM_VERSIONS> map_file_caches;
std::unordered_map<uint32_t, std::shared_ptr<const SuperMap>> supermaps; // Keyed by supermap_key
std::shared_ptr<FileContentsCache> bb_stream_files_cache;
std::shared_ptr<FileContentsCache> bb_system_cache;
std::shared_ptr<FileContentsCache> gba_files_cache;
@@ -210,8 +209,8 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
std::array<std::shared_ptr<const SetDataTableBase>, NUM_VERSIONS> set_data_tables_ep1_ult;
std::shared_ptr<const SetDataTableBase> bb_solo_set_data_table;
std::shared_ptr<const SetDataTableBase> bb_solo_set_data_table_ep1_ult;
std::array<std::shared_ptr<const Map::RareEnemyRates>, 4> rare_enemy_rates_by_difficulty;
std::shared_ptr<const Map::RareEnemyRates> rare_enemy_rates_challenge;
std::array<std::shared_ptr<const MapState::RareEnemyRates>, 4> rare_enemy_rates_by_difficulty;
std::shared_ptr<const MapState::RareEnemyRates> rare_enemy_rates_challenge;
std::array<std::array<size_t, 4>, 3> min_levels_v4; // Indexed as [episode][difficulty]
std::vector<std::string> bb_required_patches;
CheatFlags cheat_flags;
@@ -401,6 +400,29 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
std::shared_ptr<PatchServer::Config> generate_patch_server_config(bool is_bb) const;
void update_dependent_server_configs() const;
static constexpr uint32_t supermap_key(
Episode episode, GameMode mode, uint8_t difficulty, uint8_t floor, uint32_t layout, uint32_t entities) {
return (static_cast<uint32_t>(episode) << 28) |
(static_cast<uint32_t>(mode) << 26) |
(static_cast<uint32_t>(difficulty) << 24) |
(static_cast<uint32_t>(floor) << 16) |
(static_cast<uint32_t>(layout) << 8) |
(static_cast<uint32_t>(entities) << 0);
}
inline std::shared_ptr<const SuperMap> get_supermap(
Episode episode, GameMode mode, uint8_t difficulty, uint8_t floor, uint32_t layout, uint32_t entities) const {
try {
return this->supermaps.at(this->supermap_key(episode, mode, difficulty, floor, layout, entities));
} catch (const std::out_of_range&) {
return nullptr;
}
}
std::vector<std::shared_ptr<const SuperMap>> supermaps_for_variations(
Episode episode,
GameMode mode,
uint8_t difficulty,
const Variations& variations) const;
// The following functions may only be called from a non-event thread if they
// take a from_non_event_thread argument; any function that does not have this
// argument must be called only from the event thread.
@@ -413,6 +435,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
void load_accounts(bool from_non_event_thread);
void load_teams(bool from_non_event_thread);
void load_patch_indexes(bool from_non_event_thread);
void load_maps(bool from_non_event_thread);
void clear_file_caches(bool from_non_event_thread);
void load_battle_params(bool from_non_event_thread);
void load_level_tables(bool from_non_event_thread);
+2 -1
View File
@@ -6,6 +6,7 @@
#include <set>
#include <stdexcept>
#include "CommonFileFormats.hh"
#include "Compression.hh"
#include "Loggers.hh"
#include "PSOEncryption.hh"
@@ -178,7 +179,7 @@ BinaryTextSet::BinaryTextSet(const std::string& pr2_data, size_t collection_coun
// offsets in the file instead.
::set<uint32_t> used_offsets;
size_t root_offset = has_rel_footer
? r.pget_u32l(r.size() - 0x10)
? r.pget<RELFileFooter>(r.size() - 0x20).root_offset.load()
: (r.size() - collection_count * sizeof(le_uint32_t));
phosg::StringReader collection_offsets_r = r.sub(root_offset, collection_count * sizeof(le_uint32_t));
+63 -20
View File
@@ -24,7 +24,50 @@ enum class Version {
UNKNOWN = 15,
};
constexpr size_t NUM_VERSIONS = static_cast<size_t>(Version::BB_V4) + 1;
constexpr std::array<Version, 14> ALL_VERSIONS = {
Version::PC_PATCH,
Version::BB_PATCH,
Version::DC_NTE,
Version::DC_V1_11_2000_PROTOTYPE,
Version::DC_V1,
Version::DC_V2,
Version::PC_NTE,
Version::PC_V2,
Version::GC_NTE,
Version::GC_V3,
Version::GC_EP3_NTE,
Version::GC_EP3,
Version::XB_V3,
Version::BB_V4,
};
constexpr std::array<Version, 12> ALL_NON_PATCH_VERSIONS = {
Version::DC_NTE,
Version::DC_V1_11_2000_PROTOTYPE,
Version::DC_V1,
Version::DC_V2,
Version::PC_NTE,
Version::PC_V2,
Version::GC_NTE,
Version::GC_V3,
Version::GC_EP3_NTE,
Version::GC_EP3,
Version::XB_V3,
Version::BB_V4,
};
constexpr std::array<Version, 10> ALL_ARPG_SEMANTIC_VERSIONS = {
Version::DC_NTE,
Version::DC_V1_11_2000_PROTOTYPE,
Version::DC_V1,
Version::DC_V2,
Version::PC_NTE,
Version::PC_V2,
Version::GC_NTE,
Version::GC_V3,
Version::XB_V3,
Version::BB_V4,
};
constexpr size_t NUM_VERSIONS = ALL_VERSIONS.size();
constexpr size_t NUM_PATCH_VERSIONS = static_cast<size_t>(Version::BB_PATCH) + 1;
constexpr size_t NUM_NON_PATCH_VERSIONS = NUM_VERSIONS - NUM_PATCH_VERSIONS;
@@ -35,7 +78,7 @@ const char* phosg::name_for_enum<Version>(Version v);
template <>
Version phosg::enum_for_name<Version>(const char* name);
inline bool is_any_nte(Version version) {
constexpr bool is_any_nte(Version version) {
return (version == Version::DC_NTE) ||
(version == Version::DC_V1_11_2000_PROTOTYPE) ||
(version == Version::PC_NTE) ||
@@ -43,55 +86,55 @@ inline bool is_any_nte(Version version) {
(version == Version::GC_EP3_NTE);
}
inline bool is_patch(Version version) {
constexpr bool is_patch(Version version) {
return (version == Version::PC_PATCH) || (version == Version::BB_PATCH);
}
inline bool is_pre_v1(Version version) {
constexpr bool is_pre_v1(Version version) {
return (version == Version::DC_NTE) || (version == Version::DC_V1_11_2000_PROTOTYPE);
}
inline bool is_v1(Version version) {
constexpr bool is_v1(Version version) {
return (version == Version::DC_NTE) || (version == Version::DC_V1_11_2000_PROTOTYPE) || (version == Version::DC_V1);
}
inline bool is_v2(Version version) {
constexpr bool is_v2(Version version) {
return (version == Version::DC_V2) || (version == Version::PC_NTE) || (version == Version::PC_V2) || (version == Version::GC_NTE);
}
inline bool is_v1_or_v2(Version version) {
constexpr bool is_v1_or_v2(Version version) {
return is_v1(version) || is_v2(version);
}
inline bool is_v3(Version version) {
constexpr bool is_v3(Version version) {
return (version == Version::GC_V3) ||
(version == Version::GC_EP3_NTE) ||
(version == Version::GC_EP3) ||
(version == Version::XB_V3);
}
inline bool is_v4(Version version) {
constexpr bool is_v4(Version version) {
return (version == Version::BB_V4);
}
inline bool is_ep3(Version version) {
constexpr bool is_ep3(Version version) {
return (version == Version::GC_EP3_NTE) || (version == Version::GC_EP3);
}
inline bool is_dc(Version version) {
constexpr bool is_dc(Version version) {
return (version == Version::DC_NTE) ||
(version == Version::DC_V1_11_2000_PROTOTYPE) ||
(version == Version::DC_V1) ||
(version == Version::DC_V2);
}
inline bool is_gc(Version version) {
constexpr bool is_gc(Version version) {
return (version == Version::GC_NTE) ||
(version == Version::GC_V3) ||
(version == Version::GC_EP3_NTE) ||
(version == Version::GC_EP3);
}
inline bool is_sh4(Version version) {
constexpr bool is_sh4(Version version) {
return (version == Version::DC_NTE) ||
(version == Version::DC_V1_11_2000_PROTOTYPE) ||
(version == Version::DC_V1) ||
(version == Version::DC_V2);
}
inline bool is_x86(Version version) {
constexpr bool is_x86(Version version) {
return (version == Version::PC_PATCH) ||
(version == Version::BB_PATCH) ||
(version == Version::PC_NTE) ||
@@ -99,20 +142,20 @@ inline bool is_x86(Version version) {
(version == Version::XB_V3) ||
(version == Version::BB_V4);
}
inline bool is_ppc(Version version) {
constexpr bool is_ppc(Version version) {
return (version == Version::GC_NTE) ||
(version == Version::GC_V3) ||
(version == Version::GC_EP3_NTE) ||
(version == Version::GC_EP3);
}
inline bool is_big_endian(Version version) {
constexpr bool is_big_endian(Version version) {
return (version == Version::GC_NTE) ||
(version == Version::GC_V3) ||
(version == Version::GC_EP3_NTE) ||
(version == Version::GC_EP3);
}
inline bool uses_v2_encryption(Version version) {
constexpr bool uses_v2_encryption(Version version) {
return (version == Version::PC_PATCH) ||
(version == Version::BB_PATCH) ||
(version == Version::DC_NTE) ||
@@ -122,17 +165,17 @@ inline bool uses_v2_encryption(Version version) {
(version == Version::PC_V2) ||
(version == Version::GC_NTE);
}
inline bool uses_v3_encryption(Version version) {
constexpr bool uses_v3_encryption(Version version) {
return (version == Version::GC_V3) ||
(version == Version::GC_EP3_NTE) ||
(version == Version::GC_EP3) ||
(version == Version::XB_V3);
}
inline bool uses_v4_encryption(Version version) {
constexpr bool uses_v4_encryption(Version version) {
return (version == Version::BB_V4);
}
inline bool uses_utf16(Version version) {
constexpr bool uses_utf16(Version version) {
return (version == Version::PC_PATCH) ||
(version == Version::BB_PATCH) ||
(version == Version::PC_NTE) ||
+5 -7
View File
@@ -70,8 +70,8 @@ struct BBRoot {
template <bool BE, size_t StringTableCount, size_t TokenCount>
void WordSelectSet::parse_non_windows_t(const std::string& data, bool use_sjis) {
phosg::StringReader r(data);
uint32_t root_offset = r.pget<U32T<BE>>(r.size() - 0x10);
const auto& root = r.pget<NonWindowsRootT<BE>>(root_offset);
const auto& footer = r.pget<RELFileFooterT<BE>>(r.size() - sizeof(RELFileFooterT<BE>));
const auto& root = r.pget<NonWindowsRootT<BE>>(footer.root_offset);
{
auto string_offset_r = r.sub(root.strings_table, sizeof(U32T<BE>) * StringTableCount);
@@ -96,8 +96,8 @@ void WordSelectSet::parse_windows_t(const std::string& data, const std::vector<s
}
phosg::StringReader r(data);
uint32_t root_offset = r.pget<le_uint32_t>(r.size() - 0x10);
const auto& root = r.pget<RootT>(root_offset);
const auto& footer = r.pget<RELFileFooter>(r.size() - sizeof(RELFileFooter));
const auto& root = r.pget<RootT>(footer.root_offset);
this->strings = *unitxt_collection;
// this->table1 = read_indirect_table<uint16_t, le_uint16_t, le_uint32_t>(r, root.table1, Table1Count);
// this->table2 = read_indirect_table<uint16_t, le_uint16_t, le_uint32_t>(r, root.table2, Table2Count);
@@ -325,7 +325,5 @@ WordSelectMessage WordSelectTable::translate(
}
WordSelectTable::Token::Token() {
for (size_t z = 0; z < this->values_by_version.size(); z++) {
this->values_by_version[z] = 0xFFFF;
}
this->values_by_version.fill(0xFFFF);
}