diff --git a/CMakeLists.txt b/CMakeLists.txt index ec48b544..6a0a55d9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,6 +47,7 @@ set(SOURCES src/CatSession.cc src/Channel.cc src/ChatCommands.cc + src/ChoiceSearch.cc src/Client.cc src/CommonItemSet.cc src/Compression.cc diff --git a/TODO.md b/TODO.md index 1f8afbd4..76ed0113 100644 --- a/TODO.md +++ b/TODO.md @@ -2,7 +2,6 @@ - Find a way to silence audio in RunDOL.s - Encapsulate BB server-side random state and make replays deterministic -- Implement choice search - Write a simple status API - Implement per-game logging - Build an exception-handling abstraction in ChatCommands that shows formatted error messages in all cases diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index f5495126..3812910e 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -112,7 +112,7 @@ static void server_command_lobby_info(shared_ptr c, const std::string&) } else { lines.emplace_back(string_printf("$C6%08X$C7 L$C6%d-%d$C7", l->lobby_id, l->min_level + 1, l->max_level + 1)); } - lines.emplace_back(string_printf("$C7Section ID: $C6%s$C7", name_for_section_id(l->section_id).c_str())); + lines.emplace_back(string_printf("$C7Section ID: $C6%s$C7", name_for_section_id(l->section_id))); if (l->check_flag(Lobby::Flag::DROPS_ENABLED)) { if (l->item_creator) { @@ -751,8 +751,7 @@ static void server_command_secid(shared_ptr c, const std::string& args) send_text_message(c, "$C6Invalid section ID"); } else { c->config.override_section_id = new_secid; - string name = name_for_section_id(new_secid); - send_text_message_printf(c, "$C6Override section ID\nset to %s", name.c_str()); + send_text_message_printf(c, "$C6Override section ID\nset to %s", name_for_section_id(new_secid)); } } } @@ -768,8 +767,7 @@ static void proxy_command_secid(shared_ptr ses, cons send_text_message(ses->client_channel, "$C6Invalid section ID"); } else { ses->config.override_section_id = new_secid; - string name = name_for_section_id(new_secid); - send_text_message(ses->client_channel, "$C6Override section ID\nset to " + name); + send_text_message_printf(ses->client_channel, "$C6Override section ID\nset to %s", name_for_section_id(new_secid)); } } } diff --git a/src/ChoiceSearch.cc b/src/ChoiceSearch.cc new file mode 100644 index 00000000..c69728c1 --- /dev/null +++ b/src/ChoiceSearch.cc @@ -0,0 +1,149 @@ +#include "ChoiceSearch.hh" + +#include +#include + +#include "Client.hh" + +using namespace std; + +const vector CHOICE_SEARCH_CATEGORIES({ + ChoiceSearchCategory{ + .id = 0x0001, + .name = "Level", + .choices = { + {0x0000, "Any"}, + {0x0001, "Own level +/- 5"}, + {0x0002, "Level 1-10"}, + {0x0003, "Level 11-20"}, + {0x0004, "Level 21-40"}, + {0x0005, "Level 41-60"}, + {0x0006, "Level 61-80"}, + {0x0007, "Level 81-100"}, + {0x0008, "Level 101-120"}, + {0x0009, "Level 121-160"}, + {0x000A, "Level 161-200"}, + }, + .client_matches = +[](shared_ptr searcher_c, shared_ptr target_c, uint16_t choice_id) -> bool { + if (choice_id == 0x0000) { + return true; + } + uint32_t target_level = target_c->game_data.character()->disp.stats.level + 1; + switch (choice_id) { + case 0x0001: + return (labs(static_cast(target_level - searcher_c->game_data.character()->disp.stats.level)) <= 5); + case 0x0002: + return (target_level <= 10); + case 0x0003: + return (target_level > 10) && (target_level <= 20); + case 0x0004: + return (target_level > 20) && (target_level <= 40); + case 0x0005: + return (target_level > 40) && (target_level <= 60); + case 0x0006: + return (target_level > 60) && (target_level <= 80); + case 0x0007: + return (target_level > 80) && (target_level <= 100); + case 0x0008: + return (target_level > 100) && (target_level <= 120); + case 0x0009: + return (target_level > 120) && (target_level <= 160); + case 0x000A: + return (target_level > 160) && (target_level <= 200); + } + return false; + }, + }, + ChoiceSearchCategory{ + .id = 0x0002, + .name = "Class", + .choices = { + {0x0000, "Any"}, + {0x0010, "Hunter"}, + {0x0001, "HUmar"}, + {0x0002, "HUnewearl"}, + {0x0003, "HUcast"}, + {0x000A, "HUcaseal"}, + {0x0011, "Ranger"}, + {0x0004, "RAmar"}, + {0x000C, "RAmarl"}, + {0x0005, "RAcast"}, + {0x0006, "RAcaseal"}, + {0x0012, "Force"}, + {0x000B, "FOmar"}, + {0x0007, "FOmarl"}, + {0x0008, "FOnewm"}, + {0x0009, "FOnewearl"}, + }, + .client_matches = +[](shared_ptr, shared_ptr target_c, uint16_t choice_id) -> bool { + switch (choice_id) { + case 0x0000: + return true; + case 0x0010: + return target_c->game_data.character()->disp.visual.class_flags & 0x20; + case 0x0011: + return target_c->game_data.character()->disp.visual.class_flags & 0x40; + case 0x0012: + return target_c->game_data.character()->disp.visual.class_flags & 0x80; + default: + return ((choice_id - 1) == target_c->game_data.character()->disp.visual.char_class); + } + }, + }, + ChoiceSearchCategory{ + .id = 0x0003, + .name = "Platform", + .choices = { + {0x0000, "Any"}, + {0x0001, "DC betas"}, + {0x0002, "DC V1"}, + {0x0003, "DC V2 / PC"}, + {0x0004, "GC / Xbox Episodes 1&2"}, + {0x0005, "GC Episode 3"}, + {0x0006, "BB"}, + }, + .client_matches = +[](shared_ptr, shared_ptr target_c, uint16_t choice_id) -> bool { + if (choice_id == 0x0000) { + return true; + } + switch (target_c->version()) { + case Version::DC_NTE: + case Version::DC_V1_11_2000_PROTOTYPE: + return (choice_id == 0x0001); + case Version::DC_V1: + return (choice_id == 0x0002); + case Version::DC_V2: + case Version::PC_V2: + return (choice_id == 0x0003); + case Version::GC_NTE: + case Version::GC_V3: + case Version::XB_V3: + return (choice_id == 0x0004); + case Version::GC_EP3_TRIAL_EDITION: + case Version::GC_EP3: + return (choice_id == 0x0005); + case Version::BB_V4: + return (choice_id == 0x0006); + default: + return false; + } + }, + }, + ChoiceSearchCategory{ + .id = 0x0204, + .name = "Game mode", + .choices = { + {0x0000, "Any"}, + {0x0001, "Normal"}, + {0x0002, "Hard"}, + {0x0003, "Very Hard"}, + {0x0004, "Ultimate"}, + {0x0005, "Battle"}, + {0x0006, "Challenge"}, + }, + .client_matches = +[](shared_ptr, shared_ptr target_c, uint16_t choice_id) -> bool { + uint16_t target_choice_id = target_c->game_data.character()->choice_search_config.get_setting(0x0204); + return (choice_id == 0) || (target_choice_id == 0) || (choice_id == target_choice_id); + }, + }, +}); diff --git a/src/ChoiceSearch.hh b/src/ChoiceSearch.hh new file mode 100644 index 00000000..e6a390b8 --- /dev/null +++ b/src/ChoiceSearch.hh @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "Text.hh" + +struct Client; + +struct ChoiceSearchConfig { + le_uint32_t disabled = 1; // 0 = enabled, 1 = disabled. Unused in command C3 + struct Entry { + le_uint16_t parent_choice_id = 0; + le_uint16_t choice_id = 0; + } __attribute__((packed)); + parray entries; + + int32_t get_setting(uint16_t parent_choice_id) const { + for (size_t z = 0; z < this->entries.size(); z++) { + if (this->entries[z].parent_choice_id == parent_choice_id) { + return this->entries[z].choice_id; + } + } + return -1; + } +} __attribute__((packed)); + +struct ChoiceSearchCategory { + struct Choice { + uint16_t id; + const char* name; + }; + + uint16_t id; + const char* name; + std::vector choices; + std::function searcher_c, std::shared_ptr target_c, uint16_t choice_id)> client_matches; +}; + +extern const std::vector CHOICE_SEARCH_CATEGORIES; diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 8f6e1207..a917b1cf 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -605,9 +605,10 @@ struct SC_MeetUserExtension { le_uint32_t menu_id = 0; le_uint32_t item_id = 0; } __packed__; - parray lobby_refs; - le_uint32_t unknown_a2 = 0; - pstring player_name; + /* 00 */ parray lobby_refs; + /* 40 */ le_uint32_t unknown_a2 = 0; + /* 44 */ pstring player_name; + /* 64 (or 84 on UTF16 versions) */ } __packed__; struct S_LegacyJoinGame_PC_0E { @@ -1103,7 +1104,7 @@ struct C_CharacterData_DCv2_61_98 { /* 0000 */ PlayerInventory inventory; /* 034C */ PlayerDispDataDCPCV3 disp; /* 041C */ PlayerRecordsEntry_DC records; - /* 04D8 */ ChoiceSearchConfig choice_search_config; + /* 04D8 */ ChoiceSearchConfig choice_search_config; /* 04F0 */ } __attribute__((packed)); @@ -1111,7 +1112,7 @@ struct C_CharacterData_PC_61_98 { /* 0000 */ PlayerInventory inventory; /* 034C */ PlayerDispDataDCPCV3 disp; /* 041C */ PlayerRecordsEntry_PC records; - /* 0510 */ ChoiceSearchConfig choice_search_config; + /* 0510 */ ChoiceSearchConfig choice_search_config; /* 0528 */ parray blocked_senders; /* 05A0 */ le_uint32_t auto_reply_enabled = 0; // The auto-reply message can be up to 0x200 characters. If it's shorter than @@ -1124,7 +1125,7 @@ struct C_CharacterData_V3_61_98 { /* 0000 */ PlayerInventory inventory; /* 034C */ PlayerDispDataDCPCV3 disp; /* 041C */ PlayerRecordsEntry_V3 records; - /* 0538 */ ChoiceSearchConfig choice_search_config; + /* 0538 */ ChoiceSearchConfig choice_search_config; /* 0550 */ pstring info_board; /* 05FC */ parray blocked_senders; /* 0674 */ le_uint32_t auto_reply_enabled = 0; @@ -1138,7 +1139,7 @@ struct C_CharacterData_GC_Ep3_61_98 { /* 0000 */ PlayerInventory inventory; /* 034C */ PlayerDispDataDCPCV3 disp; /* 041C */ PlayerRecordsEntry_V3 records; - /* 0538 */ ChoiceSearchConfig choice_search_config; + /* 0538 */ ChoiceSearchConfig choice_search_config; /* 0550 */ pstring info_board; /* 05FC */ parray blocked_senders; /* 0674 */ le_uint32_t auto_reply_enabled = 0; @@ -1151,7 +1152,7 @@ struct C_CharacterData_BB_61_98 { /* 0000 */ PlayerInventory inventory; /* 034C */ PlayerDispDataBB disp; /* 04DC */ PlayerRecordsEntry_BB records; - /* 0638 */ ChoiceSearchConfig choice_search_config; + /* 0638 */ ChoiceSearchConfig choice_search_config; /* 0650 */ pstring info_board; /* 07A8 */ parray blocked_senders; /* 0820 */ le_uint32_t auto_reply_enabled = 0; @@ -2359,19 +2360,17 @@ struct S_TournamentMatchInformation_GC_Ep3_BB { // Internal name: RcvChoiceList // Command is a list of these; header.flag is the entry count (incl. top-level). -template +template struct S_ChoiceSearchEntry { // Category IDs are nonzero; if the high byte of the ID is nonzero then the // category can be set by the user at any time; otherwise it can't. - ItemIDT parent_category_id = 0; // 0 for top-level categories - ItemIDT category_id = 0; + le_uint16_t parent_choice_id = 0; // 0 for top-level categories + le_uint16_t choice_id = 0; pstring text; } __packed__; -struct S_ChoiceSearchEntry_DC_C0 : S_ChoiceSearchEntry { +struct S_ChoiceSearchEntry_DC_V3_C0 : S_ChoiceSearchEntry { } __packed__; -struct S_ChoiceSearchEntry_V3_C0 : S_ChoiceSearchEntry { -} __packed__; -struct S_ChoiceSearchEntry_PC_BB_C0 : S_ChoiceSearchEntry { +struct S_ChoiceSearchEntry_PC_BB_C0 : S_ChoiceSearchEntry { } __packed__; // Top-level categories are things like "Level", "Class", etc. @@ -2425,12 +2424,7 @@ struct C_CreateGame_BB_C1 : C_CreateGame { // C2 (C->S): Set choice search parameters (DCv2 and later versions) // Internal name: PutChoiceList // Server does not respond. -// The ChoiceSearchConfig structure is defined in PlayerSubordinates.hh. - -struct C_ChoiceSearchSelections_DC_C2_C3 : ChoiceSearchConfig { -} __packed__; -struct C_ChoiceSearchSelections_PC_V3_BB_C2_C3 : ChoiceSearchConfig { -} __packed__; +// Contents is a ChoiceSearchConfig, which is defined in PlayerSubordinates.hh. // C3 (C->S): Execute choice search (DCv2 and later versions) // Internal name: SndChoiceSeq @@ -2441,22 +2435,25 @@ struct C_ChoiceSearchSelections_PC_V3_BB_C2_C3 : ChoiceSearchConfig // Internal name: RcvChoiceAns // Command is a list of these; header.flag is the entry count -struct S_ChoiceSearchResultEntry_V3_C4 { +template +struct S_ChoiceSearchResultEntry_C4 { le_uint32_t guild_card_number = 0; - pstring name; // No language marker, as usual on V3 - pstring info_string; // Usually something like " Lvl " + pstring name; + pstring info_string; // Usually something like " Lvl " // Format is stricter here; this is "LOBBYNAME,BLOCKNUM,SHIPNAME" // If target is in game, for example, "Game Name,BLOCK01,Alexandria" // If target is in lobby, for example, "BLOCK01-1,BLOCK01,Alexandria" - pstring locator_string; - // Server IP and port for "meet user" option - le_uint32_t server_ip = 0; - le_uint16_t server_port = 0; - le_uint16_t unused1 = 0; - le_uint32_t menu_id = 0; - le_uint32_t lobby_id = 0; // These two are guesses - le_uint32_t game_id = 0; // Zero if target is in a lobby rather than a game - parray unused2; + pstring location_string; + HeaderT reconnect_command_header; // Ignored by the client + S_Reconnect_19 reconnect_command; + SC_MeetUserExtension meet_user; +} __packed__; + +struct S_ChoiceSearchResultEntry_DC_V3_C4 : S_ChoiceSearchResultEntry_C4 { +} __packed__; +struct S_ChoiceSearchResultEntry_PC_C4 : S_ChoiceSearchResultEntry_C4 { +} __packed__; +struct S_ChoiceSearchResultEntry_BB_C4 : S_ChoiceSearchResultEntry_C4 { } __packed__; // C5 (S->C): Player records update (DCv2 and later versions) diff --git a/src/IPStackSimulator.hh b/src/IPStackSimulator.hh index 55f16a44..2456d362 100644 --- a/src/IPStackSimulator.hh +++ b/src/IPStackSimulator.hh @@ -1,3 +1,5 @@ +#pragma once + #include #include diff --git a/src/PlayerSubordinates.hh b/src/PlayerSubordinates.hh index 5905977f..22d64e37 100644 --- a/src/PlayerSubordinates.hh +++ b/src/PlayerSubordinates.hh @@ -10,6 +10,7 @@ #include #include +#include "ChoiceSearch.hh" #include "FileContentsCache.hh" #include "ItemData.hh" #include "LevelTable.hh" @@ -453,17 +454,6 @@ struct PlayerRecords_Battle { /* 18 */ } __attribute__((packed)); -template -struct ChoiceSearchConfig { - // 0 = enabled, 1 = disabled. Unused for command C3 - le_uint32_t disabled = 1; - struct Entry { - ItemIDT parent_category_id = 0; - ItemIDT category_id = 0; - } __attribute__((packed)); - parray entries; -} __attribute__((packed)); - template DestT convert_player_disp_data(const SrcT&, uint8_t, uint8_t) { static_assert(always_false::v, diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index 8086b2ed..edd699da 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -787,7 +787,9 @@ static HandlerResult S_C4(shared_ptr ses, uint16_t, return modified ? HandlerResult::Type::MODIFIED : HandlerResult::Type::FORWARD; } -constexpr on_command_t S_V3_C4 = &S_C4; +constexpr on_command_t S_DGX_C4 = &S_C4; +constexpr on_command_t S_P_C4 = &S_C4; +constexpr on_command_t S_B_C4 = &S_C4; static HandlerResult S_G_E4(shared_ptr ses, uint16_t, uint32_t, string& data) { auto& cmd = check_size_t(data); @@ -1962,8 +1964,8 @@ static on_command_t handlers[0x100][13][2] = { /* C1 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}}, /* C2 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}}, /* C3 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}}, -/* C4 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_V3_C4, nullptr}, {S_V3_C4, nullptr}, {S_V3_C4, nullptr}, {S_V3_C4, nullptr}, {nullptr, nullptr}}, -/* C5 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}}, +/* C4 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_DGX_C4, nullptr}, {S_P_C4, nullptr}, {S_DGX_C4, nullptr}, {S_DGX_C4, nullptr}, {S_DGX_C4, nullptr}, {S_DGX_C4, nullptr}, {S_DGX_C4, nullptr}, {S_B_C4, nullptr}}, +/* C5 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}}, /* C6 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}}, /* C7 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}}, /* C8 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}}, diff --git a/src/QuestScript.cc b/src/QuestScript.cc index 419d6b3d..eef1a577 100644 --- a/src/QuestScript.cc +++ b/src/QuestScript.cc @@ -1412,8 +1412,7 @@ std::string disassemble_quest_script(const void* data, size_t size, Version vers string unused = format_data_string(visual.unused.data(), visual.unused.bytes()); lines.emplace_back(string_printf(" %04zX unused %s", l->offset + offsetof(PlayerVisualConfig, unused), unused.c_str())); lines.emplace_back(string_printf(" %04zX name_color_checksum %08" PRIX32, l->offset + offsetof(PlayerVisualConfig, name_color_checksum), visual.name_color_checksum.load())); - string secid_name = name_for_section_id(visual.section_id); - lines.emplace_back(string_printf(" %04zX section_id %02hhX (%s)", l->offset + offsetof(PlayerVisualConfig, section_id), visual.section_id, secid_name.c_str())); + lines.emplace_back(string_printf(" %04zX section_id %02hhX (%s)", l->offset + offsetof(PlayerVisualConfig, section_id), visual.section_id, name_for_section_id(visual.section_id))); lines.emplace_back(string_printf(" %04zX char_class %02hhX (%s)", l->offset + offsetof(PlayerVisualConfig, char_class), visual.char_class, name_for_char_class(visual.char_class))); lines.emplace_back(string_printf(" %04zX validation_flags %02hhX", l->offset + offsetof(PlayerVisualConfig, validation_flags), visual.validation_flags)); lines.emplace_back(string_printf(" %04zX version %02hhX", l->offset + offsetof(PlayerVisualConfig, version), visual.version)); diff --git a/src/RareItemSet.cc b/src/RareItemSet.cc index 60b67d2e..ca73476d 100644 --- a/src/RareItemSet.cc +++ b/src/RareItemSet.cc @@ -518,12 +518,11 @@ void RareItemSet::print_collection( return; } - string secid_name = name_for_section_id(section_id); fprintf(stream, "%s %s %s %s\n", name_for_mode(mode), name_for_episode(episode), name_for_difficulty(difficulty), - secid_name.c_str()); + name_for_section_id(section_id)); fprintf(stream, " Monster rares:\n"); for (size_t z = 0; z < collection->rt_index_to_specs.size(); z++) { diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index ecf9032a..b5438d8b 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -1720,12 +1720,11 @@ static void on_09(shared_ptr c, uint16_t, uint32_t, string& data) { } } - string secid_str = name_for_section_id(game->section_id); info += string_printf("%s %c %s %s\n", abbreviation_for_episode(game->episode), abbreviation_for_difficulty(game->difficulty), abbreviation_for_mode(game->mode), - secid_str.c_str()); + name_for_section_id(game->section_id)); bool cheats_enabled = game->check_flag(Lobby::Flag::CHEATS_ENABLED); bool locked = !game->password.empty(); @@ -3490,8 +3489,99 @@ static void on_40(shared_ptr c, uint16_t, uint32_t, string& data) { } static void on_C0(shared_ptr c, uint16_t, uint32_t, string&) { - // TODO: Implement choice search. - send_text_message(c, "$C6Choice Search is\nnot supported"); + send_choice_search_choices(c); +} + +static void on_C2(shared_ptr c, uint16_t, uint32_t, string& data) { + c->game_data.character()->choice_search_config = check_size_t(data); +} + +template +static void on_choice_search_t(shared_ptr c, const ChoiceSearchConfig& cmd) { + auto s = c->require_server_state(); + + vector results; + for (const auto& l : s->all_lobbies()) { + for (const auto& lc : l->clients) { + if (!lc || lc->game_data.character()->choice_search_config.disabled) { + continue; + } + + bool is_match = true; + for (const auto& cat : CHOICE_SEARCH_CATEGORIES) { + int32_t setting = cmd.get_setting(cat.id); + if (setting == -1) { + continue; + } + try { + if (!cat.client_matches(c, lc, setting)) { + is_match = false; + break; + } + } catch (const exception& e) { + c->log.info("Error in Choice Search matching for category %s: %s", cat.name, e.what()); + } + } + + if (is_match) { + auto lp = lc->game_data.character(); + auto& result = results.emplace_back(); + result.guild_card_number = lc->license->serial_number; + result.name.encode(lp->disp.name.decode(lc->language()), c->language()); + string info_string = string_printf("%s Lv%zu %s", + name_for_char_class(lp->disp.visual.char_class), + static_cast(lp->disp.stats.level + 1), + name_for_section_id(lp->disp.visual.section_id)); + result.info_string.encode(info_string, c->language()); + string lobby_name = l->is_game() ? l->name : string_printf("BLOCK01-%02" PRIu32, l->lobby_id); + string location_string; + if (l->is_game()) { + location_string = string_printf("%s,BLOCK01,%s", l->name.c_str(), s->name.c_str()); + } else if (l->is_ep3()) { + location_string = string_printf("BLOCK01-C%02" PRIu32 ",BLOCK01,%s", l->lobby_id - 15, s->name.c_str()); + } else { + location_string = string_printf("BLOCK01-%02" PRIu32 ",BLOCK01,%s", l->lobby_id, s->name.c_str()); + } + result.location_string.encode(location_string, c->language()); + result.reconnect_command_header.command = 0x19; + result.reconnect_command_header.flag = 0x00; + result.reconnect_command_header.size = sizeof(result.reconnect_command) + sizeof(result.reconnect_command_header); + result.reconnect_command.address = s->connect_address_for_client(c); + result.reconnect_command.port = s->name_to_port_config.at(lobby_port_name_for_version(c->version()))->port; + result.meet_user.lobby_refs[0].menu_id = MenuID::LOBBY; + result.meet_user.lobby_refs[0].item_id = l->lobby_id; + result.meet_user.player_name.encode(lp->disp.name.decode(lc->language()), c->language()); + if (results.size() >= 0x20) { + break; + } + } + } + } + + send_command_vt(c, 0xC4, results.size(), results); +} + +static void on_C3(shared_ptr c, uint16_t, uint32_t, string& data) { + const auto& cmd = check_size_t(data); + switch (c->version()) { + // DC V1 and the prototypes do not support this command + case Version::DC_V2: + case Version::GC_NTE: + case Version::GC_V3: + case Version::GC_EP3_TRIAL_EDITION: + case Version::GC_EP3: + case Version::XB_V3: + on_choice_search_t(c, cmd); + break; + case Version::PC_V2: + on_choice_search_t(c, cmd); + break; + case Version::BB_V4: + on_choice_search_t(c, cmd); + break; + default: + throw runtime_error("unimplemented versioned command"); + } } static void on_81(shared_ptr c, uint16_t, uint32_t, string& data) { @@ -5095,10 +5185,10 @@ static on_command_t handlers[0x100][13] = { /* BE */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}, /* BF */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}, // PC_PATCH BB_PATCH DC_NTE DC_PROTO DCV1 DCV2 PC GCNTE GC EP3TE EP3 XB BB -/* C0 */ {nullptr, nullptr, on_C0, on_C0, on_C0, on_C0, on_C0, on_C0, on_C0, on_C0, on_C0, on_C0, nullptr}, +/* C0 */ {nullptr, nullptr, nullptr, nullptr, nullptr, on_C0, on_C0, on_C0, on_C0, on_C0, on_C0, on_C0, on_C0}, /* C1 */ {nullptr, nullptr, on_0C_C1_E7_EC,on_0C_C1_E7_EC,on_0C_C1_E7_EC,on_0C_C1_E7_EC, on_C1_PC, on_0C_C1_E7_EC, on_0C_C1_E7_EC, on_0C_C1_E7_EC, on_0C_C1_E7_EC, on_0C_C1_E7_EC, on_C1_BB}, -/* C2 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}, -/* C3 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}, +/* C2 */ {nullptr, nullptr, nullptr, nullptr, nullptr, on_C2, on_C2, on_C2, on_C2, on_C2, on_C2, on_C2, on_C2}, +/* C3 */ {nullptr, nullptr, nullptr, nullptr, nullptr, on_C3, on_C3, on_C3, on_C3, on_C3, on_C3, on_C3, on_C3}, /* C4 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}, /* C5 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}, /* C6 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, on_C6, on_C6, on_C6, on_C6, on_C6, on_C6, on_C6}, diff --git a/src/ReceiveCommands.hh b/src/ReceiveCommands.hh index 9101cbf4..8b86229e 100644 --- a/src/ReceiveCommands.hh +++ b/src/ReceiveCommands.hh @@ -1,3 +1,5 @@ +#pragma once + #include #include diff --git a/src/ReceiveSubcommands.hh b/src/ReceiveSubcommands.hh index 4beee02d..12859648 100644 --- a/src/ReceiveSubcommands.hh +++ b/src/ReceiveSubcommands.hh @@ -1,3 +1,5 @@ +#pragma once + #include #include "Client.hh" diff --git a/src/SaveFileFormats.hh b/src/SaveFileFormats.hh index ac233d11..2d4454c6 100644 --- a/src/SaveFileFormats.hh +++ b/src/SaveFileFormats.hh @@ -10,6 +10,7 @@ #include #include +#include "ChoiceSearch.hh" #include "Episode3/DataIndexes.hh" #include "ItemNameIndex.hh" #include "PSOEncryption.hh" @@ -207,7 +208,7 @@ struct PSOBBCharacterFile { /* 2CB4 */ parray unknown_a4; /* 2CB8 */ PlayerRecordsBB_Challenge challenge_records; /* 2DF8 */ parray tech_menu_config; - /* 2E20 */ ChoiceSearchConfig choice_search_config; + /* 2E20 */ ChoiceSearchConfig choice_search_config; /* 2E38 */ parray unknown_a6; /* 2E48 */ parray quest_global_flags; /* 2E88 */ parray unknown_a7; diff --git a/src/SendCommands.cc b/src/SendCommands.cc index adc43a65..803be72b 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -987,6 +987,44 @@ void send_info_board(shared_ptr c) { } } +template +void send_choice_search_choices_t(shared_ptr c) { + vector entries; + for (const auto& cat : CHOICE_SEARCH_CATEGORIES) { + auto& cat_e = entries.emplace_back(); + cat_e.parent_choice_id = 0; + cat_e.choice_id = cat.id; + cat_e.text.encode(cat.name, c->language()); + for (const auto& choice : cat.choices) { + auto& e = entries.emplace_back(); + e.parent_choice_id = cat.id; + e.choice_id = choice.id; + e.text.encode(choice.name, c->language()); + } + } + send_command_vt(c, 0xC0, entries.size(), entries); +} + +void send_choice_search_choices(shared_ptr c) { + switch (c->version()) { + // DC V1 and the prototypes do not support this command + case Version::DC_V2: + case Version::GC_NTE: + case Version::GC_V3: + case Version::GC_EP3_TRIAL_EDITION: + case Version::GC_EP3: + case Version::XB_V3: + send_choice_search_choices_t(c); + break; + case Version::PC_V2: + case Version::BB_V4: + send_choice_search_choices_t(c); + break; + default: + throw logic_error("unimplemented versioned command"); + } +} + template void send_card_search_result_t( shared_ptr c, @@ -2981,7 +3019,8 @@ void send_quest_file_chunk( } cmd.data_size = size; - send_command_t(c, is_download_quest ? 0xA7 : 0x13, chunk_index, cmd); + c->log.info("Sending quest file chunk %s:%zu", filename.c_str(), chunk_index); + c->channel.send(is_download_quest ? 0xA7 : 0x13, chunk_index, &cmd, sizeof(cmd), true); } template diff --git a/src/SendCommands.hh b/src/SendCommands.hh index ca46752e..2f26d5c8 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -232,6 +232,8 @@ __attribute__((format(printf, 2, 3))) void send_ep3_text_message_printf( void send_info_board(std::shared_ptr c); +void send_choice_search_choices(std::shared_ptr c); + void send_card_search_result( std::shared_ptr c, std::shared_ptr result, diff --git a/src/ServerShell.cc b/src/ServerShell.cc index 78ca0f05..5e6d3a53 100644 --- a/src/ServerShell.cc +++ b/src/ServerShell.cc @@ -656,11 +656,11 @@ Proxy session commands:\n\ for (size_t z = 0; z < ses->lobby_players.size(); z++) { const auto& player = ses->lobby_players[z]; if (player.guild_card_number) { - auto secid_name = name_for_section_id(player.section_id); fprintf(stderr, " %zu: %" PRIu32 " => %s (%c, %s, %s)\n", z, player.guild_card_number, player.name.c_str(), char_for_language_code(player.language), - name_for_char_class(player.char_class), secid_name.c_str()); + name_for_char_class(player.char_class), + name_for_section_id(player.section_id)); } else { fprintf(stderr, " %zu: (no player)\n", z); } diff --git a/src/StaticGameData.cc b/src/StaticGameData.cc index 67f4c36c..b41f9ee9 100644 --- a/src/StaticGameData.cc +++ b/src/StaticGameData.cc @@ -89,7 +89,7 @@ const char* abbreviation_for_mode(GameMode mode) { } } -const vector section_id_to_name = { +static const array section_id_to_name = { "Viridia", "Greennill", "Skyly", "Bluefull", "Purplenum", "Pinkal", "Redria", "Oran", "Yellowboze", "Whitill"}; @@ -205,12 +205,11 @@ const vector npc_id_to_name({"ninja", "rico", "sonic", "knuckles", "tail const unordered_map name_to_npc_id = { {"ninja", 0}, {"rico", 1}, {"sonic", 2}, {"knuckles", 3}, {"tails", 4}, {"flowen", 5}, {"elly", 6}}; -const string& name_for_section_id(uint8_t section_id) { +const char* name_for_section_id(uint8_t section_id) { if (section_id < section_id_to_name.size()) { return section_id_to_name[section_id]; } else { - static const string ret = ""; - return ret; + return ""; } } diff --git a/src/StaticGameData.hh b/src/StaticGameData.hh index fa6e2c00..b3ddf1c7 100644 --- a/src/StaticGameData.hh +++ b/src/StaticGameData.hh @@ -39,7 +39,7 @@ extern const std::unordered_map name_to_tech_id; const std::string& name_for_technique(uint8_t tech); uint8_t technique_for_name(const std::string& name); -const std::string& name_for_section_id(uint8_t section_id); +const char* name_for_section_id(uint8_t section_id); uint8_t section_id_for_name(const std::string& name); const std::string& name_for_event(uint8_t event);