diff --git a/README.md b/README.md index 0dabecd8..bf2aa103 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ If you want to use parts of newserv in your project, there are two easy ways to # Compatibility -newserv supports all known versions of PSO, including development prototypes. This table lists all versions that newserv supports. (NTE stands for Network Trial Edition; the GameCube beta versions were called Trial Edition instead, but we use the NTE abbreviation anyway for consistency.) +newserv supports all known versions of PSO, including various development prototypes. This table lists all versions that newserv supports. (NTE stands for Network Trial Edition; the GameCube beta versions were called Trial Edition instead, but we use the NTE abbreviation anyway for consistency.) | Version | Lobbies | Games | Proxy | |-----------------|----------|----------|----------| @@ -339,13 +339,13 @@ Quest contents are cached in memory, but if you've changed the contents of the q newserv supports server-side item generation on all game versions, except for the earliest DC prototypes (NTE and 11/2000). By default, the game behaves as it did on the original servers - on all versions except BB, item drops are controlled by the leader client in each game, and on BB, item drops are controlled by the server. There are five different available behaviors for item drops: -* `DISABLED` (or `NONE`): No items will drop from boxes or enemies. -* `CLIENT`: The game leader generates items, all items are visible to all players, and any player may pick up any item. This is the default mode for all game versions, except this mode cannot be used on BB. -* `SERVER_SHARED`: The server generates items, all items are visible to all players, and any player may pick up any item. This is the default mode for BB. -* `SERVER_PRIVATE`: The server generates items, but each player may get a different item from any box or enemy. If a player isn't in the same area as an enemy at the time it's defeated, they won't get any item from it. Items dropped by players are visible to everyone. -* `SERVER_DUPLICATE`: The server generates items, and each player will get the same item from any box or enemy, but there is one copy of each item for each player (and each player only sees their own copy of the item). If a player isn't in the same area as an enemy at the time it's defeated, they won't get any item from it. Items dropped by players are not duplicated and are visible to everyone. +* `disabled` (or `none`): No items will drop from boxes or enemies. +* `client`: The game leader generates items, all items are visible to all players, and any player may pick up any item. This is the default mode for all game versions, except this mode cannot be used if the game leader is on BB. +* `shared`: The server generates items, all items are visible to all players, and any player may pick up any item. This is the default mode if the game leader is on BB. +* `private`: The server generates items, but each player may get a different item from any box or enemy. If a player isn't in the same area as an enemy at the time it's defeated, they won't get any item from it. Items dropped by players are visible to everyone. +* `duplicate`: The server generates items, and each player will get the same item from any box or enemy, but there is one copy of each item for each player (and each player only sees their own copy of the item). If a player isn't in the same area as an enemy at the time it's defeated, they won't get any item from it. Items dropped by players are not duplicated and are visible to everyone. -In the `SERVER_PRIVATE` and `SERVER_DUPLICATE` modes, there is no incentive to pick up items before another player, since other players cannot pick up the items you see dropped from boxes and enemies. However, if you pick up an item and drop it later, it can then be seen and picked up by any player. +In the `private` and `duplicate` modes, there is no incentive to pick up items before another player, since other players cannot pick up the items you see dropped from boxes and enemies. However, if you pick up an item and drop it later, it can then be seen and picked up by any player. The drop mode can be changed at any time during a game with the `$dropmode` chat command. If the mode is changed after some items have already been dropped, the existing items retain their visibility (that is, items dropped in private mode still can't be picked up by other players since they were dropped before the mode was changed). You can configure which drop modes are used by default, and which modes players are allowed to choose, in config.json. See the comments above the AllowedDropModes and DefaultDropMode keys. @@ -353,13 +353,18 @@ In the server drop modes, the item tables used to generate common items are in t ## Cross-version play -All versions of PSO can see and interact with each other in the lobby. newserv also allows some versions to play in-game with each other: -* DC V1 players can join DC V2 games if the difficulty level isn't set to Ultimate and the creator chose to allow V1 players. -* DC V2 players can join DC V1 games. -* If AllowDCPCGames is enabled in config.json, PC and DC players can join each other's games. DC V1 players cannot join PC games with the Ultimate difficulty level. -* If AllowGCXBGames is enabled in config.json, GC and Xbox players can join each other's games. +All versions of PSO can see and interact with each other in the lobby. By default, newserv allows V1 and V2 players to play together, and allows GC and Xbox players to play together. You can change these rules with the CompatibilityGroups setting in config.json. -In V1/V2 cross-version play, when any of the server drop modes are used, the server uses the drop table corresponding to the version the game was created with. (For example, if a DC V1 player created the game, rare-table-v1.json will be used, even after V2 players join.) +There are several cross-version restrictions that always apply regardless of the compatibility groups setting: +* DC V1 players cannot join DC V2 games if the game creator didn't choose to allow them. +* DC V1 players cannot join games if the difficulty level is set to Ultimate or the game mode is Battle or Challenge. +* Only GC, Xbox, and BB players can join games in Episode 2. +* Only BB players can join games in Episode 4. +* Episode 3 players cannot join non-Episode 3 games, and vice versa. + +V1/V2 compatibility and GC/Xbox compatibility are well-tested, but other situations are not. Not much attention has been given to how items should be handled across major versions; if you enable v2/GC compatibility, for example, there will likely be bugs. Please report such bugs as GitHub issues. + +In cross-version play, when any of the server drop modes are used, the server uses the drop tables corresponding to the leader's version and section ID. (For example, if a DC V1 player is the game leader, rare-table-v1.json will be used, even after V2 players join.) If a BB player is the leader and the `client` drop mode is used, the server generates items as if it were in `shared` mode. ## Server-side saves @@ -371,7 +376,7 @@ There is a third command, `$bbchar `, which behaves Exactly which data is saved and loaded depends on the game version: -| Game | Inventory | Character | Options/chats | Quest flags | Bank | Battle/challenge | +| Game | Inventory | Character | Options/chats | Quest flags | Bank | Battle/Challenge | |----------------------|-----------|-----------|---------------|-------------|------|------------------| | PSO DC v1 prototypes | Yes | Yes | No | No | No | N/A | | PSO DC v1 | Yes | Yes | No | No | No | N/A | @@ -547,7 +552,7 @@ Some commands only work on the game server and not on the proxy server. The chat * You'll see in-game messages from the server when you take certain actions, like killing an enemy in BB. * You'll see the rare seed value and floor variations when you join a game. * You'll be placed into the last available slot in lobbies and games instead of the first, unless you're joining a BB solo-mode game. - * You'll be able to join games with any PSO version, not only those for which crossplay is normally supported. Be prepared for client crashes and other client-side brokenness if you do this. Do not submit any issues for broken behaviors in crossplay, unless the situation is explicitly supported (see the "Cross-version play" section above). + * You'll be able to join games with any PSO version, not only those for which crossplay is normally enabled. See the "Cross-version play" section above for details on this. * The rest of the commands in this section are enabled on the game server. (They are always enabled on the proxy server.) * `$readmem
` (game server only): Reads 4 bytes from the given address and shows you the values. * `$writemem
` (game server only): Writes data to the given address. Data is not required to be any specific size. @@ -729,7 +734,6 @@ There are several actions that don't fit well into the table above, which let yo * Run a brute-force search for a decryption seed (`find-decryption-seed`) * Format Episode 3 game data in a human-readable manner (`show-ep3-maps`, `show-ep3-cards`, `generate-ep3-cards-html`) * Format Blue Burst battle parameter files in a human-readable manner (`show-battle-params`) -* Search for rare enemy seeds that result in rare enemies on console versions (`find-rare-enemy-seeds`) * Convert item data to a human-readable description, or vice versa (`describe-item`) * Connect to another PSO server and pretend to be a client (`cat-client`) * Generate or describe DC serial numbers (`generate-dc-serial-number`, `inspect-dc-serial-number`) diff --git a/src/BattleParamsIndex.hh b/src/BattleParamsIndex.hh index f6cdf209..fb4e0390 100644 --- a/src/BattleParamsIndex.hh +++ b/src/BattleParamsIndex.hh @@ -92,7 +92,7 @@ public: private: struct File { std::shared_ptr data; - const Table* table; + const Table* table = nullptr; }; // Indexed as [online/offline][episode] diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index 064f5c26..bd3ade92 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -465,7 +465,7 @@ static void server_command_swsetall(shared_ptr 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 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 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 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 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>(); - c->override_variations->clear(0); - for (size_t z = 0; z < min(c->override_variations->size(), args.size()); z++) { - c->override_variations->at(z) = args[z] - '0'; + c->override_variations = make_unique(); + for (size_t z = 0; z < min(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 c, const std::string& args) { @@ -1441,7 +1447,7 @@ static void server_command_min_level(shared_ptr 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 ses, const static void server_command_where(shared_ptr 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(c->x), static_cast(c->z)); + c->floor, + short_name_for_floor(l->episode, c->floor), + static_cast(c->pos.x.load()), + static_cast(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 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 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 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 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); diff --git a/src/Client.cc b/src/Client.cc index 20ffa934..c9c88a43 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -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); } diff --git a/src/Client.hh b/src/Client.hh index 77057d00..2383b183 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -206,10 +206,9 @@ public: // Lobby/positioning Config config; Config synced_config; - std::unique_ptr> override_variations; + std::unique_ptr override_variations; int32_t sub_version; - float x; - float z; + VectorXZF pos; uint32_t floor; std::weak_ptr lobby; uint8_t lobby_client_id; diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 72747113..e655e397 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -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 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 variations; + Variations variations; parray lobby_data; } __packed_ws__(S_JoinGame_DCNTE_64, 0x104); using S_JoinGame_DC_64 = S_JoinGameT_DC_PC; @@ -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 -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 relocations_offset = 0; // Relative to code base (after checksum_size) - U32T num_relocations = 0; - parray, 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 entrypoint_addr_offset = 0; // Relative to code base (after checksum_size). - parray, 3> unused2; -} __packed__; -using S_ExecuteCode_Footer_GC_B2 = S_ExecuteCode_FooterT_B2; -using S_ExecuteCode_Footer_DC_PC_XB_BB_B2 = S_ExecuteCode_FooterT_B2; -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 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 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 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 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 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 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 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 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 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 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 unknown_a3; } __packed_ws__(G_OlgaFlowPhase2BossActions_6xA5, 8); @@ -5637,7 +5555,7 @@ struct G_ModifyTradeProposal_6xA6 { template 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) diff --git a/src/CommonFileFormats.hh b/src/CommonFileFormats.hh new file mode 100644 index 00000000..18c71b14 --- /dev/null +++ b/src/CommonFileFormats.hh @@ -0,0 +1,136 @@ +#pragma once + +#include +#include + +#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 +struct ArrayRefT { + static constexpr bool IsBE = BE; + /* 00 */ U32T count; + /* 04 */ U32T offset; + /* 08 */ +} __packed__; +using ArrayRef = ArrayRefT; +using ArrayRefBE = ArrayRefT; +check_struct_size(ArrayRef, 8); +check_struct_size(ArrayRefBE, 8); + +template +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 relocations_offset = 0; + U32T num_relocations = 0; + parray, 2> unused1; + U32T root_offset = 0; + parray, 3> unused2; +} __attribute__((packed)); +using RELFileFooter = RELFileFooterT; +using RELFileFooterBE = RELFileFooterT; +check_struct_size(RELFileFooter, 0x20); +check_struct_size(RELFileFooterBE, 0x20); diff --git a/src/EnemyType.cc b/src/EnemyType.cc index c9aafc84..8bacadd0 100644 --- a/src/EnemyType.cc +++ b/src/EnemyType.cc @@ -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; + } +} diff --git a/src/EnemyType.hh b/src/EnemyType.hh index 72f58ffe..0f5fcc7d 100644 --- a/src/EnemyType.hh +++ b/src/EnemyType.hh @@ -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& 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); diff --git a/src/Episode3/DataIndexes.cc b/src/Episode3/DataIndexes.cc index 641fa19b..7623d8b8 100644 --- a/src/Episode3/DataIndexes.cc +++ b/src/Episode3/DataIndexes.cc @@ -8,6 +8,7 @@ #include #include +#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(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(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(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(defs[x].type) < 0) { + if (static_cast(def.type) < 0) { continue; } - auto entry = make_shared(CardEntry{defs[x], "", "", "", {}}); + auto entry = make_shared(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(defs[x].type) < 0) { continue; } diff --git a/src/Episode3/DataIndexes.hh b/src/Episode3/DataIndexes.hh index 19d72fef..65b885c4 100644 --- a/src/Episode3/DataIndexes.hh +++ b/src/Episode3/DataIndexes.hh @@ -13,6 +13,7 @@ #include #include +#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 unknown_a2; /* 18 */ parray relocations; - /* 38 */ be_uint32_t relocations_offset; - /* 3C */ be_uint32_t num_relocations; - /* 40 */ parray unused1; - /* 48 */ be_uint32_t footer_offset; - /* 4C */ parray unused2; + /* 38 */ RELFileFooterBE rel_footer; /* 58 */ } __packed_ws__(CardDefinitionsFooter, 0x58); diff --git a/src/Episode3/Server.cc b/src/Episode3/Server.cc index f8e1f7eb..aee699c1 100644 --- a/src/Episode3/Server.cc +++ b/src/Episode3/Server.cc @@ -2335,7 +2335,7 @@ void Server::handle_CAx1D_start_battle(shared_ptr, 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); } } diff --git a/src/FunctionCompiler.cc b/src/FunctionCompiler.cc index a7d5e273..99d2ea95 100644 --- a/src/FunctionCompiler.cc +++ b/src/FunctionCompiler.cc @@ -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 +template string CompiledFunctionCode::generate_client_command_t( const unordered_map& label_writes, const void* suffix_data, size_t suffix_size, uint32_t override_relocations_offset) const { + using FooterT = RELFileFooterT; + 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( + return this->generate_client_command_t( 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( + return this->generate_client_command_t( label_writes, suffix_data, suffix_size, override_relocations_offset); } else { throw logic_error("invalid architecture"); diff --git a/src/FunctionCompiler.hh b/src/FunctionCompiler.hh index 3b17e5ff..13bb2ffb 100644 --- a/src/FunctionCompiler.hh +++ b/src/FunctionCompiler.hh @@ -40,7 +40,7 @@ struct CompiledFunctionCode { bool is_big_endian() const; - template + template std::string generate_client_command_t( const std::unordered_map& label_writes, const void* suffix_data = nullptr, diff --git a/src/HTTPServer.cc b/src/HTTPServer.cc index 051e866f..bb1c2e6a 100644 --- a/src/HTTPServer.cc +++ b/src/HTTPServer.cc @@ -550,8 +550,8 @@ phosg::JSON HTTPServer::generate_game_client_json_st(shared_ptr 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_ptrlanguage())}, {"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 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 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 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(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)}, diff --git a/src/ItemParameterTable.cc b/src/ItemParameterTable.cc index 72ec0b55..9b054e26 100644 --- a/src/ItemParameterTable.cc +++ b/src/ItemParameterTable.cc @@ -1,5 +1,7 @@ #include "ItemParameterTable.hh" +#include "CommonFileFormats.hh" + using namespace std; ItemParameterTable::ItemParameterTable(shared_ptr data, Version version) @@ -416,15 +418,12 @@ ItemParameterTable::ToolV4 ItemParameterTable::ToolV3T::to_v4() const { template size_t indirect_lookup_2d_count(const phosg::StringReader& r, size_t root_offset, size_t co_index) { - using ArrayRefT = typename std::conditional_t; - return r.pget(root_offset + sizeof(ArrayRefT) * co_index).count; + return r.pget>(root_offset + sizeof(ArrayRefT) * co_index).count; } template 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; - - const auto& co = r.pget(root_offset + sizeof(ArrayRefT) * co_index); + const auto& co = r.pget>(root_offset + sizeof(ArrayRefT) * co_index); if (item_index >= co.count) { throw out_of_range("item ID out of range"); } diff --git a/src/ItemParameterTable.hh b/src/ItemParameterTable.hh index 5570ee53..d273dad5 100644 --- a/src/ItemParameterTable.hh +++ b/src/ItemParameterTable.hh @@ -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 - struct ArrayRefT { - /* 00 */ U32T count; - /* 04 */ U32T offset; - /* 08 */ - } __packed__; - - using ArrayRef = ArrayRefT; - using ArrayRefBE = ArrayRefT; - check_struct_size(ArrayRef, 8); - check_struct_size(ArrayRefBE, 8); - template struct ItemBaseV2T { // id specifies several things; notably, it doubles as the index of the diff --git a/src/Items.cc b/src/Items.cc index 3b07859c..498b466e 100644 --- a/src/Items.cc +++ b/src/Items.cc @@ -189,9 +189,14 @@ void player_use_item(shared_ptr c, size_t item_index, shared_ptrrequire_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; } diff --git a/src/LevelTable.cc b/src/LevelTable.cc index e36c2b6c..d075ed1b 100644 --- a/src/LevelTable.cc +++ b/src/LevelTable.cc @@ -4,6 +4,7 @@ #include +#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(r.pget_u32l(r.size() - 0x10)); + const auto& footer = r.pget(r.size() - sizeof(RELFileFooter)); + const auto& offsets = r.pget(footer.root_offset); const auto& level_deltas_offsets = r.pget>(offsets.level_deltas); const auto& base_stats_offsets = r.pget>(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>(r.pget_u32b(r.pget_u32b(r.size() - 0x10))); + const auto& footer = r.pget(r.size() - sizeof(RELFileFooterBE)); + const auto& offsets = r.pget>(r.pget_u32b(footer.root_offset)); for (size_t char_class = 0; char_class < 12; char_class++) { const auto& src_deltas = r.pget>(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(r.pget_u32l(r.size() - 0x10)); + const auto& footer = r.pget(r.size() - sizeof(RELFileFooter)); + const auto& offsets = r.pget(footer.root_offset); const auto& level_deltas_offsets = r.pget>(offsets.level_deltas); const auto& base_stats_offsets = r.pget>(offsets.base_stats); for (size_t char_class = 0; char_class < 12; char_class++) { diff --git a/src/Lobby.cc b/src/Lobby.cc index 73312054..cd1a1b9c 100644 --- a/src/Lobby.cc +++ b/src/Lobby.cc @@ -27,12 +27,18 @@ shared_ptr 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 from_obj, + shared_ptr from_ene, + uint16_t flags) { auto fi = make_shared(); 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 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::FloorItemManager::remove(uint32_t item_id, uint8_t client_id) { @@ -71,7 +77,7 @@ std::shared_ptr 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 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::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 rare_item_set; shared_ptr 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( 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 Lobby::load_maps( - Version version, - Episode episode, - uint8_t difficulty, - uint8_t event, - uint32_t lobby_id, - shared_ptr rare_rates, - uint32_t random_seed, - shared_ptr opt_rand_crypt, - shared_ptr quest_dat_contents_decompressed) { - auto map = make_shared(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 Lobby::load_maps( - Version version, - Episode episode, - GameMode mode, - uint8_t difficulty, - uint8_t event, - uint32_t lobby_id, - shared_ptr sdt, - function(Version, const string&)> get_file_data, - shared_ptr rare_rates, - uint32_t random_seed, - shared_ptr opt_rand_crypt, - const parray& 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 Lobby::load_maps( - const vector& enemy_filenames, - const vector& object_filenames, - const vector& event_filenames, - Version version, - Episode episode, - GameMode mode, - uint8_t difficulty, - uint8_t event, - uint32_t lobby_id, - function(Version, const string&)> get_file_data, - shared_ptr rare_rates, - uint32_t rare_seed, - shared_ptr opt_rand_crypt, - const phosg::PrefixedLogger* log) { - auto map = make_shared(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(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(); + } else if (this->quest) { + this->map_state = make_shared( + 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(this->base_version, this->lobby_id, this->random_seed, this->opt_rand_crypt); + auto s = this->require_server_state(); + this->map_state = make_shared( + 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 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::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 from_obj, + std::shared_ptr 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); } diff --git a/src/Lobby.hh b/src/Lobby.hh index 9d5e3c5f..7c69cd14 100644 --- a/src/Lobby.hh +++ b/src/Lobby.hh @@ -26,9 +26,11 @@ struct ServerState; struct Lobby : public std::enable_shared_from_this { 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 from_obj; + std::shared_ptr 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 { bool exists(uint32_t item_id) const; std::shared_ptr 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 from_obj, + std::shared_ptr from_ene, + uint16_t flags); void add(std::shared_ptr fi); std::shared_ptr remove(uint32_t item_id, uint8_t client_id); std::unordered_set> evict(); @@ -65,6 +72,7 @@ struct Lobby : public std::enable_shared_from_this { // 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 { std::array next_item_id_for_client; uint32_t next_game_item_id; std::vector floor_item_managers; - std::shared_ptr rare_enemy_rates; - std::shared_ptr map; - parray variations; + std::shared_ptr rare_enemy_rates; + std::shared_ptr map_state; // Always null for lobbies, never null for games + Variations variations; std::unique_ptr quest_flags_known; // If null, ALL quest flags are known std::unique_ptr quest_flag_values; std::unique_ptr 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 { std::shared_ptr opt_rand_crypt; uint8_t allowed_drop_modes; DropMode drop_mode; - std::shared_ptr item_creator; + std::shared_ptr 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 { std::shared_ptr require_server_state() const; std::shared_ptr 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 load_maps( - Version version, - Episode episode, - uint8_t difficulty, - uint8_t event, - uint32_t lobby_id, - std::shared_ptr rare_rates, - uint32_t random_seed, - std::shared_ptr opt_rand_crypt, - std::shared_ptr quest_dat_contents_decompressed); - static std::shared_ptr load_maps( - Version version, - Episode episode, - GameMode mode, - uint8_t difficulty, - uint8_t event, - uint32_t lobby_id, - std::shared_ptr sdt, - std::function(Version, const std::string&)> get_file_data, - std::shared_ptr rare_rates, - uint32_t random_seed, - std::shared_ptr opt_rand_crypt, - const parray& variations, - const phosg::PrefixedLogger* log = nullptr); - static std::shared_ptr load_maps( - const std::vector& enemy_filenames, - const std::vector& object_filenames, - const std::vector& event_filenames, - Version version, - Episode episode, - GameMode mode, - uint8_t difficulty, - uint8_t event, - uint32_t lobby_id, - std::function(Version, const std::string&)> get_file_data, - std::shared_ptr rare_rates, - uint32_t random_seed, - std::shared_ptr 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 { inline void allow_version(Version v) { this->allowed_versions |= (1 << static_cast(v)); } + inline void forbid_version(Version v) { + this->allowed_versions &= ~(1 << static_cast(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 { bool item_exists(uint8_t floor, uint32_t item_id) const; std::shared_ptr 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 from_obj, + std::shared_ptr from_ene, + uint16_t flags); void add_item(uint8_t floor, std::shared_ptr); void evict_items_from_floor(uint8_t floor); std::shared_ptr remove_item(uint8_t floor, uint32_t item_id, uint8_t requesting_client_id); diff --git a/src/Main.cc b/src/Main.cc index 26e48ba7..64a1f23f 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -1407,7 +1407,8 @@ Action a_encode_qst( pvr_data = make_shared(phosg::load_file(pvr_filename)); } catch (const phosg::cannot_open_file&) { } - auto vq = make_shared(0, 0, version, 0, bin_data, dat_data, pvr_data); + + auto vq = make_shared(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(read_input_data(args)); if (!args.get("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(read_input_data(args)); if (args.get("compressed")) { - data = prs_decompress(data); + *data = prs_decompress(*data); } + uint8_t floor = args.get("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(read_input_data(args)); shared_ptr rs; if (phosg::ends_with(input_filename, ".json")) { - rs = make_shared(phosg::JSON::parse(*data), s->item_name_index(version)); + rs = make_shared(phosg::JSON::parse(*data), s->item_name_index_opt(version)); } else if (phosg::ends_with(input_filename, ".gsl")) { rs = make_shared(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(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(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(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(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("threads", 0); - size_t min_count = args.get("min-count", 1); - string quest_name = args.get("quest", false); - - auto s = make_shared(get_config_filename(args)); - shared_ptr 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 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(seed); - parray variations; - - shared_ptr 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 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(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("disassemble"); + auto s = make_shared(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(v_s); - if (is_ep3(v)) { - continue; - } - const array 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 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(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((it.first >> 28) & 7); + auto mode = static_cast((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 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 episodes = {Episode::EP1, Episode::EP2, Episode::EP4}; + static const array modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO}; + + Episode episode = episodes[phosg::random_object() % episodes.size()]; + GameMode mode = modes[phosg::random_object() % modes.size()]; + uint8_t difficulty = phosg::random_object() % 4; + uint8_t event = phosg::random_object() % 8; + uint32_t random_seed = phosg::random_object(); + 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( + 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("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( + 0, + phosg::random_object() & 3, + 0, + phosg::random_object(), + 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()); } }); diff --git a/src/Map.cc b/src/Map.cc index 0d8d0579..bdff10b1 100644 --- a/src/Map.cc +++ b/src/Map.cc @@ -1,9 +1,13 @@ #include "Map.hh" +#include + #include +#include #include #include +#include "CommonFileFormats.hh" #include "ItemCreator.hh" #include "Loggers.hh" #include "PSOEncryption.hh" @@ -12,1959 +16,56 @@ using namespace std; -static constexpr float UINT32_MAX_AS_FLOAT = 4294967296.0f; +//////////////////////////////////////////////////////////////////////////////// +// Set data table (variations index) -static uint64_t section_index_key(uint8_t floor, uint16_t section, uint16_t wave_number) { - return (static_cast(floor) << 32) | (static_cast(section) << 16) | static_cast(wave_number); -} - -const char* Map::name_for_object_type(uint16_t type) { - switch (type) { - case 0x0000: - return "TObjPlayerSet"; - case 0x0001: - return "TObjParticle"; - case 0x0002: - return "TObjAreaWarpForest"; - case 0x0003: - return "TObjMapWarpForest"; - case 0x0004: - return "TObjLight"; - case 0x0006: - return "TObjEnvSound"; - case 0x0007: - return "TObjFogCollision"; - case 0x0008: - return "TObjEvtCollision"; - case 0x0009: - return "TObjCollision"; - case 0x000A: - return "TOMineIcon01"; - case 0x000B: - return "TOMineIcon02"; - case 0x000C: - return "TOMineIcon03"; - case 0x000D: - return "TOMineIcon04"; - case 0x000E: - return "TObjRoomId"; - case 0x000F: - return "TOSensorGeneral01"; - case 0x0011: - return "TEF_LensFlare"; - case 0x0012: - return "TObjQuestCol"; - case 0x0013: - return "TOHealGeneral"; - case 0x0014: - return "TObjMapCsn"; - case 0x0015: - return "TObjQuestColA"; - case 0x0016: - return "TObjItemLight"; - case 0x0017: - return "TObjRaderCol"; - case 0x0018: - return "TObjFogCollisionSwitch"; - case 0x0019: - return "TObjWarpBossMulti(off)/TObjWarpBoss(on)"; - case 0x001A: - return "TObjSinBoard"; - case 0x001B: - return "TObjAreaWarpQuest"; - case 0x001C: - return "TObjAreaWarpEnding"; - case 0x001D: - return "__UNNAMED_001D__"; - case 0x001E: - return "__UNNAMED_001E__"; - case 0x001F: - return "TObjRaderHideCol"; - case 0x0020: - return "TOSwitchItem"; - case 0x0021: - return "TOSymbolchatColli"; - case 0x0022: - return "TOKeyCol"; - case 0x0023: - return "TOAttackableCol"; - case 0x0024: - return "TOSwitchAttack"; - case 0x0025: - return "TOSwitchTimer"; - case 0x0026: - return "TOChatSensor"; - case 0x0027: - return "TObjRaderIcon"; - case 0x0028: - return "TObjEnvSoundEx"; - case 0x0029: - return "TObjEnvSoundGlobal"; - case 0x0040: - return "TShopGenerator"; - case 0x0041: - return "TObjLuker"; - case 0x0042: - return "TObjBgmCol"; - case 0x0043: - return "TObjCityMainWarp"; - case 0x0044: - return "TObjCityAreaWarp"; - case 0x0045: - return "TObjCityMapWarp"; - case 0x0046: - return "TObjCityDoor_Shop"; - case 0x0047: - return "TObjCityDoor_Guild"; - case 0x0048: - return "TObjCityDoor_Warp"; - case 0x0049: - return "TObjCityDoor_Med"; - case 0x004A: - return "__UNNAMED_004A__"; - case 0x004B: - return "TObjCity_Season_EasterEgg"; - case 0x004C: - return "TObjCity_Season_ValentineHeart"; - case 0x004D: - return "TObjCity_Season_XmasTree"; - case 0x004E: - return "TObjCity_Season_XmasWreath"; - case 0x004F: - return "TObjCity_Season_HalloweenPumpkin"; - case 0x0050: - return "TObjCity_Season_21_21"; - case 0x0051: - return "TObjCity_Season_SonicAdv2"; - case 0x0052: - return "TObjCity_Season_Board"; - case 0x0053: - return "TObjCity_Season_FireWorkCtrl"; - case 0x0054: - return "TObjCityDoor_Lobby"; - case 0x0055: - return "TObjCityMainWarpChallenge"; - case 0x0056: - return "TODoorLabo"; - case 0x0057: - return "TObjTradeCollision"; - case 0x0080: - return "TObjDoor"; - case 0x0081: - return "TObjDoorKey"; - case 0x0082: - return "TObjLazerFenceNorm"; - case 0x0083: - return "TObjLazerFence4"; - case 0x0084: - return "TLazerFenceSw"; - case 0x0085: - return "TKomorebi"; - case 0x0086: - return "TButterfly"; - case 0x0087: - return "TMotorcycle"; - case 0x0088: - return "TObjContainerItem"; - case 0x0089: - return "TObjTank"; - case 0x008B: - return "TObjComputer"; - case 0x008C: - return "TObjContainerIdo"; - case 0x008D: - return "TOCapsuleAncient01"; - case 0x008E: - return "TOBarrierEnergy01"; - case 0x008F: - return "TObjHashi"; - case 0x0090: - return "TOKeyGenericSw"; - case 0x0091: - return "TObjContainerEnemy"; - case 0x0092: - return "TObjContainerBase"; - case 0x0093: - return "TObjContainerAbeEnemy"; - case 0x0095: - return "TObjContainerNoItem"; - case 0x0096: - return "TObjLazerFenceExtra"; - case 0x00C0: - return "TOKeyCave01"; - case 0x00C1: - return "TODoorCave01"; - case 0x00C2: - return "TODoorCave02"; - case 0x00C3: - return "TOHangceilingCave01Key/TOHangceilingCave01Normal/TOHangceilingCave01KeyQuick"; - case 0x00C4: - return "TOSignCave01"; - case 0x00C5: - return "TOSignCave02"; - case 0x00C6: - return "TOSignCave03"; - case 0x00C7: - return "TOAirconCave01"; - case 0x00C8: - return "TOAirconCave02"; - case 0x00C9: - return "TORevlightCave01"; - case 0x00CB: - return "TORainbowCave01"; - case 0x00CC: - return "TOKurage"; - case 0x00CD: - return "TODragonflyCave01"; - case 0x00CE: - return "TODoorCave03"; - case 0x00CF: - return "TOBind"; - case 0x00D0: - return "TOCakeshopCave01"; - case 0x00D1: - return "TORockCaveS01"; - case 0x00D2: - return "TORockCaveM01"; - case 0x00D3: - return "TORockCaveL01"; - case 0x00D4: - return "TORockCaveS02"; - case 0x00D5: - return "TORockCaveM02"; - case 0x00D6: - return "TORockCaveL02"; - case 0x00D7: - return "TORockCaveSS02"; - case 0x00D8: - return "TORockCaveSM02"; - case 0x00D9: - return "TORockCaveSL02"; - case 0x00DA: - return "TORockCaveS03"; - case 0x00DB: - return "TORockCaveM03"; - case 0x00DC: - return "TORockCaveL03"; - case 0x00DE: - return "TODummyKeyCave01"; - case 0x00DF: - return "TORockCaveBL01"; - case 0x00E0: - return "TORockCaveBL02"; - case 0x00E1: - return "TORockCaveBL03"; - case 0x0100: - return "TODoorMachine01"; - case 0x0101: - return "TOKeyMachine01"; - case 0x0102: - return "TODoorMachine02"; - case 0x0103: - return "TOCapsuleMachine01"; - case 0x0104: - return "TOComputerMachine01"; - case 0x0105: - return "TOMonitorMachine01"; - case 0x0106: - return "TODragonflyMachine01"; - case 0x0107: - return "TOLightMachine01"; - case 0x0108: - return "TOExplosiveMachine01"; - case 0x0109: - return "TOExplosiveMachine02"; - case 0x010A: - return "TOExplosiveMachine03"; - case 0x010B: - return "TOSparkMachine01"; - case 0x010C: - return "TOHangerMachine01"; - case 0x0130: - return "TODoorVoShip"; - case 0x0140: - return "TObjGoalWarpAncient"; - case 0x0141: - return "TObjMapWarpAncient"; - case 0x0142: - return "TOKeyAncient02"; - case 0x0143: - return "TOKeyAncient03"; - case 0x0144: - return "TODoorAncient01"; - case 0x0145: - return "TODoorAncient03"; - case 0x0146: - return "TODoorAncient04"; - case 0x0147: - return "TODoorAncient05"; - case 0x0148: - return "TODoorAncient06"; - case 0x0149: - return "TODoorAncient07"; - case 0x014A: - return "TODoorAncient08"; - case 0x014B: - return "TODoorAncient09"; - case 0x014C: - return "TOSensorAncient01"; - case 0x014D: - return "TOKeyAncient01"; - case 0x014E: - return "TOFenceAncient01"; - case 0x014F: - return "TOFenceAncient02"; - case 0x0150: - return "TOFenceAncient03"; - case 0x0151: - return "TOFenceAncient04"; - case 0x0152: - return "TContainerAncient01"; - case 0x0153: - return "TOTrapAncient01"; - case 0x0154: - return "TOTrapAncient02"; - case 0x0155: - return "TOMonumentAncient01"; - case 0x0156: - return "TOMonumentAncient02"; - case 0x0159: - return "TOWreckAncient01"; - case 0x015A: - return "TOWreckAncient02"; - case 0x015B: - return "TOWreckAncient03"; - case 0x015C: - return "TOWreckAncient04"; - case 0x015D: - return "TOWreckAncient05"; - case 0x015E: - return "TOWreckAncient06"; - case 0x015F: - return "TOWreckAncient07"; - case 0x0160: - return "TObjFogCollisionPoison/TObjWarpBoss03"; - case 0x0161: - return "TOContainerAncientItemCommon"; - case 0x0162: - return "TOContainerAncientItemRare"; - case 0x0163: - return "TOContainerAncientEnemyCommon"; - case 0x0164: - return "TOContainerAncientEnemyRare"; - case 0x0165: - return "TOContainerAncientItemNone"; - case 0x0166: - return "TOWreckAncientBrakable05"; - case 0x0167: - return "TOTrapAncient02R"; - case 0x0170: - return "TOBoss4Bird"; - case 0x0171: - return "TOBoss4Tower"; - case 0x0172: - return "TOBoss4Rock"; - case 0x0180: - return "TObjInfoCol"; - case 0x0181: - return "TObjWarpLobby"; - case 0x0182: - return "TObjLobbyMain"; - case 0x0183: - return "__TObjPathObj_subclass_0183__"; - case 0x0184: - return "TObjButterflyLobby"; - case 0x0185: - return "TObjRainbowLobby"; - case 0x0186: - return "TObjKabochaLobby"; - case 0x0187: - return "TObjStendGlassLobby"; - case 0x0188: - return "TObjCurtainLobby"; - case 0x0189: - return "TObjWeddingLobby"; - case 0x018A: - return "TObjTreeLobby"; - case 0x018B: - return "TObjSuisouLobby"; - case 0x018C: - return "TObjParticleLobby"; - case 0x0190: - return "TObjCamera"; - case 0x0191: - return "TObjTuitate"; - case 0x0192: - return "TObjDoaEx01"; - case 0x0193: - return "TObjBigTuitate"; - case 0x01A0: - return "TODoorVS2Door01"; - case 0x01A1: - return "TOVS2Wreck01"; - case 0x01A2: - return "TOVS2Wreck02"; - case 0x01A3: - return "TOVS2Wreck03"; - case 0x01A4: - return "TOVS2Wreck04"; - case 0x01A5: - return "TOVS2Wreck05"; - case 0x01A6: - return "TOVS2Wreck06"; - case 0x01A7: - return "TOVS2Wall01"; - case 0x01A8: - return "__UNNAMED_01A8__"; - case 0x01A9: - return "TObjHashiVersus1"; - case 0x01AA: - return "TObjHashiVersus2"; - case 0x01AB: - return "TODoorFourLightRuins"; - case 0x01C0: - return "TODoorFourLightSpace"; - case 0x0200: - return "TObjContainerJung"; - case 0x0201: - return "TObjWarpJung"; - case 0x0202: - return "TObjDoorJung"; - case 0x0203: - return "TObjContainerJungEx"; - case 0x0204: - return "TODoorJungleMain"; - case 0x0205: - return "TOKeyJungleMain"; - case 0x0206: - return "TORockJungleS01"; - case 0x0207: - return "TORockJungleM01"; - case 0x0208: - return "TORockJungleL01"; - case 0x0209: - return "TOGrassJungle"; - case 0x020A: - return "TObjWarpJungMain"; - case 0x020B: - return "TBGLightningCtrl"; - case 0x020C: - return "__TObjPathObj_subclass_020C__"; - case 0x020D: - return "__TObjPathObj_subclass_020D__"; - case 0x020E: - return "TObjContainerJungEnemy"; - case 0x020F: - return "TOTrapChainSawDamage"; - case 0x0210: - return "TOTrapChainSawKey"; - case 0x0211: - return "TOBiwaMushi"; - case 0x0212: - return "__TObjPathObj_subclass_0212__"; - case 0x0213: - return "TOJungleDesign"; - case 0x0220: - return "TObjFish"; - case 0x0221: - return "TODoorFourLightSeabed"; - case 0x0222: - return "TODoorFourLightSeabedU"; - case 0x0223: - return "TObjSeabedSuiso_CH"; - case 0x0224: - return "TObjSeabedSuisoBrakable"; - case 0x0225: - return "TOMekaFish00"; - case 0x0226: - return "TOMekaFish01"; - case 0x0227: - return "__TObjPathObj_subclass_0227__"; - case 0x0228: - return "TOTrapSeabed01"; - case 0x0229: - return "TOCapsuleLabo"; - case 0x0240: - return "TObjParticle"; - case 0x0280: - return "__TObjAreaWarpForest_subclass_0280__"; - case 0x02A0: - return "TObjLiveCamera"; - case 0x02B0: - return "TContainerAncient01R"; - case 0x02B1: - return "TObjLaboDesignBase"; - case 0x02B2: - return "TObjLaboDesignBase"; - case 0x02B3: - return "TObjLaboDesignBase"; - case 0x02B4: - return "TObjLaboDesignBase"; - case 0x02B5: - return "TObjLaboDesignBase"; - case 0x02B6: - return "TObjLaboDesignBase"; - case 0x02B7: - return "TObjGbAdvance"; - case 0x02B8: - return "TObjQuestColALock2"; - case 0x02B9: - return "TObjMapForceWarp"; - case 0x02BA: - return "TObjQuestCol2"; - case 0x02BB: - return "TODoorLaboNormal"; - case 0x02BC: - return "TObjAreaWarpEndingJung"; - case 0x02BD: - return "TObjLaboMapWarp"; - case 0x0300: - return "__EP4_LIGHT__"; - case 0x0301: - return "__WILDS_CRATER_CACTUS__"; - case 0x0302: - return "__WILDS_CRATER_BROWN_ROCK__"; - case 0x0303: - return "__WILDS_CRATER_BROWN_ROCK_DESTRUCTIBLE__"; - case 0x0340: - return "__UNKNOWN_0340__"; - case 0x0341: - return "__UNKNOWN_0341__"; - case 0x0380: - return "__POISON_PLANT__"; - case 0x0381: - return "__UNKNOWN_0381__"; - case 0x0382: - return "__UNKNOWN_0382__"; - case 0x0383: - return "__DESERT_OOZE_PLANT__"; - case 0x0385: - return "__UNKNOWN_0385__"; - case 0x0386: - return "__WILDS_CRATER_BLACK_ROCKS__"; - case 0x0387: - return "__UNKNOWN_0387__"; - case 0x0388: - return "__UNKNOWN_0388__"; - case 0x0389: - return "__UNKNOWN_0389__"; - case 0x038A: - return "__UNKNOWN_038A__"; - case 0x038B: - return "__FALLING_ROCK__"; - case 0x038C: - return "__DESERT_PLANT_SOLID__"; - case 0x038D: - return "__DESERT_CRYSTALS_BOX__"; - case 0x038E: - return "__UNKNOWN_038E__"; - case 0x038F: - return "__BEE_HIVE__"; - case 0x0390: - return "__UNKNOWN_0390__"; - case 0x0391: - return "__HEAT__"; - case 0x03C0: - return "__EP4_BOSS_EGG__"; - case 0x03C1: - return "__UNKNOWN_03C1__"; - default: - return "__UNKNOWN__"; - } -} - -Map::RareEnemyRates::RareEnemyRates(uint32_t enemy_rate, uint32_t boss_rate) - : hildeblue(enemy_rate), - rappy(enemy_rate), - nar_lily(enemy_rate), - pouilly_slime(enemy_rate), - merissa_aa(enemy_rate), - pazuzu(enemy_rate), - dorphon_eclair(enemy_rate), - kondrieu(boss_rate) {} - -Map::RareEnemyRates::RareEnemyRates(const phosg::JSON& json) - : hildeblue(json.get_int("Hildeblue")), - rappy(json.get_int("Rappy")), - nar_lily(json.get_int("NarLily")), - pouilly_slime(json.get_int("PouillySlime")), - merissa_aa(json.get_int("MerissaAA")), - pazuzu(json.get_int("Pazuzu")), - dorphon_eclair(json.get_int("DorphonEclair")), - kondrieu(json.get_int("Kondrieu")) {} - -phosg::JSON Map::RareEnemyRates::json() const { - return phosg::JSON::dict({ - {"Hildeblue", this->hildeblue}, - {"Rappy", this->rappy}, - {"NarLily", this->nar_lily}, - {"PouillySlime", this->pouilly_slime}, - {"MerissaAA", this->merissa_aa}, - {"Pazuzu", this->pazuzu}, - {"DorphonEclair", this->dorphon_eclair}, - {"Kondrieu", this->kondrieu}, - }); -} - -string Map::ObjectEntry::str() const { - string name_str = Map::name_for_object_type(this->base_type); - return phosg::string_printf("[ObjectEntry type=%04hX \"%s\" flags=%04hX index=%04hX a2=%04hX entity_id=%04hX group=%04hX section=%04hX a3=%04hX x=%g y=%g z=%g x_angle=%08" PRIX32 " y_angle=%08" PRIX32 " z_angle=%08" PRIX32 " params=[%g %g %g %08" PRIX32 " %08" PRIX32 " %08" PRIX32 "] unused=%08" PRIX32 "]", - this->base_type.load(), - name_str.c_str(), - this->flags.load(), - this->index.load(), - this->unknown_a2.load(), - this->entity_id.load(), - this->group.load(), - this->section.load(), - this->unknown_a3.load(), - this->x.load(), - this->y.load(), - this->z.load(), - this->x_angle.load(), - this->y_angle.load(), - this->z_angle.load(), - this->param1.load(), - this->param2.load(), - this->param3.load(), - this->param4.load(), - this->param5.load(), - this->param6.load(), - this->unused.load()); -} - -string Map::EnemyEntry::str() const { - return phosg::string_printf("[EnemyEntry type=%04hX flags=%04hX index=%04hX num_children=%04hX floor=%04hX entity_id=%04hX section=%04hX wave_number=%04hX wave_number2=%04hX a1=%04hX x=%g y=%g z=%g x_angle=%08" PRIX32 " y_angle=%08" PRIX32 " z_angle=%08" PRIX32 " params=[%g %g %g %g %g %04hX %04hX] unused=%08" PRIX32 "]", - this->base_type.load(), - this->flags.load(), - this->index.load(), - this->num_children.load(), - this->floor.load(), - this->entity_id.load(), - this->section.load(), - this->wave_number.load(), - this->wave_number2.load(), - this->unknown_a1.load(), - this->x.load(), - this->y.load(), - this->z.load(), - this->x_angle.load(), - this->y_angle.load(), - this->z_angle.load(), - this->fparam1.load(), - this->fparam2.load(), - this->fparam3.load(), - this->fparam4.load(), - this->fparam5.load(), - this->uparam1.load(), - this->uparam2.load(), - this->unused.load()); -} - -Map::Enemy::Enemy( - uint16_t enemy_id, - size_t source_index, - size_t set_index, - uint8_t floor, - uint16_t section, - uint16_t wave_number, - EnemyType type, - uint16_t alias_entity_id) - : source_index(source_index), - set_index(set_index), - enemy_id(enemy_id), - total_damage(0), - game_flags(0), - section(section), - wave_number(wave_number), - type(type), - floor(floor), - server_flags(0), - alias_entity_id(alias_entity_id) {} - -string Map::Enemy::str() const { - return phosg::string_printf("[Map::Enemy E-%hX source %zX %s%s floor=%02hhX section=%04hX wave_number=%04hX server_flags=%02hhX]", - this->enemy_id, - this->source_index, - phosg::name_for_enum(this->type), - enemy_type_is_rare(this->type) ? " RARE" : "", - this->floor, - this->section, - this->wave_number, - this->server_flags); -} - -string Map::Event::str() const { - return phosg::string_printf("[Map::Event W-%02hhX-%" PRIX32 " flags=%04hX floor=%02hhX action_stream_offset=%" PRIX32 "]", - this->floor, - this->event_id, - this->flags, - this->floor, - this->action_stream_offset); -} - -string Map::Object::str() const { - return phosg::string_printf("[Map::Object floor %02hhX source %zX %04hX(%s) @%04hX:(%g %g %g)/(%04" PRIX32 " %04" PRIX32 " %04" PRIX32 ") +%04hX params [%g %g %g %08" PRIX32 " %08" PRIX32 " %08" PRIX32 "]]", - this->floor, - this->source_index, - this->args->base_type.load(), - Map::name_for_object_type(this->args->base_type), - this->args->section.load(), - this->args->x.load(), - this->args->y.load(), - this->args->z.load(), - this->args->x_angle.load(), - this->args->y_angle.load(), - this->args->z_angle.load(), - this->args->group.load(), - this->args->param1.load(), - this->args->param2.load(), - this->args->param3.load(), - this->args->param4.load(), - this->args->param5.load(), - this->args->param6.load()); -} - -Map::Map(Version version, uint32_t lobby_id, uint32_t rare_seed, std::shared_ptr opt_rand_crypt) - : log(phosg::string_printf("[Lobby:%08" PRIX32 ":map] ", lobby_id), lobby_log.min_level), - version(version), - rare_seed(rare_seed), - opt_rand_crypt(opt_rand_crypt) {} - -void Map::clear() { - this->objects.clear(); - this->enemies.clear(); - this->rare_enemy_indexes.clear(); -} - -void Map::add_objects_from_owned_map_data(uint8_t floor, const void* data, size_t size) { - size_t entry_count = size / sizeof(ObjectEntry); - if (size != entry_count * sizeof(ObjectEntry)) { - throw runtime_error("data size is not a multiple of entry size"); - } - - const auto* objects = reinterpret_cast(data); - for (size_t z = 0; z < entry_count; z++) { - uint16_t object_id = this->objects.size(); - const auto& object = this->objects.emplace_back(Object{ - .args = &objects[z], - .source_index = z, - .floor = floor, - .object_id = object_id, - .game_flags = 0, - .set_flags = 0, - .item_drop_checked = false, - }); - - uint64_t k = section_index_key(floor, objects[z].section, objects[z].group); - this->floor_section_and_group_to_object_index.emplace(k, object_id); - - uint32_t base_switch_flag = 0; - uint32_t num_switch_flags = 0; - switch (object.args->base_type) { - case 0x00C1: // TODoorCave01 - base_switch_flag = object.args->param4; - num_switch_flags = (4 - clamp(object.args->param5, 0, 4)); - break; - - case 0x14A: // TODoorAncient08 - case 0x14B: // TODoorAncient09 - base_switch_flag = object.args->param4; - num_switch_flags = (object.args->base_type == 0x14A) ? 4 : 2; - break; - - case 0x1AB: // TODoorFourLightRuins - case 0x1C0: // TODoorFourLightSpace - case 0x221: // TODoorFourLightSeabed - case 0x222: // TODoorFourLightSeabedU - base_switch_flag = object.args->param4; - num_switch_flags = object.args->param5; - break; +string Variations::str() const { + string ret = ""; + for (size_t z = 0; z < this->entries.size(); z++) { + const auto& e = this->entries[z]; + if (!ret.empty()) { + ret += ","; } - if ((num_switch_flags > 1) && !(base_switch_flag & 0xFFFFFF00)) { - for (size_t z = 0; z < num_switch_flags; z++) { - this->floor_and_switch_flag_to_door_index.emplace((floor << 8) | (base_switch_flag + z), object_id); - } - } - } -} - -void Map::add_objects_from_map_data(uint8_t floor, std::shared_ptr data) { - this->link_owned_data(data); - this->add_objects_from_owned_map_data(floor, data->data(), data->size()); -} - -bool Map::check_and_log_rare_enemy(bool default_is_rare, uint32_t rare_rate) { - if (default_is_rare) { - return true; - } - - // On BB, rare enemy indexes are generated by the server and sent to the - // client, so we can use any method we want to choose rares. On other - // versions, we must match the client's logic, even though it's more - // computationally expensive. - if (this->version == Version::BB_V4) { - if ((this->rare_enemy_indexes.size() < 0x10) && (random_from_optional_crypt(this->opt_rand_crypt) < rare_rate)) { - this->rare_enemy_indexes.emplace_back(this->enemies.size()); - return true; - } - - } else { - // TODO: We only need the first value from this crypt, so it's unfortunate - // that we have to initialize the entire thing. Find a way to make this - // faster. - PSOV2Encryption crypt(this->rare_seed + 0x1000 + this->enemies.size()); - float det = (static_cast((crypt.next() >> 16) & 0xFFFF) / 65536.0f); - // On v1 and v2 (and GC NTE), the rare rate is 0.1% instead of 0.2%. - float threshold = is_v1_or_v2(this->version) ? 0.001f : 0.002f; - if (det < threshold) { - this->rare_enemy_indexes.emplace_back(this->enemies.size()); - return true; - } - } - - return false; -} - -void Map::add_enemy( - Episode episode, - uint8_t difficulty, - uint8_t event, - uint8_t floor, - size_t source_index, - const EnemyEntry& e, - std::shared_ptr rare_rates) { - size_t set_index = this->enemy_set_flags.size(); - this->enemy_set_flags.emplace_back(0); - - auto add = [&](EnemyType type, uint16_t alias_enemy_id = 0xFFFF) -> void { - uint16_t enemy_id = this->enemies.size(); - this->enemies.emplace_back(enemy_id, source_index, set_index, floor, e.section, e.wave_number, type, alias_enemy_id); - uint64_t k = section_index_key(floor, e.section, e.wave_number); - this->floor_section_and_wave_number_to_enemy_index.emplace(k, enemy_id); - }; - - EnemyType child_type = EnemyType::UNKNOWN; - ssize_t default_num_children = 0; - switch (e.base_type) { - case 0x0001: // TObjNpcFemaleBase - case 0x0002: // TObjNpcFemaleChild - case 0x0003: // TObjNpcFemaleDwarf - case 0x0004: // TObjNpcFemaleFat - case 0x0005: // TObjNpcFemaleMacho - case 0x0006: // TObjNpcFemaleOld - case 0x0007: // TObjNpcFemaleTall - case 0x0008: // TObjNpcMaleBase - case 0x0009: // TObjNpcMaleChild - case 0x000A: // TObjNpcMaleDwarf - case 0x000B: // TObjNpcMaleFat - case 0x000C: // TObjNpcMaleMacho - case 0x000D: // TObjNpcMaleOld - case 0x000E: // TObjNpcMaleTall - case 0x0019: // TObjNpcSoldierBase - case 0x001A: // TObjNpcSoldierMacho - case 0x001B: // TObjNpcGovernorBase - case 0x001C: // TObjNpcConnoisseur - case 0x001D: // TObjNpcCloakroomBase - case 0x001E: // TObjNpcExpertBase - case 0x001F: // TObjNpcNurseBase - case 0x0020: // TObjNpcSecretaryBase - case 0x0021: // TObjNpcHHM00 - case 0x0022: // TObjNpcNHW00 - case 0x0024: // TObjNpcHRM00 - case 0x0025: // TObjNpcARM00 - case 0x0026: // TObjNpcARW00 - case 0x0027: // TObjNpcHFW00 - case 0x0028: // TObjNpcNFM00 - case 0x0029: // TObjNpcNFW00 - case 0x002B: // TObjNpcNHW01 - case 0x002C: // TObjNpcAHM01 - case 0x002D: // TObjNpcHRM01 - case 0x0030: // TObjNpcHFW01 - case 0x0031: // TObjNpcNFM01 - case 0x0032: // TObjNpcNFW01 - case 0x0033: // TObjNpcEnemy - case 0x0045: // TObjNpcLappy - case 0x0046: // TObjNpcMoja - case 0x00A9: // TObjNpcBringer - case 0x00D0: // TObjNpcKenkyu - case 0x00D1: // TObjNpcSoutokufu - case 0x00D2: // TObjNpcHosa - case 0x00D3: // TObjNpcKenkyuW - case 0x00F0: // TObjNpcHosa2 - case 0x00F1: // TObjNpcKenkyu2 - case 0x00F2: // TObjNpcNgcBase - case 0x00F3: // TObjNpcNgcBase - case 0x00F4: // TObjNpcNgcBase - case 0x00F5: // TObjNpcNgcBase - case 0x00F6: // TObjNpcNgcBase - case 0x00F7: // TObjNpcNgcBase - case 0x00F8: // TObjNpcNgcBase - case 0x00F9: // TObjNpcNgcBase - case 0x00FA: // TObjNpcNgcBase - case 0x00FB: // TObjNpcNgcBase - case 0x00FC: // TObjNpcNgcBase - case 0x00FD: // TObjNpcNgcBase - case 0x00FE: // TObjNpcNgcBase - case 0x00FF: // TObjNpcNgcBase - case 0x0100: // Unknown NPC - // All of these have a default child count of zero - add(EnemyType::NON_ENEMY_NPC); - break; - - case 0x0040: { // TObjEneMoja - bool default_is_rare = (this->version == Version::BB_V4) ? (e.uparam1 & 1) : (e.uparam1 != 0); - add(this->check_and_log_rare_enemy(default_is_rare, rare_rates->hildeblue) - ? EnemyType::HILDEBLUE - : EnemyType::HILDEBEAR); - break; - } - case 0x0041: { // TObjEneLappy - bool default_is_rare = (this->version == Version::BB_V4) ? (e.uparam1 & 1) : (e.uparam1 != 0); - bool is_rare = this->check_and_log_rare_enemy(default_is_rare, rare_rates->rappy); - switch (episode) { - case Episode::EP1: - add(is_rare ? EnemyType::AL_RAPPY : EnemyType::RAG_RAPPY); - break; - case Episode::EP2: - if (is_rare) { - switch (event) { - case 0x01: // rappy_type 1 - add(EnemyType::SAINT_RAPPY); - break; - case 0x04: // rappy_type 2 - add(EnemyType::EGG_RAPPY); - break; - case 0x05: // rappy_type 3 - add(EnemyType::HALLO_RAPPY); - break; - default: - add(EnemyType::LOVE_RAPPY); - } - } else { - add(EnemyType::RAG_RAPPY); - } - break; - case Episode::EP4: - if (e.floor > 0x05) { - add(is_rare ? EnemyType::DEL_RAPPY_ALT : EnemyType::SAND_RAPPY_ALT); - } else { - add(is_rare ? EnemyType::DEL_RAPPY : EnemyType::SAND_RAPPY); - } - break; - default: - throw logic_error("invalid episode"); - } - break; - } - case 0x0042: // TObjEneBm3FlyNest - add(EnemyType::MONEST); - child_type = EnemyType::MOTHMANT; - default_num_children = 30; - break; - case 0x0043: // TObjEneBm5Wolf - add(e.fparam2 ? EnemyType::BARBAROUS_WOLF : EnemyType::SAVAGE_WOLF); - break; - case 0x0044: { // TObjEneBeast - static const EnemyType types[3] = {EnemyType::BOOMA, EnemyType::GOBOOMA, EnemyType::GIGOBOOMA}; - add(types[e.uparam1 % 3]); - break; - } - case 0x0060: // TObjGrass - add(EnemyType::GRASS_ASSASSIN); - break; - case 0x0061: // TObjEneRe2Flower - if ((episode == Episode::EP2) && (e.floor == 0x11)) { - add(EnemyType::DEL_LILY); - } else { - add(this->check_and_log_rare_enemy(false, rare_rates->nar_lily) - ? EnemyType::NAR_LILY - : EnemyType::POISON_LILY); - } - break; - case 0x0062: // TObjEneNanoDrago - add(EnemyType::NANO_DRAGON); - break; - case 0x0063: { // TObjEneShark - static const EnemyType types[3] = {EnemyType::EVIL_SHARK, EnemyType::PAL_SHARK, EnemyType::GUIL_SHARK}; - add(types[e.uparam1 % 3]); - break; - } - case 0x0064: // TObjEneSlime - if ((e.num_children != 0) && (e.num_children != 4)) { - this->log.warning("POFUILLY_SLIME has an unusual num_children (0x%hX)", e.num_children.load()); - } - default_num_children = -1; // Skip adding children (because we do it here) - for (size_t z = 0; z < 5; z++) { - add(this->check_and_log_rare_enemy((this->version == Version::BB_V4) && (e.uparam2 & 1), rare_rates->pouilly_slime) - ? EnemyType::POUILLY_SLIME - : EnemyType::POFUILLY_SLIME); - } - break; - case 0x0065: // TObjEnePanarms - if ((e.num_children != 0) && (e.num_children != 2)) { - this->log.warning("PAN_ARMS has an unusual num_children (0x%hX)", e.num_children.load()); - } - default_num_children = -1; // Skip adding children (because we do it here) - add(EnemyType::PAN_ARMS); - add(EnemyType::HIDOOM); - add(EnemyType::MIGIUM); - break; - case 0x0080: // TObjEneDubchik - add((e.uparam1 & 0x01) ? EnemyType::GILLCHIC : EnemyType::DUBCHIC); - break; - case 0x0081: // TObjEneGyaranzo - add(EnemyType::GARANZ); - break; - case 0x0082: // TObjEneMe3ShinowaReal - add(e.fparam2 ? EnemyType::SINOW_GOLD : EnemyType::SINOW_BEAT); - default_num_children = 4; - break; - case 0x0083: // TObjEneMe1Canadin - add(EnemyType::CANADINE); - break; - case 0x0084: // TObjEneMe1CanadinLeader - add(EnemyType::CANANE); - child_type = EnemyType::CANADINE_GROUP; - default_num_children = 8; - break; - case 0x0085: // TOCtrlDubchik - add(EnemyType::DUBWITCH); - break; - case 0x00A0: // TObjEneSaver - add(EnemyType::DELSABER); - break; - case 0x00A1: // TObjEneRe4Sorcerer - if ((e.num_children != 0) && (e.num_children != 2)) { - this->log.warning("CHAOS_SORCERER has an unusual num_children (0x%hX)", e.num_children.load()); - } - default_num_children = -1; // Skip adding children (because we do it here) - add(EnemyType::CHAOS_SORCERER); - add(EnemyType::BEE_R); - add(EnemyType::BEE_L); - break; - case 0x00A2: // TObjEneDarkGunner - add(EnemyType::DARK_GUNNER); - break; - case 0x00A3: // TObjEneDarkGunCenter - add(EnemyType::DEATH_GUNNER); - break; - case 0x00A4: // TObjEneDf2Bringer - add(EnemyType::CHAOS_BRINGER); - break; - case 0x00A5: // TObjEneRe7Berura - add(EnemyType::DARK_BELRA); - break; - case 0x00A6: { // TObjEneDimedian - static const EnemyType types[3] = {EnemyType::DIMENIAN, EnemyType::LA_DIMENIAN, EnemyType::SO_DIMENIAN}; - add(types[e.uparam1 % 3]); - break; - } - case 0x00A7: // TObjEneBalClawBody - add(EnemyType::BULCLAW); - child_type = EnemyType::CLAW; - default_num_children = 4; - break; - case 0x00A8: // Unnamed subclass of TObjEneBalClawClaw - add(EnemyType::CLAW); - break; - case 0x00C0: // TBoss1Dragon or TBoss5Gryphon - if (episode == Episode::EP1) { - add(EnemyType::DRAGON); - } else if (episode == Episode::EP2) { - add(EnemyType::GAL_GRYPHON); - } else { - throw runtime_error("DRAGON placed outside of Episode 1 or 2"); - } - break; - case 0x00C1: // TBoss2DeRolLe - if ((e.num_children != 0) && (e.num_children != 0x13)) { - this->log.warning("DE_ROL_LE has an unusual num_children (0x%hX)", e.num_children.load()); - } - default_num_children = -1; // Skip adding children (because we do it here) - add(EnemyType::DE_ROL_LE); - for (size_t z = 0; z < 0x0A; z++) { - add(EnemyType::DE_ROL_LE_BODY); - } - for (size_t z = 0; z < 0x09; z++) { - add(EnemyType::DE_ROL_LE_MINE); - } - break; - case 0x00C2: // TBoss3Volopt - if ((e.num_children != 0) && (e.num_children != 0x23)) { - this->log.warning("VOL_OPT has an unusual num_children (0x%hX)", e.num_children.load()); - } - default_num_children = -1; // Skip adding children (because we do it here) - add(EnemyType::VOL_OPT_1); - for (size_t z = 0; z < 0x06; z++) { - add(EnemyType::VOL_OPT_PILLAR); - } - for (size_t z = 0; z < 0x18; z++) { - add(EnemyType::VOL_OPT_MONITOR); - } - for (size_t z = 0; z < 0x02; z++) { - add(EnemyType::NONE); - } - add(EnemyType::VOL_OPT_AMP); - add(EnemyType::VOL_OPT_CORE); - add(EnemyType::NONE); - break; - case 0x00C5: // Unnamed subclass of TObjEnemyCustom - add(EnemyType::VOL_OPT_2); - break; - case 0x00C8: { // TBoss4DarkFalz - if ((e.num_children != 0) && (e.num_children != 0x200)) { - this->log.warning("DARK_FALZ has an unusual num_children (0x%hX)", e.num_children.load()); - } - uint16_t base_enemy_id = this->enemies.size(); - if (difficulty) { - add(EnemyType::DARK_FALZ_3); - } else { - add(EnemyType::DARK_FALZ_2); - } - default_num_children = -1; // Skip adding children (because we do it here) - for (size_t x = 0; x < 0x1FD; x++) { - add(difficulty == 3 ? EnemyType::DARVANT_ULTIMATE : EnemyType::DARVANT); - } - add(EnemyType::DARK_FALZ_3, base_enemy_id); - add(EnemyType::DARK_FALZ_2, base_enemy_id); - add(EnemyType::DARK_FALZ_1, base_enemy_id); - break; - } - case 0x00CA: { // TBoss6PlotFalz - uint16_t base_enemy_id = this->enemies.size(); - add(EnemyType::OLGA_FLOW_2); - default_num_children = -1; // Skip adding children (because we do it here) - for (size_t x = 0; x < 0x200; x++) { - add(EnemyType::OLGA_FLOW_2, base_enemy_id); - } - break; - } - case 0x00CB: // TBoss7DeRolLeC - add(EnemyType::BARBA_RAY); - child_type = EnemyType::PIG_RAY; - default_num_children = 0x2F; - break; - case 0x00CC: // TBoss8Dragon - add(EnemyType::GOL_DRAGON); - default_num_children = 5; - break; - case 0x00D4: // TObjEneMe3StelthReal - add((e.uparam1 & 1) ? EnemyType::SINOW_SPIGELL : EnemyType::SINOW_BERILL); - default_num_children = 4; - break; - case 0x00D5: // TObjEneMerillLia - add((e.uparam1 & 0x01) ? EnemyType::MERILTAS : EnemyType::MERILLIA); - break; - case 0x00D6: // TObjEneBm9Mericarol - if (e.uparam1 == 0) { - add(EnemyType::MERICAROL); - } else { - add(((e.uparam1 % 3) == 2) ? EnemyType::MERICUS : EnemyType::MERIKLE); - } - break; - case 0x00D7: // TObjEneBm5GibonU - add((e.uparam1 & 0x01) ? EnemyType::ZOL_GIBBON : EnemyType::UL_GIBBON); - break; - case 0x00D8: // TObjEneGibbles - add(EnemyType::GIBBLES); - break; - case 0x00D9: // TObjEneMe1Gee - add(EnemyType::GEE); - break; - case 0x00DA: // TObjEneMe1GiGue - add(EnemyType::GI_GUE); - break; - case 0x00DB: // TObjEneDelDepth - add(EnemyType::DELDEPTH); - break; - case 0x00DC: // TObjEneDellBiter - add(EnemyType::DELBITER); - break; - case 0x00DD: // TObjEneDolmOlm - add(e.uparam1 ? EnemyType::DOLMDARL : EnemyType::DOLMOLM); - break; - case 0x00DE: // TObjEneMorfos - add(EnemyType::MORFOS); - break; - case 0x00DF: // TObjEneRecobox - add(EnemyType::RECOBOX); - child_type = EnemyType::RECON; - break; - case 0x00E0: // TObjEneMe3SinowZoaReal or TObjEneEpsilonBody - if ((episode == Episode::EP2) && (e.floor > 0x0F)) { - add(EnemyType::EPSILON); - default_num_children = 4; - child_type = EnemyType::EPSIGUARD; - } else { - add((e.uparam1 & 0x01) ? EnemyType::SINOW_ZELE : EnemyType::SINOW_ZOA); - } - break; - case 0x00E1: // TObjEneIllGill - add(EnemyType::ILL_GILL); - break; - case 0x0110: - add(EnemyType::ASTARK); - break; - case 0x0111: - if (e.floor > 0x05) { - add(e.fparam2 ? EnemyType::YOWIE_ALT : EnemyType::SATELLITE_LIZARD_ALT); - } else { - add(e.fparam2 ? EnemyType::YOWIE : EnemyType::SATELLITE_LIZARD); - } - break; - case 0x0112: - add(this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates->merissa_aa) - ? EnemyType::MERISSA_AA - : EnemyType::MERISSA_A); - break; - case 0x0113: - add(EnemyType::GIRTABLULU); - break; - case 0x0114: { - bool is_rare = this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates->pazuzu); - if (e.floor > 0x05) { - add(is_rare ? EnemyType::PAZUZU_ALT : EnemyType::ZU_ALT); - } else { - add(is_rare ? EnemyType::PAZUZU : EnemyType::ZU); - } - break; - } - case 0x0115: - if (e.uparam1 & 2) { - add(EnemyType::BA_BOOTA); - } else { - add((e.uparam1 & 1) ? EnemyType::ZE_BOOTA : EnemyType::BOOTA); - } - break; - case 0x0116: - add(this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates->dorphon_eclair) - ? EnemyType::DORPHON_ECLAIR - : EnemyType::DORPHON); - break; - case 0x0117: { - static const EnemyType types[3] = {EnemyType::GORAN, EnemyType::PYRO_GORAN, EnemyType::GORAN_DETONATOR}; - add(types[e.uparam1 % 3]); - break; - } - case 0x0119: { - bool is_rare = this->check_and_log_rare_enemy((e.fparam2 != 0.0f), rare_rates->kondrieu); - if (is_rare) { - add(EnemyType::KONDRIEU); - } else { - add((e.uparam1 & 1) ? EnemyType::SHAMBERTIN : EnemyType::SAINT_MILLION); - } - default_num_children = 0x18; - break; - } - - case 0x00C3: // TBoss3VoloptP01 - case 0x00C4: // TBoss3VoloptCore or subclass - case 0x00C6: // TBoss3VoloptMonitor - case 0x00C7: // TBoss3VoloptHiraisin - case 0x0118: - add(EnemyType::UNKNOWN); - break; - - default: - add(EnemyType::UNKNOWN); - this->log.warning( - "(Entry %zu, offset %zX in file) Invalid enemy type %04hX", - source_index, source_index * sizeof(EnemyEntry), e.base_type.load()); - break; - } - - if (default_num_children >= 0) { - size_t num_children = e.num_children ? e.num_children.load() : default_num_children; - if ((child_type == EnemyType::UNKNOWN) && !this->enemies.empty()) { - child_type = this->enemies.back().type; - } - for (size_t x = 0; x < num_children; x++) { - add(child_type); - } - } -} - -void Map::add_enemies_from_map_data( - Episode episode, - uint8_t difficulty, - uint8_t event, - uint8_t floor, - const void* data, - size_t size, - std::shared_ptr rare_rates) { - size_t entry_count = size / sizeof(EnemyEntry); - if (size != entry_count * sizeof(EnemyEntry)) { - throw runtime_error("data size is not a multiple of entry size"); - } - - phosg::StringReader r(data, size); - for (size_t y = 0; y < entry_count; y++) { - this->add_enemy(episode, difficulty, event, floor, y, r.get(), rare_rates); - } -} - -Map::DATParserRandomState::DATParserRandomState(uint32_t rare_seed) - : random(rare_seed), - location_table_random(0), - location_indexes_populated(0), - location_indexes_used(0), - location_entries_base_offset(0) { - this->location_index_table.fill(0); -} - -size_t Map::DATParserRandomState::rand_int_biased(size_t min_v, size_t max_v) { - float max_f = static_cast(max_v + 1); - uint32_t crypt_v = this->random.next(); - float det_f = static_cast(crypt_v); - return max(floorf((max_f * det_f) / UINT32_MAX_AS_FLOAT), min_v); -} - -uint32_t Map::DATParserRandomState::next_location_index() { - if (this->location_indexes_used < this->location_indexes_populated) { - return this->location_index_table.at(this->location_indexes_used++); - } - return 0; -} - -void Map::DATParserRandomState::generate_shuffled_location_table( - const Map::RandomEnemyLocationsHeader& header, phosg::StringReader r, uint16_t section) { - if (header.num_sections == 0) { - throw runtime_error("no locations defined"); - } - - phosg::StringReader sections_r = r.sub(header.section_table_offset, header.num_sections * sizeof(Map::RandomEnemyLocationSection)); - - size_t bs_min = 0; - size_t bs_max = header.num_sections - 1; - do { - size_t bs_mid = (bs_min + bs_max) / 2; - if (sections_r.pget(bs_mid * sizeof(Map::RandomEnemyLocationSection)).section < section) { - bs_min = bs_mid + 1; - } else { - bs_max = bs_mid; - } - } while (bs_min < bs_max); - - const auto& sec = sections_r.pget(bs_min * sizeof(Map::RandomEnemyLocationSection)); - if (section != sec.section) { - return; - } - - this->location_indexes_populated = sec.count; - this->location_indexes_used = 0; - this->location_entries_base_offset = sec.offset; - for (size_t z = 0; z < sec.count; z++) { - this->location_index_table.at(z) = z; - } - - for (size_t z = 0; z < 4; z++) { - for (size_t x = 0; x < sec.count; x++) { - uint32_t crypt_v = this->location_table_random.next(); - size_t choice = floorf((static_cast(sec.count) * static_cast(crypt_v)) / UINT32_MAX_AS_FLOAT); - uint32_t t = this->location_index_table[x]; - this->location_index_table[x] = this->location_index_table[choice]; - this->location_index_table[choice] = t; - } - } -} - -void Map::add_random_enemies_from_map_data( - Episode episode, - uint8_t difficulty, - uint8_t event, - uint8_t floor, - phosg::StringReader wave_events_segment_r, - phosg::StringReader locations_segment_r, - phosg::StringReader definitions_segment_r, - std::shared_ptr random_state, - std::shared_ptr rare_rates) { - - static const array rand_enemy_base_types = { - 0x44, 0x43, 0x41, 0x42, 0x40, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x80, - 0x81, 0x82, 0x83, 0x84, 0x85, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, - 0xA7, 0xA8, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, - 0xDE, 0xDF, 0xE0, 0xE0, 0xE1}; - - const auto& wave_events_header = wave_events_segment_r.get(); - if (wave_events_header.format != 0x65767432) { // 'evt2' - throw runtime_error("cannot generate random enemies from non-evt2 event stream"); - } - wave_events_segment_r.go(wave_events_header.entries_offset); - - size_t action_stream_base_offset = this->event_action_stream.size(); - this->event_action_stream += wave_events_segment_r.pread( - wave_events_header.action_stream_offset, wave_events_segment_r.size() - wave_events_header.action_stream_offset); - - const auto& locations_header = locations_segment_r.get(); - const auto& definitions_header = definitions_segment_r.get(); - auto definitions_r = definitions_segment_r.sub( - definitions_header.entries_offset, - definitions_header.entry_count * sizeof(RandomEnemyDefinition)); - auto weights_r = definitions_segment_r.sub( - definitions_header.weight_entries_offset, - definitions_header.weight_entry_count * sizeof(RandomEnemyWeight)); - - for (size_t wave_entry_index = 0; wave_entry_index < wave_events_header.entry_count; wave_entry_index++) { - auto entry_log = this->log.sub(phosg::string_printf("(Entry %zu/%" PRIu32 ") ", wave_entry_index, wave_events_header.entry_count.load())); - const auto& entry = wave_events_segment_r.get(); - - size_t remaining_waves = random_state->rand_int_biased(1, entry.max_waves); - // Trace: at 0080E125 EAX is wave count - - le_uint32_t wave_next_event_id = entry.event_id; - uint32_t wave_number = entry.wave_number; - while (remaining_waves) { - remaining_waves--; - auto wave_log = entry_log.sub(phosg::string_printf("(Wave %zu) ", remaining_waves)); - - size_t remaining_enemies = random_state->rand_int_biased(entry.min_enemies, entry.max_enemies); - // Trace: at 0080E208 EDI is enemy count - - random_state->generate_shuffled_location_table(locations_header, locations_segment_r, entry.section); - // Trace: at 0080EBB0 *(EBP + 4) points to table (0x20 uint32_ts) - - while (remaining_enemies) { - remaining_enemies--; - auto enemy_log = wave_log.sub(phosg::string_printf("(Enemy %zu) ", remaining_enemies)); - - // TODO: Factor this sum out of the loops - weights_r.go(0); - size_t weight_total = 0; - while (!weights_r.eof()) { - weight_total += weights_r.get().weight; - } - // Trace: at 0080E2C2 EBX is weight_total - - size_t det = random_state->rand_int_biased(0, weight_total - 1); - // Trace: at 0080E300 EDX is det - - weights_r.go(0); - while (!weights_r.eof()) { - const auto& weight_entry = weights_r.get(); - if (det < weight_entry.weight) { - if ((weight_entry.base_type_index != 0xFF) && (weight_entry.definition_entry_num != 0xFF)) { - EnemyEntry e; - e.base_type = rand_enemy_base_types.at(weight_entry.base_type_index); - e.wave_number = wave_number; - e.section = entry.section; - e.floor = floor; - - size_t bs_min = 0; - size_t bs_max = definitions_header.entry_count - 1; - if (bs_max == 0) { - throw runtime_error("no available random enemy definitions"); - } - do { - size_t bs_mid = (bs_min + bs_max) / 2; - if (definitions_r.pget(bs_mid * sizeof(RandomEnemyDefinition)).entry_num < weight_entry.definition_entry_num) { - bs_min = bs_mid + 1; - } else { - bs_max = bs_mid; - } - } while (bs_min < bs_max); - - const auto& def = definitions_r.pget(bs_min * sizeof(RandomEnemyDefinition)); - if (def.entry_num == weight_entry.definition_entry_num) { - e.fparam1 = def.fparam1; - e.fparam2 = def.fparam2; - e.fparam3 = def.fparam3; - e.fparam4 = def.fparam4; - e.fparam5 = def.fparam5; - e.uparam1 = def.uparam1; - e.uparam2 = def.uparam2; - e.num_children = random_state->rand_int_biased(def.min_children, def.max_children); - } else { - throw runtime_error("random enemy definition not found"); - } - - const auto& loc = locations_segment_r.pget( - locations_header.entries_offset + sizeof(RandomEnemyLocationEntry) * random_state->next_location_index()); - e.x = loc.x; - e.y = loc.y; - e.z = loc.z; - e.x_angle = loc.x_angle; - e.y_angle = loc.y_angle; - e.z_angle = loc.z_angle; - - // Trace: at 0080E6FE CX is base_type - this->add_enemy(episode, difficulty, event, floor, 0, e, rare_rates); - } else { - enemy_log.warning("Cannot create enemy: parameters are missing"); - } - break; - } else { - det -= weight_entry.weight; - } - } - } - if (remaining_waves) { - /* ev.delay = */ random_state->rand_int_biased(entry.min_delay, entry.max_delay); - this->add_event(wave_next_event_id, entry.flags, floor, entry.section, wave_number, this->event_action_stream.size()); - this->event_action_stream.push_back(0x0C); - wave_next_event_id = entry.event_id + wave_number + 10000; - this->event_action_stream.append(reinterpret_cast(&wave_next_event_id), sizeof(wave_next_event_id)); - this->event_action_stream.push_back(0x01); - wave_number++; - } - } - - /* ev.delay = */ random_state->rand_int_biased(entry.min_delay, entry.max_delay); - this->add_event(wave_next_event_id, entry.flags, floor, entry.section, wave_number, action_stream_base_offset + entry.action_stream_offset); - wave_number++; - } -} - -void Map::add_event(uint32_t event_id, uint16_t flags, uint8_t floor, uint16_t section, uint16_t wave_number, uint32_t action_stream_offset) { - size_t index = this->events.size(); - auto& ev = this->events.emplace_back(); - ev.event_id = event_id; - ev.section = section; - ev.wave_number = wave_number; - ev.flags = flags; - ev.floor = floor; - ev.action_stream_offset = action_stream_offset; - - uint64_t k = (static_cast(floor) << 32) | event_id; - this->floor_and_event_id_to_index.emplace(k, index); - k = section_index_key(floor, section, wave_number); - this->floor_section_and_wave_number_to_event_index.emplace(k, index); -} - -vector Map::get_events(uint8_t floor, uint32_t event_id) { - uint64_t k = (static_cast(floor) << 32) | event_id; - vector ret; - for (auto its = this->floor_and_event_id_to_index.equal_range(k); its.first != its.second; its.first++) { - ret.emplace_back(&this->events.at(its.first->second)); + ret += phosg::string_printf("%02zX:[%" PRIX32 ",%" PRIX32 "]", z, e.layout.load(), e.entities.load()); } return ret; } -vector Map::get_events(uint8_t floor, uint32_t event_id) const { - uint64_t k = (static_cast(floor) << 32) | event_id; - vector ret; - for (auto its = this->floor_and_event_id_to_index.equal_range(k); its.first != its.second; its.first++) { - ret.emplace_back(&this->events.at(its.first->second)); +phosg::JSON Variations::json() const { + auto ret = phosg::JSON::list(); + for (size_t z = 0; z < this->entries.size(); z++) { + const auto& e = this->entries[z]; + ret.emplace_back(phosg::JSON::dict({ + {"layout", e.layout.load()}, + {"entities", e.entities.load()}, + })); } return ret; } -void Map::add_events_from_map_data(uint8_t floor, const void* data, size_t size) { - phosg::StringReader r(data, size); - const auto& header = r.get(); - if (header.format != 0) { - throw runtime_error("events section format is not zero"); - } - - size_t action_stream_base_offset = this->event_action_stream.size(); - this->event_action_stream += r.pread(header.action_stream_offset, r.size() - header.action_stream_offset); - - this->events.reserve(this->events.size() + header.entry_count); - auto events_r = r.sub(header.entries_offset, sizeof(Event1Entry) * header.entry_count); - while (!events_r.eof()) { - const auto& entry = events_r.get(); - this->add_event(entry.event_id, entry.flags, floor, entry.section, entry.wave_number, entry.action_stream_offset + action_stream_base_offset); - } -} - -vector Map::collect_quest_map_data_sections(const void* data, size_t size) { - vector ret; - phosg::StringReader r(data, size); - while (!r.eof()) { - size_t header_offset = r.where(); - const auto& header = r.get(); - - if (header.type() == SectionHeader::Type::END && header.section_size == 0) { - break; - } - if (header.section_size < sizeof(header)) { - throw runtime_error(phosg::string_printf("quest layout has invalid section header at offset 0x%zX", r.where() - sizeof(header))); - } - - if (header.floor > 0x100) { - throw runtime_error("section floor number too large"); - } - - if (header.floor >= ret.size()) { - ret.resize(header.floor + 1); - } - auto& floor_sections = ret[header.floor]; - switch (header.type()) { - case SectionHeader::Type::OBJECTS: - if (floor_sections.objects != 0xFFFFFFFF) { - throw runtime_error("multiple objects sections for same floor"); - } - floor_sections.objects = header_offset; - break; - case SectionHeader::Type::ENEMIES: - if (floor_sections.enemies != 0xFFFFFFFF) { - throw runtime_error("multiple enemies sections for same floor"); - } - floor_sections.enemies = header_offset; - break; - case SectionHeader::Type::WAVE_EVENTS: - if (floor_sections.wave_events != 0xFFFFFFFF) { - throw runtime_error("multiple wave events sections for same floor"); - } - floor_sections.wave_events = header_offset; - break; - case SectionHeader::Type::RANDOM_ENEMY_LOCATIONS: - if (floor_sections.random_enemy_locations != 0xFFFFFFFF) { - throw runtime_error("multiple random enemy locations sections for same floor"); - } - floor_sections.random_enemy_locations = header_offset; - break; - case SectionHeader::Type::RANDOM_ENEMY_DEFINITIONS: - if (floor_sections.random_enemy_definitions != 0xFFFFFFFF) { - throw runtime_error("multiple random enemy definitions sections for same floor"); - } - floor_sections.random_enemy_definitions = header_offset; - break; - default: - throw runtime_error("invalid section type"); - } - r.skip(header.data_size); - } - return ret; -} - -void Map::add_entities_from_quest_data( - Episode episode, - uint8_t difficulty, - uint8_t event, - std::shared_ptr data, - std::shared_ptr rare_rates) { - this->link_owned_data(data); - - auto all_floor_sections = this->collect_quest_map_data_sections(data->data(), data->size()); - - phosg::StringReader r(*data); - shared_ptr random_state; - for (size_t floor = 0; floor < all_floor_sections.size(); floor++) { - const auto& floor_sections = all_floor_sections[floor]; - - if (floor_sections.objects != 0xFFFFFFFF) { - const auto& header = r.pget(floor_sections.objects); - if (header.data_size % sizeof(ObjectEntry)) { - throw runtime_error("quest layout object section size is not a multiple of object entry size"); - } - this->add_objects_from_owned_map_data(floor, r.pgetv(floor_sections.objects + sizeof(header), header.data_size), header.data_size); - } - - if ((floor_sections.wave_events != 0xFFFFFFFF) && - (floor_sections.random_enemy_locations != 0xFFFFFFFF) && - (floor_sections.random_enemy_definitions != 0xFFFFFFFF)) { - // Challenge Mode random enemy waves - const auto& wave_events_header = r.pget(floor_sections.wave_events); - const auto& random_enemy_locations_header = r.pget(floor_sections.random_enemy_locations); - const auto& random_enemy_definitions_header = r.pget(floor_sections.random_enemy_definitions); - if (!random_state) { - random_state = make_shared(this->rare_seed); - } - this->add_random_enemies_from_map_data( - episode, - difficulty, - event, - floor, - r.sub(floor_sections.wave_events + sizeof(SectionHeader), wave_events_header.data_size), - r.sub(floor_sections.random_enemy_locations + sizeof(SectionHeader), random_enemy_locations_header.data_size), - r.sub(floor_sections.random_enemy_definitions + sizeof(SectionHeader), random_enemy_definitions_header.data_size), - random_state, - rare_rates); - - } else { - // Non-Challenge (standard) enemies - if (floor_sections.enemies != 0xFFFFFFFF) { - const auto& header = r.pget(floor_sections.enemies); - if (header.data_size % sizeof(EnemyEntry)) { - throw runtime_error("quest layout enemy section size is not a multiple of enemy entry size"); - } - this->add_enemies_from_map_data( - episode, - difficulty, - event, - floor, - r.pgetv(floor_sections.enemies + sizeof(header), header.data_size), - header.data_size, - rare_rates); - } - - if (floor_sections.wave_events != 0xFFFFFFFF) { - const auto& wave_events_header = r.pget(floor_sections.wave_events); - const void* data = r.pgetv(floor_sections.wave_events + sizeof(SectionHeader), wave_events_header.data_size); - this->add_events_from_map_data(floor, data, wave_events_header.data_size); - } - } - } -} - -const Map::Enemy& Map::find_enemy(uint16_t enemy_id) const { - return const_cast(this)->find_enemy(enemy_id); -} - -Map::Enemy& Map::find_enemy(uint16_t enemy_id) { - if (this->enemies.empty()) { - throw out_of_range("no enemies defined"); - } - if (enemy_id >= this->enemies.size()) { - throw out_of_range("enemy ID out of range"); - } - auto& enemy = this->enemies[enemy_id]; - if (enemy.alias_entity_id != 0xFFFF) { - if (enemy.alias_entity_id >= this->enemies.size()) { - throw out_of_range("aliased enemy ID out of range"); - } - return this->enemies[enemy.alias_entity_id]; - } else { - return enemy; - } -} - -const Map::Enemy& Map::find_enemy(uint8_t floor, EnemyType type) const { - return const_cast(this)->find_enemy(floor, type); -} - -Map::Enemy& Map::find_enemy(uint8_t floor, EnemyType type) { - if (this->enemies.empty()) { - throw out_of_range("no enemies defined"); - } - // TODO: Linear search is bad here. Do something better, like binary search - // for the floor start and just linear search through the floor enemies. - for (auto& e : this->enemies) { - if (e.floor == floor && e.type == type) { - return e; - } - } - throw out_of_range("enemy not found"); -} - -std::vector Map::get_objects(uint8_t floor, uint16_t section, uint16_t group) { - uint64_t k = section_index_key(floor, section, group); - vector ret; - for (auto its = this->floor_section_and_group_to_object_index.equal_range(k); its.first != its.second; its.first++) { - ret.emplace_back(&this->objects.at(its.first->second)); - } - return ret; -} - -std::vector Map::get_enemies(uint8_t floor, uint16_t section, uint16_t wave_number) { - uint64_t k = section_index_key(floor, section, wave_number); - vector ret; - for (auto its = this->floor_section_and_wave_number_to_enemy_index.equal_range(k); its.first != its.second; its.first++) { - ret.emplace_back(&this->enemies.at(its.first->second)); - } - return ret; -} - -std::vector Map::get_events(uint8_t floor, uint16_t section, uint16_t wave_number) { - uint64_t k = section_index_key(floor, section, wave_number); - vector ret; - for (auto its = this->floor_section_and_wave_number_to_event_index.equal_range(k); its.first != its.second; its.first++) { - ret.emplace_back(&this->events.at(its.first->second)); - } - return ret; -} - -std::vector Map::get_events(uint8_t floor) { - uint64_t k_start = (static_cast(floor) << 32); - uint64_t k_end = (static_cast(floor + 1) << 32); - vector ret; - for (auto it = this->floor_and_event_id_to_index.lower_bound(k_start); - (it != this->floor_and_event_id_to_index.end()) && (it->first < k_end); - it++) { - ret.emplace_back(&this->events.at(it->second)); - } - return ret; -} - -vector Map::doors_for_switch_flag(uint8_t floor, uint8_t switch_flag) { - vector ret; - for (auto its = this->floor_and_switch_flag_to_door_index.equal_range((floor << 8) | switch_flag); - its.first != its.second; - its.first++) { - ret.emplace_back(&this->objects[its.first->second]); - } - return ret; -} - -template -static string disassemble_vector_file_t(const void* data, size_t size, size_t* entry_number, char type_ch) { - deque ret; - phosg::StringReader r(data, size); - - size_t local_entry_number = 0; - if (!entry_number) { - entry_number = &local_entry_number; - } - - while (r.remaining() >= sizeof(EntryT)) { - string o_str = r.get().str(); - ret.emplace_back(phosg::string_printf("/* %c-%zX */ %s", type_ch, (*entry_number)++, o_str.c_str())); - } - if (r.remaining()) { - ret.emplace_back("// Warning: section size is not a multiple of entry size"); - size_t size = r.remaining(); - ret.emplace_back(phosg::format_data(r.getv(size), size)); - } - return phosg::join(ret, "\n"); -} - -string Map::disassemble_objects_data(const void* data, size_t size, size_t* object_number) { - return disassemble_vector_file_t(data, size, object_number, 'K'); -} - -string Map::disassemble_enemies_data(const void* data, size_t size, size_t* enemy_number) { - return disassemble_vector_file_t(data, size, enemy_number, 'S'); -} - -string Map::disassemble_wave_events_data(const void* data, size_t size, uint8_t floor) { - deque ret; - phosg::StringReader r(data, size); - - const auto& evt_header = r.get(); - if (evt_header.format == 0x65767432) { // 'evt2' - ret.emplace_back(".evt2_format"); // TODO - size_t size = r.remaining(); - ret.emplace_back(phosg::format_data(r.getv(size), size)); - } else { - auto action_stream_r = r.sub(evt_header.action_stream_offset); - for (size_t z = 0; z < evt_header.entry_count; z++) { - const auto& entry = r.get(); - ret.emplace_back(phosg::string_printf("/* W-%02hhX-%" PRIX32 " */ [Event1Entry flags=%04hX type=%04hX section=%04hX wave_number=%04hX delay=%" PRIu32 "]", - floor, - entry.event_id.load(), - entry.flags.load(), - entry.event_type.load(), - entry.section.load(), - entry.wave_number.load(), - entry.delay.load())); - auto ev_actions_r = action_stream_r.sub(entry.action_stream_offset); - bool should_continue = true; - while (!ev_actions_r.eof() && should_continue) { - uint8_t opcode = ev_actions_r.get_u8(); - switch (opcode) { - case 0x00: - ret.emplace_back(phosg::string_printf(" 00 nop")); - break; - case 0x01: - ret.emplace_back(phosg::string_printf(" 01 stop")); - should_continue = false; - break; - case 0x08: { - uint16_t section = ev_actions_r.get_u16l(); - uint16_t group = ev_actions_r.get_u16l(); - ret.emplace_back(phosg::string_printf(" 08 %04hX %04hX construct_objects section=%04hX group=%04hX", - section, group, section, group)); - break; - } - case 0x09: { - uint16_t section = ev_actions_r.get_u16l(); - uint16_t wave_number = ev_actions_r.get_u16l(); - ret.emplace_back(phosg::string_printf(" 09 %04hX %04hX construct_enemies section=%04hX wave_number=%04hX", - section, wave_number, section, wave_number)); - break; - } - case 0x0A: { - uint16_t id = ev_actions_r.get_u16l(); - ret.emplace_back(phosg::string_printf(" 0A %04hX enable_switch_flag id=%04hX", id, id)); - break; - } - case 0x0B: { - uint16_t id = ev_actions_r.get_u16l(); - ret.emplace_back(phosg::string_printf(" 0B %04hX disable_switch_flag id=%04hX", id, id)); - break; - } - case 0x0C: { - uint32_t event_id = ev_actions_r.get_u32l(); - ret.emplace_back(phosg::string_printf(" 0C %08" PRIX32 " trigger_event event_id=%08" PRIX32, event_id, event_id)); - break; - } - case 0x0D: { - uint16_t section = ev_actions_r.get_u16l(); - uint16_t wave_number = ev_actions_r.get_u16l(); - ret.emplace_back(phosg::string_printf(" 0D %04hX %04hX construct_enemies_stop section=%04hX wave_number=%04hX", - section, wave_number, section, wave_number)); - break; - } - default: - ret.emplace_back(phosg::string_printf(" %02hhX .invalid", opcode)); - } - } - } - } - - return phosg::join(ret, "\n"); -} - -string Map::disassemble_quest_data(const void* data, size_t size) { - auto all_floor_sections = Map::collect_quest_map_data_sections(data, size); - - deque ret; - phosg::StringReader r(data, size); - size_t object_number = 0; - size_t enemy_number = 0; - for (size_t floor = 0; floor < all_floor_sections.size(); floor++) { - const auto& floor_sections = all_floor_sections[floor]; - - if (floor_sections.objects != 0xFFFFFFFF) { - ret.emplace_back(phosg::string_printf(".objects %zu", floor)); - const auto& header = r.pget(floor_sections.objects); - size_t offset = floor_sections.objects + sizeof(SectionHeader); - ret.emplace_back(Map::disassemble_objects_data(r.pgetv(offset, header.data_size), header.data_size, &object_number)); - } - if (floor_sections.enemies != 0xFFFFFFFF) { - ret.emplace_back(phosg::string_printf(".enemies %zu", floor)); - const auto& header = r.pget(floor_sections.enemies); - size_t offset = floor_sections.enemies + sizeof(SectionHeader); - ret.emplace_back(Map::disassemble_enemies_data(r.pgetv(offset, header.data_size), header.data_size, &enemy_number)); - } - if (floor_sections.wave_events != 0xFFFFFFFF) { - ret.emplace_back(phosg::string_printf(".wave_events %zu", floor)); - const auto& header = r.pget(floor_sections.wave_events); - size_t offset = floor_sections.wave_events + sizeof(SectionHeader); - ret.emplace_back(Map::disassemble_wave_events_data(r.pgetv(offset, header.data_size), header.data_size, floor)); - } - if (floor_sections.random_enemy_locations != 0xFFFFFFFF) { - ret.emplace_back(phosg::string_printf(".random_enemy_locations %zu", floor)); - const auto& header = r.pget(floor_sections.random_enemy_locations); - size_t offset = floor_sections.random_enemy_locations + sizeof(SectionHeader); - auto sub_r = r.sub(offset, header.data_size); - ret.emplace_back(phosg::format_data(sub_r.getv(sub_r.remaining()), header.data_size, offset)); - } - if (floor_sections.random_enemy_definitions != 0xFFFFFFFF) { - ret.emplace_back(phosg::string_printf(".random_enemy_definitions %zu", floor)); - const auto& header = r.pget(floor_sections.random_enemy_definitions); - size_t offset = floor_sections.random_enemy_definitions + sizeof(SectionHeader); - auto sub_r = r.sub(offset, header.data_size); - ret.emplace_back(phosg::format_data(sub_r.getv(sub_r.remaining()), header.data_size, offset)); - } - } - - return phosg::join(ret, "\n") + "\n"; -} - SetDataTableBase::SetDataTableBase(Version version) : version(version) {} -parray SetDataTableBase::generate_variations( - Episode episode, bool is_solo, std::shared_ptr opt_rand_crypt) const { - parray ret; - for (size_t floor = 0; floor < 0x10; floor++) { - auto num_vars = this->num_free_roam_variations_for_floor(episode, is_solo, floor); - ret[floor * 2] = (num_vars.first > 1) ? (random_from_optional_crypt(opt_rand_crypt) % num_vars.first) : 0; - ret[floor * 2 + 1] = (num_vars.second > 1) ? (random_from_optional_crypt(opt_rand_crypt) % num_vars.second) : 0; +Variations SetDataTableBase::generate_variations( + Episode episode, bool is_solo, shared_ptr opt_rand_crypt) const { + Variations ret; + for (size_t floor = 0; floor < ret.entries.size(); floor++) { + auto& e = ret.entries[floor]; + auto num_vars = this->num_free_play_variations_for_floor(episode, is_solo, floor); + e.layout = (num_vars.layout > 1) ? (random_from_optional_crypt(opt_rand_crypt) % num_vars.layout) : 0; + e.entities = (num_vars.entities > 1) ? (random_from_optional_crypt(opt_rand_crypt) % num_vars.entities) : 0; } return ret; } vector SetDataTableBase::map_filenames_for_variations( - const parray& variations, Episode episode, GameMode mode, FilenameType type) const { + Episode episode, GameMode mode, const Variations& variations, FilenameType type) const { vector ret; for (uint8_t floor = 0; floor < 0x10; floor++) { - ret.emplace_back(this->map_filename_for_variation( - floor, variations[floor * 2], variations[floor * 2 + 1], episode, mode, type)); + const auto& e = variations.entries[floor]; + ret.emplace_back(this->map_filename_for_variation(episode, mode, floor, e.layout, e.entities, type)); } for (uint8_t floor = 0x10; floor < 0x12; floor++) { - ret.emplace_back(this->map_filename_for_variation(floor, 0, 0, episode, mode, type)); + ret.emplace_back(this->map_filename_for_variation(episode, mode, floor, 0, 0, type)); } return ret; } @@ -1972,13 +73,13 @@ vector SetDataTableBase::map_filenames_for_variations( uint8_t SetDataTableBase::default_area_for_floor(Episode episode, uint8_t floor) const { // For some inscrutable reason, Pioneer 2's area number in Episode 4 is // discontiguous with all the rest. Why, Sega?? - static const std::array areas_ep1 = { + static const array areas_ep1 = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11}; - static const std::array areas_ep2_gc_nte = { + static const array areas_ep2_gc_nte = { 0x00, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0xFF, 0xFF}; - static const std::array areas_ep2 = { + static const array areas_ep2 = { 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23}; - static const std::array areas_ep4 = { + static const array areas_ep4 = { 0x2D, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2E, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; switch (episode) { case Episode::EP1: @@ -2004,63 +105,54 @@ SetDataTable::SetDataTable(Version version, const string& data) : SetDataTableBa template void SetDataTable::load_table_t(const string& data) { + using FooterT = RELFileFooterT; + phosg::StringReader r(data); - struct Footer { - U32T table3_offset; - U32T table3_count; // In le_uint16_ts (so *2 for size in bytes) - U32T unknown_a3; // == 1 - U32T unknown_a4; // == 0 - U32T root_table_offset_offset; - U32T unknown_a6; // == 0 - U32T unknown_a7; // == 0 - U32T unknown_a8; // == 0 - } __packed_ws__(Footer, 0x20); - - if (r.size() < sizeof(Footer)) { + if (r.size() < sizeof(FooterT)) { throw runtime_error("set data table is too small"); } - auto& footer = r.pget