From c0f4f7af5fdbf79f7a8912454b1b3210d69cc6c0 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Tue, 15 Aug 2023 09:00:50 -0700 Subject: [PATCH] add Ep3 trial map format --- README.md | 3 +- src/Episode3/DataIndexes.cc | 60 ++++++++++++++++++++++++++-- src/Episode3/DataIndexes.hh | 79 +++++++++++++++++++++++++++++++------ src/Episode3/Server.cc | 12 +++--- src/Episode3/Server.hh | 4 +- src/ProxyCommands.cc | 6 +-- src/ReceiveCommands.cc | 5 ++- 7 files changed, 140 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index b8d9e04f..2ff31fc6 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,6 @@ Current known issues / missing features / things to do: - Implement the C5 (battle/challenge records) command. - Implement choice search. - Episode 3 bugs - - Trial Edition can't select maps in battle setup. Fix this. - Fix behavior when joining a spectator team after the beginning of a battle. - Disconnecting during a match turns you into a COM if there are other humans in the match, even if the match is part of a tournament. This may be incorrect behavior for tournaments. - Disconnecting during a tournament when there are no other humans in the match simply cancels the match (so it can be replayed) instead of forfeiting, which is almost certainly incorrect behavior. (Then again, no one likes losing tournaments to COMs...) @@ -101,7 +100,7 @@ newserv supports several versions of PSO. Specifically: 2. *newserv's implementations of these versions are based on disassembly of the client executables and have never been tested.* 3. *BB games are mostly playable, but there are still some unimplemented features (for example, some quests that use rare commands may not work). Please submit a GitHub issue if you find something that doesn't work.* 4. *Support for PSO Dreamcast Trial Edition and the December 2000 prototype is somewhat incomplete and probably never will be complete. These versions are rather unstable and seem to crash often, but it's not obvious whether it's because they're prototypes or because newserv sends data they can't handle.* -5. *Creating a game works, but choosing a map during battle setup causes the Trial Edition client to crash. This is likely due to the trial version's map format being slightly different from the final version's map format.* +5. *Creating a game works and battle setup behaves mostly normally, but starting a battle doesn't work.* ## Setup diff --git a/src/Episode3/DataIndexes.cc b/src/Episode3/DataIndexes.cc index 47ced51f..f17e78fa 100644 --- a/src/Episode3/DataIndexes.cc +++ b/src/Episode3/DataIndexes.cc @@ -1630,6 +1630,50 @@ string MapDefinition::str(const CardIndex* card_index) const { return join(lines, "\n"); } +MapDefinitionTrial::MapDefinitionTrial(const MapDefinition& map) + : unknown_a1(map.unknown_a1), + map_number(map.map_number), + width(map.width), + height(map.height), + environment_number(map.environment_number), + num_alt_maps(map.num_alt_maps), + map_tiles(map.map_tiles), + start_tile_definitions(map.start_tile_definitions), + alt_maps1(map.alt_maps1), + alt_maps_unknown_a3(map.alt_maps_unknown_a3), + unknown_a4(map.unknown_a4), + modification_tiles(map.modification_tiles), + unknown_a5(map.unknown_a5), + default_rules(map.default_rules), + unknown_a6(map.unknown_a6), + name(map.name), + location_name(map.location_name), + quest_name(map.quest_name), + description(map.description), + map_x(map.map_x), + map_y(map.map_y), + npc_decks(map.npc_decks), + npc_chars(map.npc_chars), + unknown_a7_a(map.unknown_a7_a), + unknown_a7_b(map.unknown_a7_b), + before_message(map.before_message), + after_message(map.after_message), + dispatch_message(map.dispatch_message), + dialogue_sets(), + reward_card_ids(map.reward_card_ids), + unknown_a9_a(map.unknown_a9_a), + unknown_a9_b(map.unknown_a9_b), + unknown_a9_c(map.unknown_a9_c), + unknown_a9_d(map.unknown_a9_d), + unknown_a10(map.unknown_a10), + cyber_block_type(map.cyber_block_type), + unknown_a11(map.unknown_a11), + unknown_t12(0xFF) { + for (size_t z = 0; z < this->dialogue_sets.size(); z++) { + this->dialogue_sets[z] = map.dialogue_sets[z].sub<8>(0); + } +} + bool Rules::check_invalid_fields() const { Rules t = *this; return t.check_and_reset_invalid_fields(); @@ -1969,11 +2013,19 @@ MapIndex::MapEntry::MapEntry(const string& compressed, bool is_quest) this->map = *reinterpret_cast(decompressed.data()); } -string MapIndex::MapEntry::compressed() const { - if (this->compressed_data.empty()) { - this->compressed_data = prs_compress(&this->map, sizeof(this->map)); +const string& MapIndex::MapEntry::compressed(bool is_trial) const { + if (is_trial) { + if (this->compressed_trial_data.empty()) { + MapDefinitionTrial mdt(this->map); + this->compressed_trial_data = prs_compress(&mdt, sizeof(mdt)); + } + return this->compressed_trial_data; + } else { + if (this->compressed_data.empty()) { + this->compressed_data = prs_compress(&this->map, sizeof(this->map)); + } + return this->compressed_data; } - return this->compressed_data; } const string& MapIndex::get_compressed_list() const { diff --git a/src/Episode3/DataIndexes.hh b/src/Episode3/DataIndexes.hh index f53f3a9a..1298f073 100644 --- a/src/Episode3/DataIndexes.hh +++ b/src/Episode3/DataIndexes.hh @@ -553,7 +553,7 @@ struct CardDefinition { /* 00A0 */ ptext en_name; /* 00B4 */ ptext jp_short_name; /* 00BF */ ptext en_short_name; - /* 00C7 */ Effect effects[3]; + /* 00C7 */ parray effects; /* 0127 */ uint8_t unused4; /* 0128 */ @@ -573,9 +573,9 @@ struct CardDefinitionsFooter { /* 08 */ be_uint32_t num_cards2; /* 0C */ parray unknown_a2; /* 38 */ be_uint32_t unknown_offset_a3; - /* 3C */ be_uint32_t unknown_a4[3]; + /* 3C */ parray unknown_a4; /* 48 */ be_uint32_t footer_offset; - /* 4C */ be_uint32_t unknown_a5[3]; + /* 4C */ parray unknown_a5; /* 58 */ } __attribute__((packed)); @@ -701,6 +701,12 @@ struct Rules { /* 0D */ parray unused; /* 10 */ + // Annoyingly, this structure is a different size in Episode 3 Trial Edition. + // This means that many command formats, as well as the map format, are + // different, and the existing Server implementation can't serve Trial Edition + // clients. It'd be nice to support Trial Edition battles, but that would + // likely be more work than it's worth. + Rules(); explicit Rules(std::shared_ptr json); std::shared_ptr json() const; @@ -838,9 +844,9 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests // - If the team has 3 players, bytes [3] through [5] are used. /* 010C */ parray, 2> start_tile_definitions; - /* 0118 */ parray, 0x10> alt_maps1[2][0x0A]; - /* 1518 */ parray alt_maps_unknown_a3[2][0x0A]; - /* 1AB8 */ parray unknown_a4[3]; + /* 0118 */ parray, 0x10>, 0x0A>, 2> alt_maps1; + /* 1518 */ parray, 0x0A>, 2> alt_maps_unknown_a3; + /* 1AB8 */ parray, 3> unknown_a4; // In the modification_tiles array, the values are: // 10 = blocked by rock (as if the corresponding map_tiles value was 00) @@ -871,7 +877,8 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests /* 18 */ parray card_ids; // Last one appears to always be FFFF /* 58 */ } __attribute__((packed)); - /* 1FE8 */ NPCDeck npc_decks[3]; // Unused if name[0] == 0 + /* 1FE8 */ parray npc_decks; // Unused if name[0] == 0 + struct NPCCharacter { /* 0000 */ parray unknown_a1; /* 0004 */ parray unknown_a2; @@ -879,7 +886,7 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests /* 0018 */ parray unknown_a3; /* 0114 */ } __attribute__((packed)); - /* 20F0 */ NPCCharacter npc_chars[3]; // Unused if name[0] == 0 + /* 20F0 */ parray npc_chars; // Unused if name[0] == 0 /* 242C */ parray unknown_a7_a; // Always FF? /* 2434 */ parray unknown_a7_b; // Always FF? @@ -896,10 +903,10 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests struct DialogueSet { /* 0000 */ be_uint16_t unknown_a1; /* 0002 */ be_uint16_t unknown_a2; // Always 0x0064 if valid, 0xFFFF if unused? - /* 0004 */ ptext strings[4]; + /* 0004 */ parray, 4> strings; /* 0104 */ } __attribute__((packed)); - /* 28F0 */ DialogueSet dialogue_sets[3][0x10]; // Up to 0x10 per valid NPC + /* 28F0 */ parray, 3> dialogue_sets; // Up to 0x10 per valid NPC /* 59B0 */ parray reward_card_ids; @@ -958,6 +965,55 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests std::string str(const CardIndex* card_index = nullptr) const; } __attribute__((packed)); +struct MapDefinitionTrial { + // This is the format of Episode 3 Trial Edition maps. See the comments in + // MapDefinition for what each field means. + + /* 0000 */ be_uint32_t unknown_a1; + /* 0004 */ be_uint32_t map_number; + /* 0008 */ uint8_t width; + /* 0009 */ uint8_t height; + /* 000A */ uint8_t environment_number; + /* 000B */ uint8_t num_alt_maps; + /* 000C */ parray, 0x10> map_tiles; + /* 010C */ parray, 2> start_tile_definitions; + /* 0118 */ parray, 0x10>, 0x0A>, 2> alt_maps1; + /* 1518 */ parray, 0x0A>, 2> alt_maps_unknown_a3; + /* 1AB8 */ parray, 3> unknown_a4; + /* 1C68 */ parray, 0x10> modification_tiles; + /* 1D68 */ parray unknown_a5; + /* 1DD4 */ Rules default_rules; + /* 1DE4 */ parray unknown_a6; + /* 1DE8 */ ptext name; + /* 1DFC */ ptext location_name; + /* 1E10 */ ptext quest_name; + /* 1E4C */ ptext description; + /* 1FDC */ be_uint16_t map_x; + /* 1FDE */ be_uint16_t map_y; + /* 1FE0 */ parray npc_decks; + /* 20E8 */ parray npc_chars; + /* 2424 */ parray unknown_a7_a; + /* 242C */ parray unknown_a7_b; + /* 2438 */ ptext before_message; + /* 25C8 */ ptext after_message; + /* 2758 */ ptext dispatch_message; + /* 28E8 */ parray, 3> dialogue_sets; + /* 4148 */ parray reward_card_ids; + /* 4168 */ be_uint32_t unknown_a9_a; + /* 416C */ be_uint32_t unknown_a9_b; + /* 4170 */ be_uint16_t unknown_a9_c; + /* 4172 */ be_uint16_t unknown_a9_d; + /* 4174 */ uint8_t unknown_a10; + /* 4175 */ uint8_t cyber_block_type; + /* 4176 */ parray unknown_a11; + // TODO: This field may contain some version of unavailable_sc_cards and/or + // entry_states from MapDefinition, but the format isn't the same + /* 4178 */ parray unknown_t12; + /* 41A0 */ + + MapDefinitionTrial(const MapDefinition& map); +} __attribute__((packed)); + struct COMDeckDefinition { size_t index; std::string player_name; @@ -1000,10 +1056,11 @@ public: MapEntry(const MapDefinition& map, bool is_quest); MapEntry(const std::string& compressed_data, bool is_quest); - std::string compressed() const; + const std::string& compressed(bool is_trial) const; private: mutable std::string compressed_data; + mutable std::string compressed_trial_data; }; const std::string& get_compressed_list() const; diff --git a/src/Episode3/Server.cc b/src/Episode3/Server.cc index 9a81f280..80a47b5c 100644 --- a/src/Episode3/Server.cc +++ b/src/Episode3/Server.cc @@ -225,8 +225,9 @@ void Server::send_6xB4x46() const { this->send(cmd46); } -string Server::prepare_6xB6x41_map_definition(shared_ptr map) { - const auto& compressed = map->compressed(); +string Server::prepare_6xB6x41_map_definition( + shared_ptr map, bool is_trial) { + const auto& compressed = map->compressed(is_trial); StringWriter w; uint32_t subcommand_size = (compressed.size() + sizeof(G_MapData_GC_Ep3_6xB6x41) + 3) & (~3); @@ -235,7 +236,7 @@ string Server::prepare_6xB6x41_map_definition(shared_ptrsetup_phase == SetupPhase::REGISTRATION) { // If registration is still in progress, we only need to send the map data @@ -248,7 +249,7 @@ void Server::send_commands_for_joining_spectator(Channel& c) const { auto map = this->base()->last_chosen_map; if (map) { - string data = this->prepare_6xB6x41_map_definition(map); + string data = this->prepare_6xB6x41_map_definition(map, is_trial); c.send(0x6C, 0x00, data); } @@ -2167,7 +2168,8 @@ void Server::handle_6xB3x41_map_request(const string& data) { } base->last_chosen_map = base->map_index->definition_for_number(cmd.map_number); - auto out_cmd = this->prepare_6xB6x41_map_definition(base->last_chosen_map); + auto out_cmd = this->prepare_6xB6x41_map_definition( + base->last_chosen_map, l->flags & Lobby::Flag::IS_EP3_TRIAL); send_command(l, 0x6C, 0x00, out_cmd); for (auto watcher_l : l->watcher_lobbies) { send_command_if_not_loading(watcher_l, 0x6C, 0x00, out_cmd); diff --git a/src/Episode3/Server.hh b/src/Episode3/Server.hh index 16fdb664..dfcd9d4b 100644 --- a/src/Episode3/Server.hh +++ b/src/Episode3/Server.hh @@ -120,7 +120,7 @@ public: } void send(const void* data, size_t size) const; - void send_commands_for_joining_spectator(Channel& ch) const; + void send_commands_for_joining_spectator(Channel& ch, bool is_trial) const; __attribute__((format(printf, 2, 3))) void log_debug(const char* fmt, ...) const; @@ -233,7 +233,7 @@ public: G_UpdateDecks_GC_Ep3_6xB4x07 prepare_6xB4x07_decks_update() const; G_SetPlayerNames_GC_Ep3_6xB4x1C prepare_6xB4x1C_names_update() const; static std::string prepare_6xB6x41_map_definition( - std::shared_ptr map); + std::shared_ptr map, bool is_trial); G_SetTrapTileLocations_GC_Ep3_6xB4x50 prepare_6xB4x50_trap_tile_locations() const; std::vector> const_cast_set_cards_v( diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index e1a8776c..97c06df1 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -916,9 +916,9 @@ static HandlerResult S_6x(shared_ptr, string map_data = prs_decompress( data.data() + sizeof(cmd), data.size() - sizeof(cmd)); save_file(filename, map_data); - if (map_data.size() != sizeof(Episode3::MapDefinition)) { - session.log.warning("Wrote %zu bytes to %s (expected %zu bytes; the file may be invalid)", - map_data.size(), filename.c_str(), sizeof(Episode3::MapDefinition)); + if (map_data.size() != sizeof(Episode3::MapDefinition) && map_data.size() != sizeof(Episode3::MapDefinitionTrial)) { + session.log.warning("Wrote %zu bytes to %s (expected %zu or %zu bytes; the file may be invalid)", + map_data.size(), filename.c_str(), sizeof(Episode3::MapDefinitionTrial), sizeof(Episode3::MapDefinition)); } else { session.log.info("Wrote %zu bytes to %s", map_data.size(), filename.c_str()); } diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index c7f2990e..323f8b7e 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -1241,7 +1241,7 @@ static void on_DC_Ep3(shared_ptr s, shared_ptr c, if (tourn) { send_ep3_set_tournament_player_decks(s, l, c, l->tournament_match); string data = Episode3::Server::prepare_6xB6x41_map_definition( - tourn->get_map()); + tourn->get_map(), l->flags & Lobby::Flag::IS_EP3_TRIAL); c->channel.send(0x6C, 0x00, data); } } @@ -3525,7 +3525,8 @@ static void on_6F(shared_ptr s, shared_ptr c, if (l->battle_player && (l->flags & Lobby::Flag::START_BATTLE_PLAYER_IMMEDIATELY)) { l->battle_player->start(); } else if (watched_lobby && watched_lobby->ep3_server_base) { - watched_lobby->ep3_server_base->server->send_commands_for_joining_spectator(c->channel); + watched_lobby->ep3_server_base->server->send_commands_for_joining_spectator( + c->channel, c->flags & Client::Flag::IS_EP3_TRIAL_EDITION); } // If there are more players to bring in, try to do so