From 894ac6b8ff28c2d718bcf9456657bff68c117ccf Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sun, 21 Dec 2025 10:35:41 -0800 Subject: [PATCH] reformat more files; add Ep3 map endpoint in HTTP server --- src/DownloadSession.cc | 8 +- src/Episode3/DataIndexes.cc | 217 ++++++++++++++++++++++++++++++------ src/Episode3/DataIndexes.hh | 4 +- src/FileContentsCache.cc | 6 +- src/FileContentsCache.hh | 6 +- src/FunctionCompiler.cc | 5 +- src/GSLArchive.cc | 6 +- src/GameServer.cc | 18 ++- src/GameServer.hh | 3 +- src/HTTPServer.cc | 55 ++++++++- src/HTTPServer.hh | 3 +- src/IPFrameInfo.cc | 3 +- src/IPStackSimulator.cc | 132 +++++++++------------- src/ImageEncoder.hh | 5 +- src/IntegralExpression.cc | 23 ++-- src/Map.hh | 2 +- src/StaticGameData.cc | 32 ++++++ src/StaticGameData.hh | 1 + src/Text.hh | 15 +++ 19 files changed, 376 insertions(+), 168 deletions(-) diff --git a/src/DownloadSession.cc b/src/DownloadSession.cc index 41d2c914..0581b4f0 100644 --- a/src/DownloadSession.cc +++ b/src/DownloadSession.cc @@ -544,8 +544,8 @@ asio::awaitable DownloadSession::on_message(Channel::Message& msg) { } case 0x67: { - // Technically we should assign item IDs here, but the server will never - // be able to see that we didn't, so we don't bother + // Technically we should assign item IDs here, but the server will never be able to see that we didn't, so we + // don't bother const auto& game_config = this->game_configs[this->current_game_config_index]; if (this->version == Version::PC_V2) { @@ -688,9 +688,7 @@ asio::awaitable DownloadSession::on_message(Channel::Message& msg) { } auto& f = this->open_files.at(cmd.filename.decode()); size_t block_offset = msg.flag * 0x400; - size_t allowed_block_size = (block_offset < f.total_size) - ? min(f.total_size - block_offset, 0x400) - : 0; + size_t allowed_block_size = (block_offset < f.total_size) ? min(f.total_size - block_offset, 0x400) : 0; size_t data_size = min(cmd.data_size, allowed_block_size); size_t block_end_offset = block_offset + data_size; if (block_end_offset > f.data.size()) { diff --git a/src/Episode3/DataIndexes.cc b/src/Episode3/DataIndexes.cc index ad8ea785..693939a9 100644 --- a/src/Episode3/DataIndexes.cc +++ b/src/Episode3/DataIndexes.cc @@ -1814,7 +1814,7 @@ phosg::JSON MapDefinition::AIParams::json(Language language) const { return phosg::JSON::dict({ {"IsArkz", this->is_arkz ? true : false}, {"Name", this->ai_name.decode(language)}, - {"CardIDs", std::move(params_json)}, + {"Params", std::move(params_json)}, }); } @@ -1826,7 +1826,7 @@ phosg::JSON MapDefinition::DialogueSet::json(Language language) const { return phosg::JSON::dict({ {"When", this->when.load()}, {"PercentChance", this->percent_chance.load()}, - {"CardIDs", std::move(strings_json)}, + {"Strings", std::move(strings_json)}, }); } @@ -1871,9 +1871,6 @@ phosg::JSON MapDefinition::EntryState::json() const { return phosg::JSON::dict({{"PlayerType", std::move(player_type_json)}, {"DeckType", std::move(deck_type_json)}}); } -// TODO: -// phosg::JSON MapDefinition::json() const { ... } - string MapDefinition::CameraSpec::str() const { return std::format( "CameraSpec[a1=({:g} {:g} {:g} {:g} {:g} {:g} {:g} {:g} {:g}) camera=({:g} {:g} {:g}) focus=({:g} {:g} {:g}) a2=({:g} {:g} {:g})]", @@ -1883,6 +1880,57 @@ string MapDefinition::CameraSpec::str() const { this->unknown_a2[0], this->unknown_a2[1], this->unknown_a2[2]); } +struct UnavailableSCCardDefinition { + size_t index; + uint16_t card_id; + const char* internal_name; +}; + +static const array unavailable_sc_card_defs = { + UnavailableSCCardDefinition{0x00, 0x0005, "Guykild"}, + UnavailableSCCardDefinition{0x01, 0x0006, "Kylria"}, + UnavailableSCCardDefinition{0x02, 0x0110, "Saligun"}, + UnavailableSCCardDefinition{0x03, 0x0111, "Relmitos"}, + UnavailableSCCardDefinition{0x04, 0x0002, "Kranz"}, + UnavailableSCCardDefinition{0x05, 0x0004, "Sil'fer"}, + UnavailableSCCardDefinition{0x06, 0x0003, "Ino'lis"}, + UnavailableSCCardDefinition{0x07, 0x0112, "Viviana"}, + UnavailableSCCardDefinition{0x08, 0x0113, "Teifu"}, + UnavailableSCCardDefinition{0x09, 0x0001, "Orland"}, + UnavailableSCCardDefinition{0x0A, 0x0114, "Stella"}, + UnavailableSCCardDefinition{0x0B, 0x0115, "Glustar"}, + UnavailableSCCardDefinition{0x0C, 0x0117, "Hyze"}, + UnavailableSCCardDefinition{0x0D, 0x0118, "Rufina"}, + UnavailableSCCardDefinition{0x0E, 0x0119, "Peko"}, + UnavailableSCCardDefinition{0x0F, 0x011A, "Creinu"}, + UnavailableSCCardDefinition{0x10, 0x011B, "Reiz"}, + UnavailableSCCardDefinition{0x11, 0x0007, "Lura"}, + UnavailableSCCardDefinition{0x12, 0x0008, "Break"}, + UnavailableSCCardDefinition{0x13, 0x011C, "Rio"}, + UnavailableSCCardDefinition{0x14, 0x0116, "Endu"}, + UnavailableSCCardDefinition{0x15, 0x011D, "Memoru"}, + UnavailableSCCardDefinition{0x16, 0x011E, "K.C."}, + UnavailableSCCardDefinition{0x17, 0x011F, "Ohgun"}, + UnavailableSCCardDefinition{0x18, 0x02AA, "HERO_1"}, + UnavailableSCCardDefinition{0x19, 0x02AB, "HERO_2"}, + UnavailableSCCardDefinition{0x1A, 0x02AC, "HERO_3"}, + UnavailableSCCardDefinition{0x1B, 0x02AD, "HERO_4"}, + UnavailableSCCardDefinition{0x1C, 0x02AE, "HERO_5"}, + UnavailableSCCardDefinition{0x1D, 0x02AF, "HERO_6"}, + UnavailableSCCardDefinition{0x1E, 0x02B0, "DARK_1"}, + UnavailableSCCardDefinition{0x1F, 0x02B1, "DARK_2"}, + UnavailableSCCardDefinition{0x20, 0x02B2, "DARK_3"}, + UnavailableSCCardDefinition{0x21, 0x02B3, "DARK_4"}, + UnavailableSCCardDefinition{0x22, 0x02B4, "DARK_5"}, + UnavailableSCCardDefinition{0x23, 0x02B5, "DARK_6"}, + UnavailableSCCardDefinition{0x24, 0x029B, "LEUKON"}, + UnavailableSCCardDefinition{0x25, 0x029C, "CASTOR"}, + UnavailableSCCardDefinition{0x26, 0x029D, "POLLUX"}, + UnavailableSCCardDefinition{0x27, 0x029E, "AMPLUM"}, + UnavailableSCCardDefinition{0x28, 0x02BE, "CASTOR_USR"}, + UnavailableSCCardDefinition{0x29, 0x02BF, "POLLUX_USR"}, +}; + string MapDefinition::str(const CardIndex* card_index, Language language) const { deque lines; auto add_map = [&](const parray, 0x10>& tiles) { @@ -2043,32 +2091,6 @@ string MapDefinition::str(const CardIndex* card_index, Language language) const lines.emplace_back(std::format(" map_category: {:02X}", this->map_category)); lines.emplace_back(std::format(" cyber_block_type: {:02X}", this->cyber_block_type)); lines.emplace_back(std::format(" a11: {:04X}", this->unknown_a11)); - static const array sc_card_entry_names = { - "00 (Guykild; 0005)", - "01 (Kylria; 0006)", - "02 (Saligun; 0110)", - "03 (Relmitos; 0111)", - "04 (Kranz; 0002)", - "05 (Sil'fer; 0004)", - "06 (Ino'lis; 0003)", - "07 (Viviana; 0112)", - "08 (Teifu; 0113)", - "09 (Orland; 0001)", - "0A (Stella; 0114)", - "0B (Glustar; 0115)", - "0C (Hyze; 0117)", - "0D (Rufina; 0118)", - "0E (Peko; 0119)", - "0F (Creinu; 011A)", - "10 (Reiz; 011B)", - "11 (Lura; 0007)", - "12 (Break; 0008)", - "13 (Rio; 011C)", - "14 (Endu; 0116)", - "15 (Memoru; 011D)", - "16 (K.C.; 011E)", - "17 (Ohgun; 011F)", - }; string unavailable_sc_cards = " unavailable_sc_cards: ["; for (size_t z = 0; z < 0x18; z++) { if (this->unavailable_sc_cards[z] == 0xFFFF) { @@ -2077,10 +2099,11 @@ string MapDefinition::str(const CardIndex* card_index, Language language) const if (unavailable_sc_cards.size() > 25) { unavailable_sc_cards += ", "; } - if (this->unavailable_sc_cards[z] >= sc_card_entry_names.size()) { + if (this->unavailable_sc_cards[z] >= unavailable_sc_card_defs.size()) { unavailable_sc_cards += std::format("{:04X} (invalid)", this->unavailable_sc_cards[z]); } else { - unavailable_sc_cards += sc_card_entry_names[this->unavailable_sc_cards[z]]; + const auto& def = unavailable_sc_card_defs[this->unavailable_sc_cards[z]]; + unavailable_sc_cards += std::format("{:04X} ({}; {:04X})", def.index, def.internal_name, def.card_id); } } unavailable_sc_cards += ']'; @@ -2130,6 +2153,134 @@ string MapDefinition::str(const CardIndex* card_index, Language language) const return phosg::join(lines, "\n"); } +phosg::JSON MapDefinition::json(Language language) const { + phosg::JSON camera_zones_json = phosg::JSON::list(); + for (size_t team_id = 0; team_id < 2; team_id++) { + phosg::JSON team_camera_zones_json = phosg::JSON::list(); + for (size_t camera_zone_id = 0; camera_zone_id < std::min(this->num_camera_zones, 10); camera_zone_id++) { + team_camera_zones_json.emplace_back(phosg::JSON::dict({ + {"Map", this->camera_zone_maps[team_id][camera_zone_id].json()}, + {"Spec", this->camera_zone_specs[team_id][camera_zone_id].json()}, + })); + } + camera_zones_json.emplace_back(std::move(team_camera_zones_json)); + } + phosg::JSON overview_specs_json = phosg::JSON::list(); + for (size_t team_id = 0; team_id < 2; team_id++) { + phosg::JSON team_overview_specs_json = phosg::JSON::list(); + for (size_t spec_id = 0; spec_id < 3; spec_id++) { + team_overview_specs_json.emplace_back(this->overview_specs[spec_id][team_id].json()); + } + overview_specs_json.emplace_back(std::move(team_overview_specs_json)); + } + + auto overlay_state_json = phosg::JSON::dict({ + {"Tiles", this->overlay_state.tiles.json()}, + {"NTETrapTileColors", this->overlay_state.trap_tile_colors_nte.json()}, + {"NTETrapCardIDs", this->overlay_state.trap_card_ids_nte.json()}, + }); + + // Note: All typos/errors here are from AIPrm.dat + static const array default_ai_names = { + "Sample_Hunter", "Glustar", "Guykild", "Inolis", "Kilia", "Kranz", "Orland", "Relmitos", "Saligun", "Silfer", + "Sample_Hunter", "Teifu", "Viviana", "Sample_Dark", "Break", "Creinu", "Endu", "Heiz", "KC", "Lura", + "memoru", "Ohgun", "Peko", "Reiz", "Rio", "Rufina", "LKnight", "Boss_Castor", "Boss_Pollux", "Sample_Dark"}; + + auto npcs_json = phosg::JSON::list(); + for (size_t npc_index = 0; npc_index < 3; npc_index++) { + auto npc = phosg::JSON::dict(); + + const auto& deck = this->npc_decks[npc_index]; + if (deck.deck_name.at(0)) { + npc.emplace("Deck", deck.json(language)); + } else { + npc.emplace("Deck", nullptr); + } + + int32_t entry_index = this->npc_ai_params_entry_index[npc_index]; + if (entry_index < 0) { + const auto& ai_params = this->npc_ai_params[npc_index]; + if (ai_params.ai_name.at(0)) { + npc.emplace("AIParams", ai_params.json(language)); + } else { + npc.emplace("AIParams", nullptr); + } + } else if (static_cast(entry_index) < default_ai_names.size()) { + npc.emplace("AIParams", default_ai_names[entry_index]); + } else { + npc.emplace("AIParams", entry_index); + } + + auto dialogue_set_json = phosg::JSON::list(); + for (size_t ds_index = 0; ds_index < this->dialogue_sets[npc_index].size(); ds_index++) { + const auto& ds = this->dialogue_sets[npc_index][ds_index]; + if (ds.when >= 0) { + dialogue_set_json.emplace_back(ds.json(language)); + } + } + npc.emplace("DialogueSet", std::move(dialogue_set_json)); + } + + auto unavailable_sc_cards_json = phosg::JSON::list(); + for (size_t z = 0; z < this->unavailable_sc_cards.size(); z++) { + uint16_t index = this->unavailable_sc_cards[z]; + if (index < unavailable_sc_card_defs.size()) { + const auto& def = unavailable_sc_card_defs[index]; + unavailable_sc_cards_json.emplace_back(phosg::JSON::dict({ + {"Index", def.index}, + {"Name", def.internal_name}, + {"CardID", def.card_id}, + })); + } else if (index != 0xFFFF) { + unavailable_sc_cards_json.emplace_back(index); + } + } + + auto reward_card_ids_json = phosg::JSON::list(); + for (size_t z = 0; z < this->reward_card_ids.size(); z++) { + if (this->reward_card_ids[z] != 0xFFFF) { + reward_card_ids_json.emplace_back(this->reward_card_ids[z].load()); + } + } + + auto entry_states_json = phosg::JSON::list(); + for (size_t z = 0; z < this->entry_states.size(); z++) { + entry_states_json.emplace_back(this->entry_states[z].json()); + } + + return phosg::JSON::dict({ + {"Tag", this->tag.load()}, + {"MapNumber", this->map_number.load()}, + {"Width", this->width}, + {"Height", this->height}, + {"EnvironmentNumber", this->environment_number}, + {"EnvironmentName", name_for_environment_number(this->environment_number)}, + {"MapTiles", this->map_tiles.json()}, + {"StartTileDefinitions", this->start_tile_definitions.json()}, + {"CameraZones", std::move(camera_zones_json)}, + {"OverviewSpecs", std::move(overview_specs_json)}, + {"OverlayState", std::move(overlay_state_json)}, + {"Rules", this->default_rules.json()}, + {"Name", this->name.decode(language)}, + {"LocationName", this->location_name.decode(language)}, + {"QuestName", this->quest_name.decode(language)}, + {"Description", this->description.decode(language)}, + {"WorldMapCoords", phosg::JSON::list({this->map_x.load(), this->map_y.load()})}, + {"NPCs", std::move(npcs_json)}, + {"BeforeMessage", this->before_message.decode(language)}, + {"AfterMessage", this->after_message.decode(language)}, + {"DispatchMessage", this->dispatch_message.decode(language)}, + {"UnavailableSCCards", std::move(unavailable_sc_cards_json)}, + {"RewardCardIDs", std::move(reward_card_ids_json)}, + {"WinLevelOverride", this->win_level_override.load()}, + {"LossLevelOverride", this->loss_level_override.load()}, + {"FieldOffset", phosg::JSON::list({this->field_offset_x.load(), this->field_offset_y.load()})}, + {"MapCategory", this->map_category}, + {"CyberBlockType", this->cyber_block_type}, + {"EntryStates", std::move(entry_states_json)}, + }); +} + MapDefinitionTrial::MapDefinitionTrial(const MapDefinition& map) : tag(map.tag), map_number(map.map_number), diff --git a/src/Episode3/DataIndexes.hh b/src/Episode3/DataIndexes.hh index 776650ec..a78bd135 100644 --- a/src/Episode3/DataIndexes.hh +++ b/src/Episode3/DataIndexes.hh @@ -1257,7 +1257,7 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests /* 58 */ phosg::JSON json(Language language) const; } __packed_ws__(NPCDeck, 0x58); - /* 1FE8 */ parray npc_decks; // Unused if name[0] == 0 + /* 1FE8 */ parray npc_decks; // Unused if deck_name[0] == 0 // These are not quite the same format as the entries in aiprm.dat. These entries are only used if the corresponding // NPC exists (if .name[0] is not 0) and if the corresponding entry in the npc_ai_params_entry_index is -1. @@ -1271,7 +1271,7 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests /* 0114 */ phosg::JSON json(Language language) const; } __packed_ws__(AIParams, 0x114); - /* 20F0 */ parray npc_ai_params; // Unused if name[0] == 0 + /* 20F0 */ parray npc_ai_params; // Unused if ai_name[0] == 0 /* 242C */ parray unknown_a7; diff --git a/src/FileContentsCache.cc b/src/FileContentsCache.cc index 85b78807..40a1235e 100644 --- a/src/FileContentsCache.cc +++ b/src/FileContentsCache.cc @@ -44,16 +44,14 @@ FileContentsCache::GetResult FileContentsCache::get_or_load(const char* name) { return this->get_or_load(string(name)); } -shared_ptr FileContentsCache::get_or_throw( - const std::string& name) { +shared_ptr FileContentsCache::get_or_throw(const std::string& name) { auto throw_fn = +[](const std::string&) -> string { throw out_of_range("file missing from cache"); }; return this->get(name, throw_fn).file; } -shared_ptr FileContentsCache::get_or_throw( - const char* name) { +shared_ptr FileContentsCache::get_or_throw(const char* name) { return this->get_or_throw(string(name)); } diff --git a/src/FileContentsCache.hh b/src/FileContentsCache.hh index 6d49dfd2..260203b1 100644 --- a/src/FileContentsCache.hh +++ b/src/FileContentsCache.hh @@ -114,9 +114,9 @@ public: ThreadSafeFileCache& operator=(ThreadSafeFileCache&&) = delete; ~ThreadSafeFileCache() = default; - // Warning: generate() is called while the lock is held for writing, so it - // will block other threads. - std::shared_ptr get(const std::string& name, std::function(const std::string&)> generate); + // generate() is called while the lock is held for writing, so it will block other threads. + std::shared_ptr get( + const std::string& name, std::function(const std::string&)> generate); private: std::shared_mutex lock; diff --git a/src/FunctionCompiler.cc b/src/FunctionCompiler.cc index a44774f0..dfe542dd 100644 --- a/src/FunctionCompiler.cc +++ b/src/FunctionCompiler.cc @@ -105,7 +105,7 @@ string CompiledFunctionCode::generate_client_command( } bool CompiledFunctionCode::is_big_endian() const { - return this->arch == Architecture::POWERPC; + return (this->arch == Architecture::POWERPC); } static unordered_map preprocess_function_code(const std::string& text) { @@ -483,8 +483,7 @@ bool FunctionCodeIndex::patch_menu_empty(uint32_t specific_version) const { std::shared_ptr FunctionCodeIndex::get_patch( const std::string& name, uint32_t specific_version) const { - return this->name_and_specific_version_to_patch_function.at( - std::format("{}-{:08X}", name, specific_version)); + return this->name_and_specific_version_to_patch_function.at(std::format("{}-{:08X}", name, specific_version)); } DOLFileIndex::DOLFileIndex(const string& directory) { diff --git a/src/GSLArchive.cc b/src/GSLArchive.cc index 6394502a..562df76c 100644 --- a/src/GSLArchive.cc +++ b/src/GSLArchive.cc @@ -39,8 +39,7 @@ void GSLArchive::load_t() { } } -GSLArchive::GSLArchive(shared_ptr data, bool big_endian) - : data(data) { +GSLArchive::GSLArchive(shared_ptr data, bool big_endian) : data(data) { if (big_endian) { this->load_t(); } else { @@ -87,8 +86,7 @@ template string GSLArchive::generate_t(const unordered_map& files) { phosg::StringWriter w; - // Make sure there's enough space for a blank header entry before any file's - // data pages begin + // Make sure there's enough space for a blank header entry before any file's data pages begin uint32_t data_start_offset = ((sizeof(GSLHeaderEntryT) * (files.size() + 1)) + 0x7FF) & (~0x7FF); uint32_t data_offset = data_start_offset; for (const auto& file : files) { diff --git a/src/GameServer.cc b/src/GameServer.cc index 3e0aaa9f..8f0d4ed5 100644 --- a/src/GameServer.cc +++ b/src/GameServer.cc @@ -21,8 +21,7 @@ using namespace std; using namespace std::placeholders; -GameServer::GameServer(shared_ptr state) - : Server(state->io_context, "[GameServer] "), state(state) {} +GameServer::GameServer(shared_ptr state) : Server(state->io_context, "[GameServer] "), state(state) {} void GameServer::listen( const std::string& name, @@ -75,8 +74,8 @@ vector> GameServer::get_clients_by_identifier(const string& i } catch (const invalid_argument&) { } - // TODO: It's kind of not great that we do a linear search here, but this is - // only used in the shell, so it should be pretty rare. + // TODO: It's kind of not great that we do a linear search here, but this is only used in the shell, so it should be + // pretty rare. vector> results; for (const auto& c : this->clients) { if (c->login && c->login->account->account_id == account_id_hex) { @@ -115,7 +114,8 @@ vector> GameServer::get_clients_by_identifier(const string& i return results; } -shared_ptr GameServer::create_client(shared_ptr listen_sock, asio::ip::tcp::socket&& client_sock) { +shared_ptr GameServer::create_client( + shared_ptr listen_sock, asio::ip::tcp::socket&& client_sock) { uint32_t addr = ipv4_addr_for_asio_addr(client_sock.remote_endpoint().address()); if (this->state->banned_ipv4_ranges->check(addr)) { if (client_sock.is_open()) { @@ -166,8 +166,7 @@ asio::awaitable GameServer::handle_client(shared_ptr c) { asio::awaitable GameServer::destroy_client(std::shared_ptr c) { this->log.info_f("Running cleanup tasks for {}", c->channel->name); - // The client may not actually be disconnected yet if an uncaught exception - // occurred in a handler task + // The client may not actually be disconnected yet if an uncaught exception occurred in a handler task c->channel->disconnect(); // Close the proxy session, if any @@ -184,9 +183,8 @@ asio::awaitable GameServer::destroy_client(std::shared_ptr c) { this->log.warning_f("Error during client disconnect cleanup: {}", e.what()); } - // Note: It's important to move the disconnect hooks out of the client here - // because the hooks could modify c->disconnect_hooks while it's being - // iterated here, which would invalidate these iterators. + // Note: It's important to move the disconnect hooks out of the client here because the hooks could modify + // c->disconnect_hooks while it's being iterated here, which would invalidate these iterators. unordered_map> hooks = std::move(c->disconnect_hooks); for (auto h_it : hooks) { try { diff --git a/src/GameServer.hh b/src/GameServer.hh index 714e87bb..a0450fd0 100644 --- a/src/GameServer.hh +++ b/src/GameServer.hh @@ -25,7 +25,8 @@ public: explicit GameServer(std::shared_ptr state); virtual ~GameServer() = default; - void listen(const std::string& name, const std::string& addr, uint16_t port, Version version, ServerBehavior initial_state); + void listen( + const std::string& name, const std::string& addr, uint16_t port, Version version, ServerBehavior initial_state); std::shared_ptr connect_channel(std::shared_ptr ch, uint16_t port, ServerBehavior initial_state); diff --git a/src/HTTPServer.cc b/src/HTTPServer.cc index 6a91cd1e..31254368 100644 --- a/src/HTTPServer.cc +++ b/src/HTTPServer.cc @@ -471,9 +471,10 @@ HTTPServer::HTTPServer(shared_ptr state) {"BattleStartTimeUsecs", ep3s->battle_start_usecs}, {"TeamEXP", phosg::JSON::list({ep3s->team_exp[0], ep3s->team_exp[1]})}, {"TeamDiceBonus", phosg::JSON::list({ep3s->team_dice_bonus[0], ep3s->team_dice_bonus[1]})}, + // TODO: Include information from these too? + // std::shared_ptr state_flags; + // std::array, 4> player_states; }); - // std::shared_ptr state_flags; - // std::array, 4> player_states; lobby_json.emplace("Episode3BattleState", std::move(battle_state_json)); } else { lobby_json.emplace("Episode3BattleState", nullptr); @@ -660,7 +661,51 @@ HTTPServer::HTTPServer(shared_ptr state) } }); - // TODO: Add /y/data/ep3/maps, /y/data/ep3/map/:map_number and /y/data/ep3/map/:map_number/raw + this->router.add(HTTPRequest::Method::GET, "/y/data/ep3/maps", [this](ArgsT&& args) -> RetT { + co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> shared_ptr { + auto ret = make_shared(phosg::JSON::dict()); + for (const auto& [map_number, map] : this->state->ep3_map_index->all_maps()) { + auto languages_json = phosg::JSON::list(); + for (const auto& vm : map->all_versions()) { + if (vm) { + languages_json.emplace_back(name_for_language(vm->language)); + } + } + auto map_json = phosg::JSON::dict({ + {"Name", map->version(Language::ENGLISH)->map->name.decode(Language::ENGLISH)}, + {"VisibilityFlags", map->visibility_flags}, + {"Languages", std::move(languages_json)}, + }); + ret->emplace(std::format("{:08X}", map_number), std::move(map_json)); + } + return ret; + }); + }); + + this->router.add(HTTPRequest::Method::GET, "/y/data/ep3/map/:map_number/:language", [this](ArgsT&& args) -> RetT { + co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> shared_ptr { + try { + auto map = this->state->ep3_map_index->map_for_id(args.get_param("map_number", true)); + auto vm = map->version(language_for_name(args.params.at("language"))); + return make_shared(vm->map->json(vm->language)); + } catch (const std::out_of_range&) { + throw HTTPError(404, "Map version does not exist"); + } + }); + }); + + this->router.add(HTTPRequest::Method::GET, "/y/data/ep3/map/:map_number/:language/raw", [this](ArgsT&& args) -> RetT { + co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> RawResponse { + try { + auto map = this->state->ep3_map_index->map_for_id(args.get_param("map_number")); + auto vm = map->version(language_for_name(args.params.at("language"))); + string data(reinterpret_cast(vm->map.get()), sizeof(Episode3::MapDefinition)); + return RawResponse{.content_type = "application/octet-stream", .data = std::move(data)}; + } catch (const std::out_of_range&) { + throw HTTPError(404, "Map version does not exist"); + } + }); + }); this->router.add(HTTPRequest::Method::GET, "/y/data/common-tables", [this](ArgsT&&) -> RetT { auto ret = make_shared(phosg::JSON::list()); @@ -731,8 +776,8 @@ asio::awaitable HTTPServer::send_rare_drop_notification(shared_ptrrare_drop_subscribers.empty()) { string data = message->serialize(); - // Make a copy of the rare drop subscribers set, so we can guarantee that - // the client objects are all valid until this coroutine returns + // Make a copy of the rare drop subscribers set, so we can guarantee that the client objects are all valid until + // this coroutine returns unordered_set> subscribers = this->rare_drop_subscribers; size_t expected_results = subscribers.size(); diff --git a/src/HTTPServer.hh b/src/HTTPServer.hh index 9ba8480e..388da0be 100644 --- a/src/HTTPServer.hh +++ b/src/HTTPServer.hh @@ -32,6 +32,7 @@ protected: void require_GET(const HTTPRequest& req); phosg::JSON require_POST(const HTTPRequest& req); - virtual asio::awaitable> handle_request(std::shared_ptr c, HTTPRequest&& req); + virtual asio::awaitable> handle_request( + std::shared_ptr c, HTTPRequest&& req); virtual asio::awaitable destroy_client(std::shared_ptr c); }; diff --git a/src/IPFrameInfo.cc b/src/IPFrameInfo.cc index 39d77197..74d7e5ba 100644 --- a/src/IPFrameInfo.cc +++ b/src/IPFrameInfo.cc @@ -7,8 +7,7 @@ using namespace std; static inline uint16_t collapse_checksum(uint32_t sum) { - // It's impossible for this to be necessary more than twice: the first - // addition can carry out at most a single bit. + // It's impossible for this to be necessary more than twice: the first addition can carry out at most a single bit. sum = (sum & 0xFFFF) + (sum >> 16); return (sum & 0xFFFF) + (sum >> 16); } diff --git a/src/IPStackSimulator.cc b/src/IPStackSimulator.cc index 295f286b..492f849e 100644 --- a/src/IPStackSimulator.cc +++ b/src/IPStackSimulator.cc @@ -78,9 +78,8 @@ static string escape_hdlc_frame(const string& data, uint32_t escape_control_char return escape_hdlc_frame(data.data(), data.size(), escape_control_character_flags); } -// Note: these functions exist because seq nums are allowed to wrap around the -// 32-bit integer space by design. We have to do the subtraction before the -// comparison to allow integer overflow to occur if needed. +// Note: these functions exist because seq nums are allowed to wrap around the 32-bit integer space by design. We have +// to do the subtraction before the comparison to allow integer overflow to occur if needed. static inline bool seq_num_less(uint32_t a, uint32_t b) { return (a - b) & 0x80000000; @@ -200,9 +199,8 @@ void IPSSChannel::disconnect() { } void IPSSChannel::add_inbound_data(const void* data, size_t size) { - // If recv_buf is not null, there is a coroutine waiting to receive data, and - // inbound_data must be empty. Copy the data directly to the waiting - // coroutine's buffer, and put the rest in this->inbound_data if needed. + // If recv_buf is not null, there is a coroutine waiting to receive data, and inbound_data must be empty. Copy the + // data directly to the waiting coroutine's buffer, and put the rest in this->inbound_data if needed. if (this->recv_buf) { size_t direct_size = min(this->recv_buf_size, size); memcpy(this->recv_buf, data, direct_size); @@ -212,8 +210,7 @@ void IPSSChannel::add_inbound_data(const void* data, size_t size) { this->recv_buf = this->recv_buf_size ? (reinterpret_cast(this->recv_buf) + direct_size) : nullptr; } - // If there is still data left after the above, add it to the pending inbound - // data buffer + // If there is still data left after the above, add it to the pending inbound data buffer if (size > 0) { this->inbound_data.emplace_back(reinterpret_cast(data), size); } @@ -239,10 +236,9 @@ void IPSSChannel::send_raw(string&& data) { conn->outbound_data_bytes += data.size(); conn->outbound_data.emplace_back(std::move(data)); - // If we're already waiting for an ACK from the remote client, don't send - // another PSH right now - we will either send another PSH when we receive - // the ACK or will retry sending the PSH soon (which will then include the - // new data, if it's within the MTU from the last acked sequence number). + // If we're already waiting for an ACK from the remote client, don't send another PSH right now - we will either send + // another PSH when we receive the ACK or will retry sending the PSH soon (which will then include the new data, if + // it's within the MTU from the last acked sequence number). if (!conn->awaiting_ack) { sim->schedule_send_pending_push_frame(conn, 0); } @@ -270,8 +266,7 @@ asio::awaitable IPSSChannel::recv_raw(void* data, size_t size) { } } - // If there's still more data to read, block until it's available - // (add_inbound_data is responsible for waking this coroutine) + // If there's still more data to read, block until it's available (add_inbound_data will wake this coroutine) if (size > 0) { this->recv_buf = data; this->recv_buf_size = size; @@ -304,9 +299,8 @@ void IPStackSimulator::listen(const std::string& name, const string& addr, int p } uint32_t IPStackSimulator::connect_address_for_remote_address(uint32_t remote_addr) { - // Use an address not on the same subnet as the client, so that PSO Plus and - // Episode III will think they're talking to a remote network and won't - // reject the connection. + // Use an address not on the same subnet as the client, so that PSO Plus and Episode III will think they're talking + // to a remote network and won't reject the connection. return ((remote_addr & 0xFF000000) == 0x23000000) ? 0x24242424 : 0x23232323; } @@ -555,10 +549,9 @@ asio::awaitable IPStackSimulator::on_client_lcp_frame(shared_ptr IPStackSimulator::on_client_ipcp_frame(shared_ptr IPStackSimulator::on_client_ipcp_frame(shared_ptripv4_addr = remote_ip; - // As with LCP, we technically should implement the state machine, but I - // continue to be lazy. + // As with LCP, we technically should implement the state machine, but I continue to be lazy. phosg::StringWriter opts_w; opts_w.put_u8(0x03); // IP address opts_w.put_u8(0x06); @@ -806,15 +797,15 @@ asio::awaitable IPStackSimulator::on_client_arp_frame(shared_ptr(fi.payload); w.write(this->host_mac_address_bytes.data(), 6); w.write(payload_bytes + 16, 4); @@ -826,9 +817,7 @@ asio::awaitable IPStackSimulator::on_client_arp_frame(shared_ptr IPStackSimulator::on_client_udp_frame(shared_ptr c, const FrameInfo& fi) { // We only implement DHCP and newserv's DNS server here. - // Every received UDP packet will elicit exactly one UDP response from - // newserv, so we prepare the response headers in advance - + // Every received UDP packet will elicit exactly one UDP response from newserv, so we prepare the headers in advance IPv4Header r_ipv4; r_ipv4.version_ihl = 0x45; r_ipv4.tos = 0; @@ -888,8 +877,8 @@ asio::awaitable IPStackSimulator::on_client_udp_frame(shared_ptrmac_addr = dhcp.client_hardware_address.data(); c->ipv4_addr = 0x0A000105; // 10.0.1.5 - // In this case, the client doesn't know its IPv4 address or ours yet, - // so we overwrite the existing fields with the appropriate addresses. + // In this case, the client doesn't know its IPv4 address or ours yet, so we overwrite the existing fields with + // the appropriate addresses. r_ipv4.src_addr = 0x0A000101; // 10.0.1.1 r_ipv4.dest_addr = c->ipv4_addr; @@ -1005,8 +994,8 @@ asio::awaitable IPStackSimulator::on_client_tcp_frame(shared_ptrflags & TCPHeader::Flag::SYN) { - // We never make connections back to the client, so we should never receive - // a SYN+ACK. Essentially, no other flags should be set in any received SYN. + // We never make connections back to the client, so we should never receive a SYN+ACK. Essentially, no other flags + // should be set in any received SYN. if ((fi.tcp->flags & 0x0FFF) != TCPHeader::Flag::SYN) { throw runtime_error("TCP SYN contains extra flags"); } @@ -1081,8 +1070,7 @@ asio::awaitable IPStackSimulator::on_client_tcp_frame(shared_ptrawaiting_first_ack) { throw logic_error("SYN received on already-open connection after initial phase"); } - // TODO: We should check the syn/ack numbers here instead of just assuming - // they're correct + // TODO: We should check the syn/ack numbers here instead of just assuming they're correct conn_str = this->str_for_tcp_connection(c, conn); this->log.debug_f("Client resent SYN for TCP connection {}", conn_str); } @@ -1093,8 +1081,7 @@ asio::awaitable IPStackSimulator::on_client_tcp_frame(shared_ptracked_server_seq, conn->next_client_seq); } else { - // This frame isn't a SYN, so a connection object should already exist; - // ignore the frame if there's no connection + // This frame isn't a SYN, so a connection object should already exist; ignore the frame if there's no connection uint64_t key = this->tcp_conn_key_for_client_frame(fi); auto conn_it = c->tcp_connections.find(key); if (conn_it == c->tcp_connections.end()) { @@ -1158,24 +1145,20 @@ asio::awaitable IPStackSimulator::on_client_tcp_frame(shared_ptrserver_channel.reset(); } - // TODO: Are we supposed to send a response to an RST? Here we do, and the - // client probably just ignores it anyway + // TODO: Are we supposed to send a response to an RST? Here we do, and the client probably just ignores it anyway co_await this->send_tcp_frame(c, conn, fi.tcp->flags & (TCPHeader::Flag::RST | TCPHeader::Flag::FIN)); - // Delete the connection object. The unique_ptr destructor flushes the - // bufferevent, and thereby sends an EOF to the server's end. + // Delete the connection object. The unique_ptr destructor flushes the bufferevent, and thereby sends an EOF to + // the server's end. c->tcp_connections.erase(key); conn_valid = false; } else if (fi.payload_size != 0) { - // Note: The PSH flag isn't required to be set on all packets that - // contain data. The PSH flag just means "tell the application that data - // is available", so some senders only set the PSH flag on the last frame - // of a large segment of data, since the application wouldn't be able to - // process the segment until all of it is available. newserv can handle - // incomplete commands, so we just ignore the PSH flag and forward any - // data to the server immediately (hence the lack of a flag check in the - // above condition). + // Note: The PSH flag isn't required to be set on all packets that contain data. The PSH flag just means "tell + // the application that data is available", so some senders only set the PSH flag on the last frame of a large + // segment of data, since the application wouldn't be able to process the segment until all of it is available. + // newserv can handle incomplete commands, so we just ignore the PSH flag and forward any data to the server + // immediately (hence the lack of a flag check in the above condition). string conn_str = this->log.should_log(phosg::LogLevel::L_WARNING) ? this->str_for_tcp_connection(c, conn) @@ -1186,8 +1169,8 @@ asio::awaitable IPStackSimulator::on_client_tcp_frame(shared_ptrseq_num, conn->next_client_seq)) { - // If the frame overlaps an existing boundary, we'll accept some of the - // data; otherwise we'll ignore it entirely (but still send an ACK) + // If the frame overlaps an existing boundary, we'll accept some of the data; otherwise we'll ignore it + // entirely (but still send an ACK) uint32_t end_seq = fi.tcp->seq_num + fi.payload_size; if (seq_num_less_or_equal(end_seq, conn->next_client_seq)) { // Fully "in the past" payload_skip_bytes = fi.payload_size; @@ -1196,9 +1179,8 @@ asio::awaitable IPStackSimulator::on_client_tcp_frame(shared_ptrlog.warning_f( "Client sent out-of-order sequence number (expected {:08X}, received {:08X}, 0x{:X} data bytes)", conn->next_client_seq, fi.tcp->seq_num, fi.payload_size); @@ -1288,10 +1270,8 @@ asio::awaitable IPStackSimulator::send_pending_push_frame( size_t bytes_to_send = min(conn->outbound_data_bytes, conn->next_push_max_frame_size); if (c->protocol == VirtualNetworkProtocol::HDLC_TAPSERVER) { - // There is a bug in Dolphin's modem implementation (which I wrote, so it's - // my fault) that causes commands to be dropped when too much data is sent - // at once. To work around this, we only send up to 200 bytes in each push - // frame. + // There is a bug in Dolphin's modem implementation (which I wrote, so it's my fault) that causes commands to be + // dropped when too much data is sent. To work around this, we only send up to 200 bytes in each push frame. bytes_to_send = min(bytes_to_send, 200); } @@ -1300,23 +1280,20 @@ asio::awaitable IPStackSimulator::send_pending_push_frame( conn->linearize_outbound_data(bytes_to_send); if (conn->outbound_data.empty() || conn->outbound_data.front().size() < bytes_to_send) { - // This should never happen because bytes_to_send should always be less - // than or equal to conn->outbound_data_bytes, which itself should be equal - // to the number of bytes that can be linearized + // This should never happen because bytes_to_send should always be less than or equal to conn->outbound_data_bytes, + // which itself should be equal to the number of bytes that can be linearized throw logic_error("failed to linearize enough bytes before sending TCP PSH"); } co_await this->send_tcp_frame(c, conn, TCPHeader::Flag::PSH, conn->outbound_data.front().data(), bytes_to_send); conn->awaiting_ack = true; - // Schedule the timer for sending another PSH, in case the client doesn't - // respond quickly enough + // Schedule the timer for sending another PSH, in case the client doesn't respond quickly enough this->schedule_send_pending_push_frame(conn, conn->resend_push_usecs); - // If the client isn't responding to our PSHes, back off exponentially up to - // a limit of 5 seconds between PSH frames. This window is reset when - // acked_server_seq changes (that is, when the client has acknowledged any new - // data). It seems some situations cause GameCube clients to drop packets more - // often; to alleviate this, we also try to resend less data. + // If the client isn't responding to our PSHes, back off exponentially up to a limit of 5 seconds between PSH frames. + // This window is reset when acked_server_seq changes (that is, when the client has acknowledged any new data). It + // seems some situations cause GameCube clients to drop packets more often; to alleviate this, we also try to resend + // less data. conn->resend_push_usecs *= 2; if (conn->resend_push_usecs > 5000000) { conn->resend_push_usecs = 5000000; @@ -1401,9 +1378,8 @@ asio::awaitable IPStackSimulator::open_server_connection( asio::awaitable IPStackSimulator::close_tcp_connection( shared_ptr c, shared_ptr conn) { - // Send an RST to the client. This is kind of rude (we really should use FIN) - // but the PSO network stack always sends an RST to us when disconnecting, so - // whatever + // Send an RST to the client. This is kind of rude (we really should use FIN) but the PSO network stack always sends + // an RST to us when disconnecting, so whatever co_await this->send_tcp_frame(c, conn, TCPHeader::Flag::RST); // Delete the connection object diff --git a/src/ImageEncoder.hh b/src/ImageEncoder.hh index 23b2347e..366b9941 100644 --- a/src/ImageEncoder.hh +++ b/src/ImageEncoder.hh @@ -20,7 +20,10 @@ enum class GVRDataFormat : uint8_t { }; std::string encode_gvm( - const phosg::ImageRGBA8888N& img, GVRDataFormat data_format, const std::string& internal_name, uint32_t global_index); + const phosg::ImageRGBA8888N& img, + GVRDataFormat data_format, + const std::string& internal_name, + uint32_t global_index); phosg::ImageRGB888 decode_fon(const std::string& data, size_t width); std::string encode_fon(const phosg::ImageRGB888& img); diff --git a/src/IntegralExpression.cc b/src/IntegralExpression.cc index 44761971..9c2edca0 100644 --- a/src/IntegralExpression.cc +++ b/src/IntegralExpression.cc @@ -164,8 +164,7 @@ string IntegralExpression::UnaryOperatorNode::str() const { } } -IntegralExpression::FlagLookupNode::FlagLookupNode(uint16_t flag_index) - : flag_index(flag_index) {} +IntegralExpression::FlagLookupNode::FlagLookupNode(uint16_t flag_index) : flag_index(flag_index) {} bool IntegralExpression::FlagLookupNode::operator==(const Node& other) const { try { @@ -187,10 +186,8 @@ string IntegralExpression::FlagLookupNode::str() const { return std::format("F_{:04X}", this->flag_index); } -IntegralExpression::ChallengeCompletionLookupNode::ChallengeCompletionLookupNode( - Episode episode, uint8_t stage_index) - : episode(episode), - stage_index(stage_index) {} +IntegralExpression::ChallengeCompletionLookupNode::ChallengeCompletionLookupNode(Episode episode, uint8_t stage_index) + : episode(episode), stage_index(stage_index) {} bool IntegralExpression::ChallengeCompletionLookupNode::operator==(const Node& other) const { try { @@ -217,8 +214,7 @@ string IntegralExpression::ChallengeCompletionLookupNode::str() const { return std::format("CC_{}_{}", abbreviation_for_episode(this->episode), static_cast(this->stage_index + 1)); } -IntegralExpression::TeamRewardLookupNode::TeamRewardLookupNode(const string& reward_name) - : reward_name(reward_name) {} +IntegralExpression::TeamRewardLookupNode::TeamRewardLookupNode(const string& reward_name) : reward_name(reward_name) {} bool IntegralExpression::TeamRewardLookupNode::operator==(const Node& other) const { try { @@ -310,9 +306,8 @@ unique_ptr IntegralExpression::parse_expr(string text = text.substr(0, text.size() - 1); } if (text.at(0) == '(' && text.at(text.size() - 1) == ')') { - // It doesn't suffice to just check the first ant last characters, since - // text could be like "(a) && (b)". Instead, we ignore the first and last - // characters, and don't strip anything if the internal parentheses are + // It doesn't suffice to just check the first and last characters, since text could be like "(a) && (b)". + // Instead, we ignore the first and last characters, and don't strip anything if the internal parentheses are // unbalanced. size_t paren_level = 1; for (size_t z = 1; z < text.size() - 1; z++) { @@ -363,10 +358,8 @@ unique_ptr IntegralExpression::parse_expr(string } if (!paren_level) { for (const auto& oper : operators) { - // Awful hack (because I'm too lazy to add a tokenization step): if - // the operator is followed or preceded by another copy of itself, - // don't match it (this prevents us from matching & when the token is - // actually &&) + // Awful hack (because I'm too lazy to add a tokenization step): if the operator is followed or preceded by + // another copy of itself, don't match it (this prevents us from matching & when the token is actually &&) if ((text.size() > z + oper.first.size()) && ((z < oper.first.size()) || (text.compare(z - oper.first.size(), oper.first.size(), oper.first) != 0)) && (text.compare(z, oper.first.size(), oper.first) == 0) && diff --git a/src/Map.hh b/src/Map.hh index bbaf2a16..e48c54b7 100644 --- a/src/Map.hh +++ b/src/Map.hh @@ -507,7 +507,7 @@ protected: // identifies the entity on all PSO versions. (These are the IDs which newserv formats as K-XXX, E-XXX, and W-XXX, // though they are offset as needed for floors beyond the first.) // There must not be any random enemy sections in any MapFile passed to SuperMap; to resolve them, -// materialize_random_sections must be called on all MapFiles first. This generally only is needed in Challenge mode. +// materialize_random_sections must be called on all MapFiles first. This is generally only needed in Challenge mode. class SuperMap { public: diff --git a/src/StaticGameData.cc b/src/StaticGameData.cc index 6fa798e9..8b45ae9e 100644 --- a/src/StaticGameData.cc +++ b/src/StaticGameData.cc @@ -566,6 +566,38 @@ Language language_for_char(char language_char) { } } +Language language_for_name(const string& name) { + if (name.size() == 1) { + return language_for_char(name[0]); + } + string lower_name = phosg::tolower(name); + if (lower_name == "japanese") { + return Language::JAPANESE; + } + if (lower_name == "english") { + return Language::ENGLISH; + } + if (lower_name == "german") { + return Language::GERMAN; + } + if (lower_name == "french") { + return Language::FRENCH; + } + if (lower_name == "spanish") { + return Language::SPANISH; + } + if (lower_name == "simplified chinese") { + return Language::SIMPLIFIED_CHINESE; + } + if (lower_name == "traditional chinese") { + return Language::TRADITIONAL_CHINESE; + } + if (lower_name == "korean") { + return Language::KOREAN; + } + throw runtime_error("unknown language"); +} + const vector tech_id_to_name = { "foie", "gifoie", "rafoie", "barta", "gibarta", "rabarta", diff --git a/src/StaticGameData.hh b/src/StaticGameData.hh index fc431f92..640d9558 100644 --- a/src/StaticGameData.hh +++ b/src/StaticGameData.hh @@ -91,6 +91,7 @@ char abbreviation_for_difficulty(Difficulty difficulty); const char* name_for_language(Language language); char char_for_language(Language language); Language language_for_char(char language_char); +Language language_for_name(const std::string& name); extern const std::vector name_for_mag_color; extern const std::unordered_map mag_color_for_name; diff --git a/src/Text.hh b/src/Text.hh index de6fab10..6f1be767 100644 --- a/src/Text.hh +++ b/src/Text.hh @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -303,6 +304,20 @@ struct parray { } return true; } + + phosg::JSON json() const { + auto ret = phosg::JSON::list(); + for (size_t z = 0; z < Count; z++) { + if constexpr (requires(ItemT x) { x.json(); }) { + ret.emplace_back(this->items[z].json()); + } else if constexpr (requires(ItemT x) { x.load(); }) { + ret.emplace_back(this->items[z].load()); + } else { + ret.emplace_back(this->items[z]); + } + } + return ret; + } } __attribute__((packed)); template