From 45b33a3c3a3ebb066b12a246f123610e193a33a0 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sat, 13 Jun 2026 10:25:16 -0700 Subject: [PATCH 1/9] add timing in check-quests --- src/Main.cc | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/Main.cc b/src/Main.cc index 2f3f3e9a..039eea2a 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -3682,10 +3682,12 @@ Action a_check_quests( bool reassemble_scripts = args.get("reassemble-scripts"); bool reassemble_maps = args.get("reassemble-maps"); + uint64_t script_time = 0, map_time = 0; if (reassemble_scripts || reassemble_maps) { for (const auto& [_, q] : s->quest_index->quests_by_number) { for (const auto& [_, vq] : q->versions) { if (reassemble_maps) { + uint64_t start_time = phosg::now(); auto dat = prs_decompress(*vq->dat_contents); auto serialized = vq->map_file->serialize(); if (dat != serialized) { @@ -3701,9 +3703,17 @@ Action a_check_quests( vq->meta.name); throw std::runtime_error("re-serialized map file differs from original"); } - phosg::log_info_f("... {} {} {} ({}) MAP OK", phosg::name_for_enum(vq->meta.version), name_for_language(vq->meta.language), vq->dat_filename(), vq->meta.name); + uint64_t end_time = phosg::now(); + map_time += (end_time - start_time); + phosg::log_info_f("... {} {} {} ({}) MAP OK ({})", + phosg::name_for_enum(vq->meta.version), + name_for_language(vq->meta.language), + vq->dat_filename(), + vq->meta.name, + phosg::format_duration(end_time - start_time)); } if (reassemble_scripts) { + uint64_t start_time = phosg::now(); auto bin = prs_decompress(*vq->bin_contents); auto disassembled = disassemble_quest_script( bin.data(), bin.size(), vq->meta.version, vq->meta.language, vq->map_file, false, false); @@ -3768,11 +3778,24 @@ Action a_check_quests( phosg::log_info_f("... {} {} {} ({}) SCRIPT FAILED", phosg::name_for_enum(vq->meta.version), name_for_language(vq->meta.language), vq->bin_filename(), vq->meta.name); throw; } - phosg::log_info_f("... {} {} {} ({}) SCRIPT OK", phosg::name_for_enum(vq->meta.version), name_for_language(vq->meta.language), vq->bin_filename(), vq->meta.name); + uint64_t end_time = phosg::now(); + script_time += (end_time - start_time); + phosg::log_info_f("... {} {} {} ({}) SCRIPT OK ({})", + phosg::name_for_enum(vq->meta.version), + name_for_language(vq->meta.language), + vq->bin_filename(), + vq->meta.name, + phosg::format_duration(end_time - start_time)); } } } } + if (script_time > 0) { + phosg::log_info_f("... SCRIPT CHECKS: {}", phosg::format_duration(script_time)); + } + if (map_time > 0) { + phosg::log_info_f("... MAP CHECKS: {}", phosg::format_duration(map_time)); + } }); Action a_check_ep3_maps( From 7c007d1b1e6866e3c38819c2832c1af4e9cc96a6 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sat, 13 Jun 2026 10:18:19 -0700 Subject: [PATCH 2/9] fix payment item handling in 6xDA --- src/ReceiveSubcommands.cc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 54b480a7..0665e148 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -5479,8 +5479,9 @@ static void on_upgrade_weapon_attribute_bb(std::shared_ptr c, Subcommand throw std::runtime_error("bonus value exceeds 100"); } - p->remove_item(payment_item.id, cmd.payment_count, *s->item_stack_limits(c->version())); - send_destroy_item_to_lobby(c, payment_item.id, cmd.payment_count); + auto removed_payment_item = p->remove_item( + payment_item.id, cmd.payment_count, *s->item_stack_limits(c->version())); + send_destroy_item_to_lobby(c, removed_payment_item.id, cmd.payment_count); item.data1[attribute_index] = cmd.attribute; item.data1[attribute_index + 1] = new_attr_value; From 9647fe4d6382157521de4b3c8952ffac03620b1d Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sat, 13 Jun 2026 10:18:40 -0700 Subject: [PATCH 3/9] make $item command use game item ID space --- src/ChatCommands.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index 2a911c20..ce9b00dc 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -1240,7 +1240,7 @@ ChatCommandDefinition cc_item( } else { auto l = a.c->require_lobby(); item = s->parse_item_description(a.c->version(), a.text); - item.id = l->generate_item_id(a.c->lobby_client_id); + item.id = l->generate_item_id(0xFF); if ((l->drop_mode == ServerDropMode::SERVER_PRIVATE) || (l->drop_mode == ServerDropMode::SERVER_DUPLICATE)) { l->add_item(a.c->floor, item, a.c->pos, nullptr, nullptr, (1 << a.c->lobby_client_id)); From 5739f99912b09758d35a09a5ce79f3a2b442c50c Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sat, 13 Jun 2026 15:55:02 -0700 Subject: [PATCH 4/9] port 60 FPS code to all GC versions --- notes/ar-codes.txt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/notes/ar-codes.txt b/notes/ar-codes.txt index c5dc71f7..7e71b439 100644 --- a/notes/ar-codes.txt +++ b/notes/ar-codes.txt @@ -682,8 +682,20 @@ Allow loading corrupted save files 041156D4 4E800020 60 frames per second -This does not adjust any logic or animations; everything just runs faster -3OE1 => 045CDEF8 00000001 +This doesn't adjust any logic or animations; everything just runs faster +3OJT => 043F5AC0 38800001 +3OJ2 => 043D8550 38800001 +3OJ3 => 043DAF58 38800001 +3OJ4 => 043DCDF8 38800001 +3OJ5 => 043DCBA8 38800001 +3OE0 => 043D9820 38800001 +3OE1 => 043D9878 38800001 +3OE2 => 043DCF78 38800001 +3OP0 => 043DBA68 38800001 +3SJT => 043567AC 38800001 +3SE0 => 0438A804 38800001 +3SJ0 => 043897B4 38800001 +3SP0 => 0438B6D4 38800001 Show extended item info when targeting a dropped item (Compiled from the ExtendedItemInfo patch, also written by me) From 0dbb34b9f9457efeba5810414f8d7eb08da7ec31 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sat, 13 Jun 2026 17:15:57 -0700 Subject: [PATCH 5/9] don't share iconv_t objects between threads --- src/Text.cc | 21 +++++++++++---------- src/Text.hh | 20 ++++++++++---------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/Text.cc b/src/Text.cc index c68a851e..1876aef0 100644 --- a/src/Text.cc +++ b/src/Text.cc @@ -257,16 +257,17 @@ std::string TextTranscoderUTF8ToCustomSJIS::on_untranslatable(const void** src, } } -TextTranscoder tt_8859_to_utf8("UTF-8", "ISO-8859-1"); -TextTranscoder tt_utf8_to_8859("ISO-8859-1", "UTF-8"); -TextTranscoder tt_standard_sjis_to_utf8("UTF-8", "SHIFT_JIS"); -TextTranscoder tt_utf8_to_standard_sjis("SHIFT_JIS", "UTF-8"); -TextTranscoderCustomSJISToUTF8 tt_sega_sjis_to_utf8; -TextTranscoderUTF8ToCustomSJIS tt_utf8_to_sega_sjis; -TextTranscoder tt_utf16_to_utf8("UTF-8", "UTF-16LE"); -TextTranscoder tt_utf8_to_utf16("UTF-16LE", "UTF-8"); -TextTranscoder tt_ascii_to_utf8("UTF-8", "ASCII"); -TextTranscoder tt_utf8_to_ascii("ASCII", "UTF-8"); +// iconv_t is not thread-safe, so we need a separate one of these for each thread +thread_local TextTranscoder tt_8859_to_utf8("UTF-8", "ISO-8859-1"); +thread_local TextTranscoder tt_utf8_to_8859("ISO-8859-1", "UTF-8"); +thread_local TextTranscoder tt_standard_sjis_to_utf8("UTF-8", "SHIFT_JIS"); +thread_local TextTranscoder tt_utf8_to_standard_sjis("SHIFT_JIS", "UTF-8"); +thread_local TextTranscoderCustomSJISToUTF8 tt_sega_sjis_to_utf8; +thread_local TextTranscoderUTF8ToCustomSJIS tt_utf8_to_sega_sjis; +thread_local TextTranscoder tt_utf16_to_utf8("UTF-8", "UTF-16LE"); +thread_local TextTranscoder tt_utf8_to_utf16("UTF-16LE", "UTF-8"); +thread_local TextTranscoder tt_ascii_to_utf8("UTF-8", "ASCII"); +thread_local TextTranscoder tt_utf8_to_ascii("ASCII", "UTF-8"); std::string tt_encode_marked_optional(const std::string& utf8, Language default_language, bool is_utf16) { if (is_utf16) { diff --git a/src/Text.hh b/src/Text.hh index a9707805..52024f67 100644 --- a/src/Text.hh +++ b/src/Text.hh @@ -77,16 +77,16 @@ protected: virtual std::string on_untranslatable(const void** src, size_t* size) const; }; -extern TextTranscoder tt_8859_to_utf8; -extern TextTranscoder tt_utf8_to_8859; -extern TextTranscoder tt_standard_sjis_to_utf8; -extern TextTranscoder tt_utf8_to_standard_sjis; -extern TextTranscoderCustomSJISToUTF8 tt_sega_sjis_to_utf8; -extern TextTranscoderUTF8ToCustomSJIS tt_utf8_to_sega_sjis; -extern TextTranscoder tt_utf16_to_utf8; -extern TextTranscoder tt_utf8_to_utf16; -extern TextTranscoder tt_ascii_to_utf8; -extern TextTranscoder tt_utf8_to_ascii; +extern thread_local TextTranscoder tt_8859_to_utf8; +extern thread_local TextTranscoder tt_utf8_to_8859; +extern thread_local TextTranscoder tt_standard_sjis_to_utf8; +extern thread_local TextTranscoder tt_utf8_to_standard_sjis; +extern thread_local TextTranscoderCustomSJISToUTF8 tt_sega_sjis_to_utf8; +extern thread_local TextTranscoderUTF8ToCustomSJIS tt_utf8_to_sega_sjis; +extern thread_local TextTranscoder tt_utf16_to_utf8; +extern thread_local TextTranscoder tt_utf8_to_utf16; +extern thread_local TextTranscoder tt_ascii_to_utf8; +extern thread_local TextTranscoder tt_utf8_to_ascii; std::string tt_encode_marked_optional(const std::string& utf8, Language default_language, bool is_utf16); std::string tt_encode_marked(const std::string& utf8, Language default_language, bool is_utf16); From 554fc5d208077b7299cd2e1abd6033b34c6ed00e Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sat, 13 Jun 2026 17:15:42 -0700 Subject: [PATCH 6/9] make check-quests parallel --- src/Main.cc | 228 +++++++++++++++++++++++++++++----------------------- 1 file changed, 129 insertions(+), 99 deletions(-) diff --git a/src/Main.cc b/src/Main.cc index 039eea2a..f32b91dc 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -3670,7 +3670,12 @@ Action a_print_free_supermap( Action a_check_quests( "check-quests", nullptr, +[](phosg::Arguments& args) { + size_t num_threads = args.get("threads", 0); + bool reassemble_scripts = args.get("reassemble-scripts"); + bool reassemble_maps = args.get("reassemble-maps"); + check_quest_opcode_definitions(); + phosg::log_info_f("Opcode definitions OK"); auto s = std::make_shared(get_config_filename(args)); s->is_debug = true; @@ -3680,114 +3685,139 @@ Action a_check_quests( s->load_maps(); s->load_quest_index(true); - bool reassemble_scripts = args.get("reassemble-scripts"); - bool reassemble_maps = args.get("reassemble-maps"); uint64_t script_time = 0, map_time = 0; if (reassemble_scripts || reassemble_maps) { - for (const auto& [_, q] : s->quest_index->quests_by_number) { - for (const auto& [_, vq] : q->versions) { - if (reassemble_maps) { - uint64_t start_time = phosg::now(); - auto dat = prs_decompress(*vq->dat_contents); - auto serialized = vq->map_file->serialize(); - if (dat != serialized) { - phosg::log_info_f("... DISASSEMBLY:"); - phosg::fwritex(stdout, vq->map_file->disassemble(false, vq->meta.version)); - phosg::log_info_f("... BINDIFF:"); - phosg::print_binary_diff( - stdout, dat.data(), dat.size(), serialized.data(), serialized.size(), isatty(fileno(stdout))); - phosg::log_info_f("... {} {} {} ({}) MAP FAILED", - phosg::name_for_enum(vq->meta.version), - name_for_language(vq->meta.language), - vq->dat_filename(), - vq->meta.name); - throw std::runtime_error("re-serialized map file differs from original"); - } - uint64_t end_time = phosg::now(); - map_time += (end_time - start_time); - phosg::log_info_f("... {} {} {} ({}) MAP OK ({})", + std::mutex output_lock; + auto check_vq = [&](const std::shared_ptr& vq, size_t) -> void { + if (reassemble_maps) { + uint64_t start_time = phosg::now(); + auto dat = prs_decompress(*vq->dat_contents); + auto serialized = vq->map_file->serialize(); + if (dat != serialized) { + std::lock_guard g(output_lock); + phosg::log_info_f("... DISASSEMBLY:"); + phosg::fwritex(stdout, vq->map_file->disassemble(false, vq->meta.version)); + phosg::log_info_f("... BINDIFF:"); + phosg::print_binary_diff( + stdout, dat.data(), dat.size(), serialized.data(), serialized.size(), isatty(fileno(stdout))); + phosg::log_info_f("... {} {} {} ({}) MAP FAILED", phosg::name_for_enum(vq->meta.version), name_for_language(vq->meta.language), vq->dat_filename(), - vq->meta.name, - phosg::format_duration(end_time - start_time)); + vq->meta.name); + throw std::runtime_error("re-serialized map file differs from original"); } - if (reassemble_scripts) { - uint64_t start_time = phosg::now(); - auto bin = prs_decompress(*vq->bin_contents); - auto disassembled = disassemble_quest_script( - bin.data(), bin.size(), vq->meta.version, vq->meta.language, vq->map_file, false, false); - auto reassembly = disassemble_quest_script( - bin.data(), bin.size(), vq->meta.version, vq->meta.language, vq->map_file, true, false); - std::string include_dir = phosg::dirname(vq->bin_filename()); - AssembledQuestScript assembled; - try { - assembled = assemble_quest_script( - reassembly, - {"system/quests/includes"}, - {"system/quests/includes", "system/client-functions/System"}, - false); - if (vq->json_contents) { - assembled.meta.apply_json_overrides(*vq->json_contents); - } - if (assembled.data != bin) { - throw std::runtime_error("Reassembled quest script does not match original"); - } - // Don't check quest number, since we override it based on the filename - if (assembled.meta.version != vq->meta.version) { - throw std::runtime_error(std::format("Reassembled quest version ({}) does not match original ({})", - phosg::name_for_enum(assembled.meta.version), phosg::name_for_enum(vq->meta.version))); - } - if (assembled.meta.language != vq->meta.language) { - throw std::runtime_error(std::format("Reassembled quest language ({}) does not match original ({})", - name_for_language(assembled.meta.language), name_for_language(vq->meta.language))); - } - if (assembled.meta.episode != vq->meta.episode) { - throw std::runtime_error(std::format("Reassembled quest episode ({}) does not match original ({})", - name_for_episode(assembled.meta.episode), name_for_episode(vq->meta.episode))); - } - if (assembled.meta.joinable != vq->meta.joinable) { - throw std::runtime_error(std::format("Reassembled quest joinable ({}) does not match original ({})", - assembled.meta.joinable, vq->meta.joinable)); - } - if (assembled.meta.max_players != vq->meta.max_players) { - throw std::runtime_error(std::format("Reassembled quest max_players ({}) does not match original ({})", - assembled.meta.max_players, vq->meta.max_players)); - } - if (assembled.meta.name != vq->meta.name) { - throw std::runtime_error(std::format("Reassembled quest name ({}) does not match original ({})", - assembled.meta.name, vq->meta.name)); - } - if (assembled.meta.short_description != vq->meta.short_description) { - throw std::runtime_error(std::format("Reassembled quest short description ({}) does not match original ({})", - assembled.meta.short_description, vq->meta.short_description)); - } - if (assembled.meta.long_description != vq->meta.long_description) { - throw std::runtime_error(std::format("Reassembled quest long description ({}) does not match original ({})", - assembled.meta.long_description, vq->meta.long_description)); - } - } catch (const std::exception& e) { - phosg::log_error_f("================ DISASSEMBLY:"); - phosg::fwritex(stderr, disassembled); - phosg::log_error_f("================ REASSEMBLY:"); - phosg::fwritex(stderr, reassembly); - if (!assembled.data.empty()) { - phosg::log_error_f("================ BINDIFF:"); - phosg::print_binary_diff(stderr, bin.data(), bin.size(), assembled.data.data(), assembled.data.size(), isatty(fileno(stderr)), 3, 0); - } - phosg::log_info_f("... {} {} {} ({}) SCRIPT FAILED", phosg::name_for_enum(vq->meta.version), name_for_language(vq->meta.language), vq->bin_filename(), vq->meta.name); - throw; + uint64_t end_time = phosg::now(); + map_time += (end_time - start_time); + std::lock_guard g(output_lock); + phosg::log_info_f("... {} {} {} ({}) MAP OK ({})", + phosg::name_for_enum(vq->meta.version), + name_for_language(vq->meta.language), + vq->dat_filename(), + vq->meta.name, + phosg::format_duration(end_time - start_time)); + } + if (reassemble_scripts) { + uint64_t start_time = phosg::now(); + auto bin = prs_decompress(*vq->bin_contents); + auto disassembled = disassemble_quest_script( + bin.data(), bin.size(), vq->meta.version, vq->meta.language, vq->map_file, false, false); + auto reassembly = disassemble_quest_script( + bin.data(), bin.size(), vq->meta.version, vq->meta.language, vq->map_file, true, false); + std::string include_dir = phosg::dirname(vq->bin_filename()); + AssembledQuestScript assembled; + try { + assembled = assemble_quest_script( + reassembly, + {"system/quests/includes"}, + {"system/quests/includes", "system/client-functions/System"}, + false); + if (vq->json_contents) { + assembled.meta.apply_json_overrides(*vq->json_contents); } - uint64_t end_time = phosg::now(); - script_time += (end_time - start_time); - phosg::log_info_f("... {} {} {} ({}) SCRIPT OK ({})", - phosg::name_for_enum(vq->meta.version), - name_for_language(vq->meta.language), - vq->bin_filename(), - vq->meta.name, - phosg::format_duration(end_time - start_time)); + if (assembled.data != bin) { + throw std::runtime_error("Reassembled quest script does not match original"); + } + // Don't check quest number, since we override it based on the filename + if (assembled.meta.version != vq->meta.version) { + throw std::runtime_error(std::format("Reassembled quest version ({}) does not match original ({})", + phosg::name_for_enum(assembled.meta.version), phosg::name_for_enum(vq->meta.version))); + } + if (assembled.meta.language != vq->meta.language) { + throw std::runtime_error(std::format("Reassembled quest language ({}) does not match original ({})", + name_for_language(assembled.meta.language), name_for_language(vq->meta.language))); + } + if (assembled.meta.episode != vq->meta.episode) { + throw std::runtime_error(std::format("Reassembled quest episode ({}) does not match original ({})", + name_for_episode(assembled.meta.episode), name_for_episode(vq->meta.episode))); + } + if (assembled.meta.joinable != vq->meta.joinable) { + throw std::runtime_error(std::format("Reassembled quest joinable ({}) does not match original ({})", + assembled.meta.joinable, vq->meta.joinable)); + } + if (assembled.meta.max_players != vq->meta.max_players) { + throw std::runtime_error(std::format("Reassembled quest max_players ({}) does not match original ({})", + assembled.meta.max_players, vq->meta.max_players)); + } + if (assembled.meta.name != vq->meta.name) { + throw std::runtime_error(std::format("Reassembled quest name ({}) does not match original ({})", + assembled.meta.name, vq->meta.name)); + } + if (assembled.meta.short_description != vq->meta.short_description) { + throw std::runtime_error(std::format("Reassembled quest short description ({}) does not match original ({})", + assembled.meta.short_description, vq->meta.short_description)); + } + if (assembled.meta.long_description != vq->meta.long_description) { + throw std::runtime_error(std::format("Reassembled quest long description ({}) does not match original ({})", + assembled.meta.long_description, vq->meta.long_description)); + } + } catch (const std::exception& e) { + std::lock_guard g(output_lock); + phosg::log_error_f("================ DISASSEMBLY:"); + phosg::fwritex(stderr, disassembled); + phosg::log_error_f("================ REASSEMBLY:"); + phosg::fwritex(stderr, reassembly); + if (!assembled.data.empty()) { + phosg::log_error_f("================ BINDIFF:"); + phosg::print_binary_diff(stderr, bin.data(), bin.size(), assembled.data.data(), assembled.data.size(), isatty(fileno(stderr)), 3, 0); + } + phosg::log_info_f("... {} {} {} ({}) SCRIPT FAILED", phosg::name_for_enum(vq->meta.version), name_for_language(vq->meta.language), vq->bin_filename(), vq->meta.name); + throw; + } + uint64_t end_time = phosg::now(); + script_time += (end_time - start_time); + std::lock_guard g(output_lock); + phosg::log_info_f("... {} {} {} ({}) SCRIPT OK ({})", + phosg::name_for_enum(vq->meta.version), + name_for_language(vq->meta.language), + vq->bin_filename(), + vq->meta.name, + phosg::format_duration(end_time - start_time)); + } + }; + + if (num_threads == 1) { + for (const auto& [_, q] : s->quest_index->quests_by_number) { + for (const auto& [_, vq] : q->versions) { + check_vq(vq, 0); } } + + } else { + std::vector> all_vqs; + for (const auto& [_, q] : s->quest_index->quests_by_number) { + for (const auto& [_, vq] : q->versions) { + all_vqs.emplace_back(vq); + } + } + + // Sort them in decreasing order of bin file size, so the slowest ones are run first (this packs the work + // into the threads' timelines more efficiently) + std::sort(all_vqs.begin(), all_vqs.end(), [](const std::shared_ptr& a, const std::shared_ptr& b) -> bool { + return a->bin_contents->size() > b->bin_contents->size(); + }); + + phosg::parallel_range(all_vqs, check_vq, num_threads); } } if (script_time > 0) { From 1737d8abc806e8fe7c07e26ffd6475ee77d111f2 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sat, 13 Jun 2026 20:07:23 -0700 Subject: [PATCH 7/9] add more options in IntegralExpression --- src/ChoiceSearch.cc | 4 +-- src/Client.cc | 4 ++- src/DownloadSession.cc | 4 +-- src/Episode3/Server.cc | 4 +-- src/IntegralExpression.cc | 49 ++++++++++++++++++++++++------- src/IntegralExpression.hh | 20 ++++++++++--- src/ItemData.cc | 8 ++--- src/ReceiveCommands.cc | 4 ++- src/SendCommands.cc | 12 ++++---- src/ShopRandomSets.cc | 12 ++++---- src/StaticGameData.cc | 16 +++++----- system/config.example.json | 44 +++++++++++++-------------- system/quests/retrieval/q058.json | 5 +++- 13 files changed, 117 insertions(+), 69 deletions(-) diff --git a/src/ChoiceSearch.cc b/src/ChoiceSearch.cc index 5d45cd79..eed10627 100644 --- a/src/ChoiceSearch.cc +++ b/src/ChoiceSearch.cc @@ -5,7 +5,7 @@ #include "Client.hh" -const std::vector CHOICE_SEARCH_CATEGORIES({ +const std::vector CHOICE_SEARCH_CATEGORIES{ ChoiceSearchCategory{ .id = 0x0001, .name = "Level", @@ -145,4 +145,4 @@ const std::vector CHOICE_SEARCH_CATEGORIES({ return (choice_id == 0) || (target_choice_id == 0) || (choice_id == target_choice_id); }, }, -}); +}; diff --git a/src/Client.cc b/src/Client.cc index 09601ae8..c6e7567c 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -377,7 +377,9 @@ bool Client::evaluate_quest_availability_expression( } auto p = this->character_file(); IntegralExpression::Env env = { - .flags = &p->quest_flags.data.at(static_cast(difficulty)), + .section_id = p->disp.visual.sh.section_id, + .difficulty = difficulty, + .flags = &p->quest_flags, .challenge_records = &p->challenge_records, .team = this->team(), .num_players = num_players, diff --git a/src/DownloadSession.cc b/src/DownloadSession.cc index 93a708b1..900e291c 100644 --- a/src/DownloadSession.cc +++ b/src/DownloadSession.cc @@ -811,7 +811,7 @@ void DownloadSession::on_request_complete() { } } -const std::vector DownloadSession::game_configs({ +const std::vector DownloadSession::game_configs{ {.mode = GameMode::NORMAL, .episode = Episode::EP1, .v1 = true, .v2 = true, .v3 = true}, {.mode = GameMode::NORMAL, .episode = Episode::EP2, .v1 = false, .v2 = false, .v3 = true}, {.mode = GameMode::NORMAL, .episode = Episode::EP4, .v1 = false, .v2 = false, .v3 = false}, @@ -821,4 +821,4 @@ const std::vector DownloadSession::game_configs({ {.mode = GameMode::SOLO, .episode = Episode::EP1, .v1 = false, .v2 = false, .v3 = false}, {.mode = GameMode::SOLO, .episode = Episode::EP2, .v1 = false, .v2 = false, .v3 = false}, {.mode = GameMode::SOLO, .episode = Episode::EP4, .v1 = false, .v2 = false, .v3 = false}, -}); +}; diff --git a/src/Episode3/Server.cc b/src/Episode3/Server.cc index 5285d6ed..77504133 100644 --- a/src/Episode3/Server.cc +++ b/src/Episode3/Server.cc @@ -1738,7 +1738,7 @@ bool Server::update_registration_phase() { return true; } -const std::unordered_map Server::subcommand_handlers({ +const std::unordered_map Server::subcommand_handlers{ {0x0B, &Server::handle_CAx0B_redraw_initial_hand}, {0x0C, &Server::handle_CAx0C_end_redraw_initial_hand_phase}, {0x0D, &Server::handle_CAx0D_end_non_action_phase}, @@ -1762,7 +1762,7 @@ const std::unordered_map Server::subcommand_handlers {0x41, &Server::handle_CAx41_map_request}, {0x48, &Server::handle_CAx48_end_turn}, {0x49, &Server::handle_CAx49_card_counts}, -}); +}; void Server::on_server_data_input(std::shared_ptr sender_c, const std::string& data) { auto header = check_size_t(data, 0xFFFF); diff --git a/src/IntegralExpression.cc b/src/IntegralExpression.cc index 79b4dc85..d0ffb362 100644 --- a/src/IntegralExpression.cc +++ b/src/IntegralExpression.cc @@ -158,26 +158,42 @@ std::string IntegralExpression::UnaryOperatorNode::str() const { } } -IntegralExpression::FlagLookupNode::FlagLookupNode(uint16_t flag_index) : flag_index(flag_index) {} +bool IntegralExpression::SectionIDLookupNode::operator==(const Node& other) const { + return (dynamic_cast(&other) != nullptr); +} -bool IntegralExpression::FlagLookupNode::operator==(const Node& other) const { +int64_t IntegralExpression::SectionIDLookupNode::evaluate(const Env& env) const { + return env.section_id; +} + +std::string IntegralExpression::SectionIDLookupNode::str() const { + return "P_SID"; +} + +IntegralExpression::QuestFlagLookupNode::QuestFlagLookupNode(Difficulty difficulty, uint16_t flag_index) + : difficulty(difficulty), flag_index(flag_index) {} + +bool IntegralExpression::QuestFlagLookupNode::operator==(const Node& other) const { try { - const FlagLookupNode& other_flag = dynamic_cast(other); - return other_flag.flag_index == this->flag_index; + const QuestFlagLookupNode& other_flag = dynamic_cast(other); + return ((other_flag.difficulty == this->difficulty) && (other_flag.flag_index == this->flag_index)); } catch (const std::bad_cast&) { return false; } } -int64_t IntegralExpression::FlagLookupNode::evaluate(const Env& env) const { +int64_t IntegralExpression::QuestFlagLookupNode::evaluate(const Env& env) const { if (!env.flags) { throw std::runtime_error("quest flags not available"); } - return env.flags->get(this->flag_index) ? 1 : 0; + Difficulty effective_difficulty = (this->difficulty == Difficulty::UNKNOWN) ? env.difficulty : this->difficulty; + return env.flags->get(effective_difficulty, this->flag_index) ? 1 : 0; } -std::string IntegralExpression::FlagLookupNode::str() const { - return std::format("F_{:04X}", this->flag_index); +std::string IntegralExpression::QuestFlagLookupNode::str() const { + return (this->difficulty == Difficulty::UNKNOWN) + ? std::format("F_{:04X}", this->flag_index) + : std::format("F_{:c}_{:04X}", abbreviation_for_difficulty(this->difficulty), this->flag_index); } IntegralExpression::ChallengeCompletionLookupNode::ChallengeCompletionLookupNode(Episode episode, uint8_t stage_index) @@ -380,16 +396,29 @@ std::unique_ptr IntegralExpression::parse_expr(s } // Check for env lookups + if (text == "P_SID") { + return std::make_unique(); + } if (text.starts_with("F_")) { + Difficulty difficulty = Difficulty::UNKNOWN; + if (text.starts_with("F_N_")) { + difficulty = Difficulty::NORMAL; + } else if (text.starts_with("F_H_")) { + difficulty = Difficulty::HARD; + } else if (text.starts_with("F_V_")) { + difficulty = Difficulty::VERY_HARD; + } else if (text.starts_with("F_U_")) { + difficulty = Difficulty::ULTIMATE; + } char* endptr = nullptr; - uint64_t flag = strtoul(text.data() + 2, &endptr, 16); + uint64_t flag = strtoul(text.data() + ((difficulty == Difficulty::UNKNOWN) ? 2 : 4), &endptr, 16); if (endptr != text.data() + text.size()) { throw std::runtime_error("invalid flag lookup token"); } if (flag >= 0x400) { throw std::runtime_error("invalid flag index"); } - return std::make_unique(flag); + return std::make_unique(difficulty, flag); } if (text.starts_with("CC_")) { Episode episode; diff --git a/src/IntegralExpression.hh b/src/IntegralExpression.hh index 553acc27..e22bd188 100644 --- a/src/IntegralExpression.hh +++ b/src/IntegralExpression.hh @@ -15,7 +15,9 @@ class IntegralExpression { public: struct Env { - const QuestFlagsForDifficulty* flags; + uint8_t section_id; + Difficulty difficulty; + const QuestFlags* flags; const PlayerRecordsChallengeBB* challenge_records; std::shared_ptr team; size_t num_players; @@ -105,15 +107,25 @@ protected: std::unique_ptr sub; }; - class FlagLookupNode : public Node { + class SectionIDLookupNode : public Node { public: - FlagLookupNode(uint16_t flag_index); - virtual ~FlagLookupNode() = default; + SectionIDLookupNode() = default; + virtual ~SectionIDLookupNode() = default; + virtual bool operator==(const Node& other) const; + virtual int64_t evaluate(const Env& env) const; + virtual std::string str() const; + }; + + class QuestFlagLookupNode : public Node { + public: + QuestFlagLookupNode(Difficulty difficulty, uint16_t flag_index); + virtual ~QuestFlagLookupNode() = default; virtual bool operator==(const Node& other) const; virtual int64_t evaluate(const Env& env) const; virtual std::string str() const; protected: + Difficulty difficulty; uint16_t flag_index; }; diff --git a/src/ItemData.cc b/src/ItemData.cc index f53f1f35..acd79fa8 100644 --- a/src/ItemData.cc +++ b/src/ItemData.cc @@ -6,10 +6,10 @@ #include "ItemParameterTable.hh" #include "StaticGameData.hh" -const std::vector ItemData::StackLimits::DEFAULT_TOOL_LIMITS_DC_NTE({10}); -const std::vector ItemData::StackLimits::DEFAULT_TOOL_LIMITS_V1_V2({10, 10, 1, 10, 10, 10, 10, 10, 10, 1}); -const std::vector ItemData::StackLimits::DEFAULT_TOOL_LIMITS_V3_V4( - {10, 10, 1, 10, 10, 10, 10, 10, 10, 1, 1, 1, 1, 1, 1, 1, 99, 1}); +const std::vector ItemData::StackLimits::DEFAULT_TOOL_LIMITS_DC_NTE{10}; +const std::vector ItemData::StackLimits::DEFAULT_TOOL_LIMITS_V1_V2{10, 10, 1, 10, 10, 10, 10, 10, 10, 1}; +const std::vector ItemData::StackLimits::DEFAULT_TOOL_LIMITS_V3_V4{ + 10, 10, 1, 10, 10, 10, 10, 10, 10, 1, 1, 1, 1, 1, 1, 1, 99, 1}; const ItemData::StackLimits ItemData::StackLimits::DEFAULT_STACK_LIMITS_DC_NTE( Version::DC_NTE, ItemData::StackLimits::DEFAULT_TOOL_LIMITS_DC_NTE, 999999); diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index ab761de0..78603e2c 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -4735,7 +4735,9 @@ std::shared_ptr create_game_generic( if (quest_flag_rewrites && !quest_flag_rewrites->empty()) { IntegralExpression::Env env = { - .flags = &p->quest_flags.array(difficulty), + .section_id = game->effective_section_id(), + .difficulty = game->difficulty, + .flags = &p->quest_flags, .challenge_records = &p->challenge_records, .team = creator_c->team(), .num_players = 1, diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 9afa89b1..a2742289 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -33,7 +33,7 @@ inline uint8_t get_pre_v1_subcommand(Version v, uint8_t nte_subcommand, uint8_t } } -const std::unordered_set v2_crypt_initial_client_commands({ +const std::unordered_set v2_crypt_initial_client_commands{ 0x00260088, // (17) DCNTE license check 0x00B0008B, // (02) DCNTE login 0x00B0018B, // (02) DCNTE login (UDP off) @@ -52,20 +52,20 @@ const std::unordered_set v2_crypt_initial_client_commands({ 0x0130019D, // (02) DCv2/GCNTE extended login (UDP off) // Note: PSO PC initial commands are not listed here because we don't use a detector encryption for PSO PC // (instead, we use the split reconnect command to send PC to a different port). -}); -const std::unordered_set v3_crypt_initial_client_commands({ +}; +const std::unordered_set v3_crypt_initial_client_commands{ 0x00E000DB, // (17) GC/XB license check 0x00EC009E, // (02) GC login 0x00EC019E, // (02) GC login (UDP off) 0x0150009E, // (02) GC extended login 0x0150019E, // (02) GC extended login (UDP off) -}); +}; -const std::unordered_set bb_crypt_initial_client_commands({ +const std::unordered_set bb_crypt_initial_client_commands{ std::string("\xB4\x00\x93\x00\x00\x00\x00\x00", 8), std::string("\xAC\x00\x93\x00\x00\x00\x00\x00", 8), std::string("\xDC\x00\xDB\x00\x00\x00\x00\x00", 8), -}); +}; void send_command( std::shared_ptr c, diff --git a/src/ShopRandomSets.cc b/src/ShopRandomSets.cc index ef3bf273..a31e7071 100644 --- a/src/ShopRandomSets.cc +++ b/src/ShopRandomSets.cc @@ -515,7 +515,7 @@ struct WeaponRootT { U32T favored_grind_range_table; // {u32 min, u32 max}[6] } __packed_ws_be__(WeaponRootT, 0x20); -const std::array, 0x48> WeaponShopRandomSet::type_defs({ +const std::array, 0x48> WeaponShopRandomSet::type_defs{{ /* 00 */ {0x01, 0x00}, // Saber /* 01 */ {0x01, 0x01}, // Brand /* 02 */ {0x01, 0x02}, // Buster @@ -588,9 +588,9 @@ const std::array, 0x48> WeaponShopRandomSet::type_de /* 45 */ {0x0A, 0x05}, // MACE OF ADAMAN /* 46 */ {0x0C, 0x05}, // ICE STAFF:DAGON /* 47 */ {0x0B, 0x05}, // BRAVE HAMMER -}); +}}; -const std::array, 10> WeaponShopRandomSet::type_defs_39({ +const std::array, 10> WeaponShopRandomSet::type_defs_39{{ // Indexed by section_id {0x28, 0x00}, // HARISEN BATTLE FAN {0x2A, 0x00}, // AKIKO'S WOK @@ -602,9 +602,9 @@ const std::array, 10> WeaponShopRandomSet::type_defs {0x59, 0x00}, // BROOM {0x8A, 0x00}, // SANGE {0x99, 0x00}, // ANGEL HARP -}); +}}; -const std::array, 10> WeaponShopRandomSet::type_defs_3A({ +const std::array, 10> WeaponShopRandomSet::type_defs_3A{{ // Indexed by section_id {0x99, 0x00}, // ANGEL HARP {0x64, 0x00}, // CHAMELEON SCYTHE @@ -616,7 +616,7 @@ const std::array, 10> WeaponShopRandomSet::type_defs {0x2A, 0x00}, // AKIKO'S WOK {0x48, 0x00}, // SAMBA MARACAS {0x35, 0x00}, // CRAZY TUNE -}); +}}; const std::array WeaponShopRandomSet::bonus_values{ -50, -45, -40, -35, -30, -25, -20, -15, -10, -5, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50}; diff --git a/src/StaticGameData.cc b/src/StaticGameData.cc index a703149e..7922b385 100644 --- a/src/StaticGameData.cc +++ b/src/StaticGameData.cc @@ -121,13 +121,13 @@ static const std::array section_id_to_name = { static const std::array section_id_to_abbreviation = { "Vir", "Grn", "Sky", "Blu", "Prp", "Pnk", "Red", "Orn", "Ylw", "Wht"}; -const std::unordered_map name_to_section_id({{"viridia", 0}, +const std::unordered_map name_to_section_id{ // Greennill is spelled Greenill in some places, so we accept both spellings - {"greennill", 1}, {"greenill", 1}, {"skyly", 2}, {"bluefull", 3}, {"purplenum", 4}, {"pinkal", 5}, {"redria", 6}, - {"oran", 7}, {"yellowboze", 8}, {"whitill", 9}, + {"viridia", 0}, {"greennill", 1}, {"greenill", 1}, {"skyly", 2}, {"bluefull", 3}, {"purplenum", 4}, {"pinkal", 5}, + {"redria", 6}, {"oran", 7}, {"yellowboze", 8}, {"whitill", 9}, // Shortcuts for chat commands - {"b", 3}, {"g", 1}, {"o", 7}, {"pi", 5}, {"pu", 4}, {"r", 6}, {"s", 2}, {"v", 0}, {"w", 9}, {"y", 8}}); + {"b", 3}, {"g", 1}, {"o", 7}, {"pi", 5}, {"pu", 4}, {"r", 6}, {"s", 2}, {"v", 0}, {"w", 9}, {"y", 8}}; const std::vector lobby_event_to_name = { "none", "xmas", "none", "val", "easter", "hallo", "sonic", "newyear", @@ -673,10 +673,10 @@ char char_for_challenge_rank(uint8_t rank) { return "BAS"[rank]; } -const std::array DEFAULT_MIN_LEVELS_V123({0, 19, 39, 79}); -const std::array DEFAULT_MIN_LEVELS_V4_EP1({0, 19, 39, 79}); -const std::array DEFAULT_MIN_LEVELS_V4_EP2({0, 29, 49, 89}); -const std::array DEFAULT_MIN_LEVELS_V4_EP4({0, 39, 79, 109}); +const std::array DEFAULT_MIN_LEVELS_V123{{0, 19, 39, 79}}; +const std::array DEFAULT_MIN_LEVELS_V4_EP1{{0, 19, 39, 79}}; +const std::array DEFAULT_MIN_LEVELS_V4_EP2{{0, 29, 49, 89}}; +const std::array DEFAULT_MIN_LEVELS_V4_EP4{{0, 39, 79, 109}}; const std::array ALL_GAME_MODES_V1 = {GameMode::NORMAL, GameMode::BATTLE}; const std::array ALL_GAME_MODES_V23 = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE}; diff --git a/system/config.example.json b/system/config.example.json index 1f5cd287..00db2c0a 100644 --- a/system/config.example.json +++ b/system/config.example.json @@ -1338,35 +1338,35 @@ // doesn't have direct access to the client's quest flags from their save file. // If you use an expression, the format is the same as the AvailableIf and EnabledIf fields in quest JSONs (see // system/quests/retrieval/q058.json for details). Note that the expression is only evaluated at the time the game is - // created, and the player-specific tokens like C_EpX_YY refer to the player who created the game. + // created, and the player-specific tokens like CC_EpX_YY refer to the player who created the game. // The UnlockAllAreas option is now gone; if you want the same behavior as if it were enabled, uncomment all the - // "area unlocks" lines below. Note that some late PSOBB client versions (for example, the Tethealla client) open all + // "area unlock" lines below. Note that some late PSOBB client versions (for example, the Tethealla client) open all // areas by default, so the area unlock flags have no effect for them. "QuestFlagRewritesV1V2": { - // "F_0017": true, // Ep1 area unlocks - // "F_0020": true, // Ep1 area unlocks - // "F_002A": true, // Ep1 area unlocks + // "F_0017": true, // Ep1 area unlock (Caves) + // "F_0020": true, // Ep1 area unlock (Mines) + // "F_002A": true, // Ep1 area unlock (Ruins) }, "QuestFlagRewritesV3": { - // "F_0017": true, // Ep1 area unlocks - // "F_0020": true, // Ep1 area unlocks - // "F_002A": true, // Ep1 area unlocks - // "F_004C": true, // Ep2 area unlocks - // "F_004F": true, // Ep2 area unlocks - // "F_0052": true, // Ep2 area unlocks + // "F_0017": true, // Ep1 area unlock (Caves) + // "F_0020": true, // Ep1 area unlock (Mines) + // "F_002A": true, // Ep1 area unlock (Ruins) + // "F_004C": true, // Ep2 area unlock (VR Spaceship) + // "F_004F": true, // Ep2 area unlock (CCA) + // "F_0052": true, // Ep2 area unlock (Seabed) }, "QuestFlagRewritesV4": { - // "F_01F9": true, // Ep1 area unlocks - // "F_0201": true, // Ep1 area unlocks - // "F_0207": true, // Ep1 area unlocks - // "F_021B": true, // Ep2 area unlocks - // "F_0225": true, // Ep2 area unlocks - // "F_022F": true, // Ep2 area unlocks - // "F_02BD": true, // Ep4 area unlocks - // "F_02BE": true, // Ep4 area unlocks - // "F_02BF": true, // Ep4 area unlocks - // "F_02C0": true, // Ep4 area unlocks - // "F_02C1": true, // Ep4 area unlocks + // "F_01F9": true, // Ep1 area unlock (Caves) + // "F_0201": true, // Ep1 area unlock (Mines) + // "F_0207": true, // Ep1 area unlock (Ruins) + // "F_021B": true, // Ep2 area unlock (VR Spaceship) + // "F_0225": true, // Ep2 area unlock (CCA) + // "F_022F": true, // Ep2 area unlock (Seabed) + // "F_02BD": true, // Ep4 area unlock (Crater West) + // "F_02BE": true, // Ep4 area unlock (Crater South) + // "F_02BF": true, // Ep4 area unlock (Crater North) + // "F_02C0": true, // Ep4 area unlock (Crater Interior) + // "F_02C1": true, // Ep4 area unlock (Desert) "F_0046": false, // Ep2 CCA door lock fix "F_0047": false, // Ep2 CCA door lock fix "F_0048": false, // Ep2 CCA door lock fix diff --git a/system/quests/retrieval/q058.json b/system/quests/retrieval/q058.json index 68c1c6de..2e71fd98 100644 --- a/system/quests/retrieval/q058.json +++ b/system/quests/retrieval/q058.json @@ -5,7 +5,10 @@ // Quests that are considered unavailable don't appear in the quest menu at all. To set a condition for a quest to be // available, you can set AvailableIf in the quest's JSON file. The value for AvailableIf should be an expression // that tests any of the following: - // F_XXXX: Quest flag specified in hex (e.g. F_014D) + // P_SID: Current effective section ID for the game; see name_to_section_id in StaticGameData.cc for values + // F_XXXX: Quest flag specified in hex (e.g. F_014D) for the difficulty the player is currently playing in + // F_N_XXXX, F_H_XXXX, F_V_XXXX, F_U_XXXX: Same as F_XXXX, but read from a specific difficulty regardless of which + // difficulty the player is currently playing in // CC_EpX_Y: Whether or not Challenge stage X in Episode Y is complete (e.g. CC_Ep1_7) // T_ZZZ: Whether or not the player's BB team has reward with key ZZZ (as defined in TeamRewards in config.json) // V_NumPlayers: The number of players in the current game From 629e2bb4cdacede744cd8032ef39a1192a9feebb Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sun, 14 Jun 2026 09:24:56 -0700 Subject: [PATCH 8/9] make replay tests run in parallel and share immutable data --- CMakeLists.txt | 17 +- src/ChatCommands.cc | 123 +- src/Client.cc | 38 +- src/DNSServer.cc | 9 +- src/DNSServer.hh | 2 +- src/DataIndex.cc | 2037 ++++++++++++++++++++++ src/DataIndex.hh | 412 +++++ src/Episode3/Tournament.hh | 2 +- src/GameServer.cc | 4 +- src/HTTPServer.cc | 48 +- src/IPStackSimulator.cc | 14 +- src/Items.cc | 22 +- src/Lobby.cc | 28 +- src/Lobby.hh | 2 +- src/Main.cc | 474 ++--- src/ProxyCommands.cc | 8 +- src/ProxySession.cc | 16 +- src/ProxySession.hh | 2 +- src/ReceiveCommands.cc | 283 +-- src/ReceiveSubcommands.cc | 213 ++- src/ReplaySession.cc | 107 +- src/ReplaySession.hh | 10 +- src/SendCommands.cc | 124 +- src/ServerState.cc | 2094 +---------------------- src/ServerState.hh | 388 +---- src/ShellCommands.cc | 57 +- src/SignalWatcher.cc | 10 +- tests/DC-11-2000-GameSmokeTest.test.txt | 1 + 28 files changed, 3357 insertions(+), 3188 deletions(-) create mode 100644 src/DataIndex.cc create mode 100644 src/DataIndex.hh diff --git a/CMakeLists.txt b/CMakeLists.txt index d3d08b64..052c1e68 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,6 +67,7 @@ set(SOURCES src/DCSerialNumbers.cc src/DNSServer.cc src/DOLFileIndex.cc + src/DataIndex.cc src/DownloadSession.cc src/EnemyType.cc src/Episode3/AssistServer.cc @@ -157,17 +158,11 @@ add_dependencies(newserv newserv-Revision-cc) enable_testing() file(GLOB LOG_TEST_CASES ${CMAKE_SOURCE_DIR}/tests/*.test.txt) -foreach(LOG_TEST_CASE IN ITEMS ${LOG_TEST_CASES}) - add_test( - NAME ${LOG_TEST_CASE} - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - COMMAND ${CMAKE_BINARY_DIR}/newserv --replay-log=${LOG_TEST_CASE} --config=${CMAKE_SOURCE_DIR}/tests/config.json) -endforeach() -# list(TRANSFORM LOG_TEST_CASES PREPEND "--replay-log=" OUTPUT_VARIABLE LOG_REPLAY_ARGS) -# add_test( -# NAME "log-replays" -# WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} -# COMMAND ${CMAKE_BINARY_DIR}/newserv --config=${CMAKE_SOURCE_DIR}/tests/config.json ${LOG_REPLAY_ARGS}) +list(TRANSFORM LOG_TEST_CASES PREPEND "--replay-log=" OUTPUT_VARIABLE LOG_REPLAY_ARGS) +add_test( + NAME "log-replays" + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMAND ${CMAKE_BINARY_DIR}/newserv --parallel --config=${CMAKE_SOURCE_DIR}/tests/config.json ${LOG_REPLAY_ARGS}) file(GLOB SCRIPT_TEST_CASES ${CMAKE_SOURCE_DIR}/tests/*.test.sh) foreach(SCRIPT_TEST_CASE IN ITEMS ${SCRIPT_TEST_CASES}) diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index ce9b00dc..29ec6240 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -142,7 +142,7 @@ struct Args { void check_cheat_mode_available(bool behavior_is_cheating) const { if (behavior_is_cheating && this->check_permissions && - (this->c->require_server_state()->cheat_mode_behavior == ServerState::BehaviorSwitch::OFF) && + (this->c->require_server_state()->data->cheat_mode_behavior == DataIndex::BehaviorSwitch::OFF) && (!this->c->login || !this->c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE))) { throw precondition_failed("$C6Cheats are disabled"); } @@ -219,7 +219,7 @@ static asio::awaitable server_command_announce_inner(const Args& a, bool m auto s = a.c->require_server_state(); if (anonymous) { if (mail) { - send_simple_mail(s, 0, s->name, a.text); + send_simple_mail(s, 0, s->data->name, a.text); } else { send_text_or_scrolling_message(s, a.text, a.text); } @@ -472,8 +472,8 @@ static asio::awaitable server_command_bbchar_savechar(const Args& a, bool } else { dest_character_index = stoull(a.text) - 1; - if (dest_character_index >= s->num_backup_character_slots) { - throw precondition_failed("$C6Player index must\nbe in range 1-{}", s->num_backup_character_slots); + if (dest_character_index >= s->data->num_backup_character_slots) { + throw precondition_failed("$C6Player index must\nbe in range 1-{}", s->data->num_backup_character_slots); } dest_account = a.c->login->account; } @@ -514,13 +514,13 @@ static asio::awaitable server_command_bbchar_savechar(const Args& a, bool // Client sent 61; generate a BB-format player from the information we have and save that instead if (ch.character) { auto bb_player = PSOBBCharacterFile::create_from_config( - a.c->login->account->account_id, a.c->language(), ch.character->disp.visual, s->level_table(a.c->version())); + a.c->login->account->account_id, a.c->language(), ch.character->disp.visual, s->data->level_table(a.c->version())); bb_player->disp.visual.sh.version = 4; bb_player->disp.visual.sh.name_color_checksum = 0x00000000; bb_player->inventory = ch.character->inventory; // Before V3, player stats can't be correctly computed from other fields because material usage isn't stored // anywhere. For these versions, we have to trust the stats field from the player's data. - auto level_table = s->level_table(a.c->version()); + auto level_table = s->data->level_table(a.c->version()); if (is_v1_or_v2(a.c->version())) { bb_player->disp.stats = ch.character->disp.stats; bb_player->import_tethealla_material_usage(level_table); @@ -578,8 +578,8 @@ ChatCommandDefinition cc_cheat( auto s = a.c->require_server_state(); if (!l->check_flag(Lobby::Flag::CHEATS_ENABLED) && !a.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(a.c->version(), l->episode, l->difficulty); + s->data->cheat_flags.insufficient_minimum_level) { + size_t default_min_level = s->data->default_min_level_for_game(a.c->version(), l->episode, l->difficulty); if (l->min_level < default_min_level) { l->min_level = default_min_level; send_text_message_fmt(l, "$C6Minimum level set\nto {}", l->min_level + 1); @@ -603,7 +603,7 @@ ChatCommandDefinition cc_checkchar( std::vector flags; flags.emplace_back(false); - for (size_t z = 0; z < s->num_backup_character_slots; z++) { + for (size_t z = 0; z < s->data->num_backup_character_slots; z++) { std::string filename = a.c->backup_character_filename(a.c->login->account->account_id, z, is_ep3); flags.emplace_back(std::filesystem::is_regular_file(filename)); } @@ -615,8 +615,8 @@ ChatCommandDefinition cc_checkchar( } else { size_t index = stoull(a.text, nullptr, 0) - 1; - if (index >= s->num_backup_character_slots) { - throw precondition_failed("$C6Player index must\nbe in range 1-{}", s->num_backup_character_slots); + if (index >= s->data->num_backup_character_slots) { + throw precondition_failed("$C6Player index must\nbe in range 1-{}", s->data->num_backup_character_slots); } try { @@ -760,7 +760,7 @@ ChatCommandDefinition cc_dropmode( +[](const Args& a) -> asio::awaitable { a.check_is_game(true); auto s = a.c->require_server_state(); - a.check_cheats_enabled_or_allowed(s->cheat_flags.proxy_override_drops); + a.check_cheats_enabled_or_allowed(s->data->cheat_flags.proxy_override_drops); if (a.c->proxy_session) { @@ -881,7 +881,7 @@ ChatCommandDefinition cc_edit( } bool cheats_allowed = (!a.check_permissions || - (s->cheat_mode_behavior != ServerState::BehaviorSwitch::OFF) || + (s->data->cheat_mode_behavior != DataIndex::BehaviorSwitch::OFF) || a.c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE)); std::string encoded_args = phosg::tolower(a.text); @@ -891,28 +891,28 @@ ChatCommandDefinition cc_edit( try { auto p = a.c->character_file(); - if (tokens.at(0) == "atp" && (cheats_allowed || !s->cheat_flags.edit_stats)) { + if (tokens.at(0) == "atp" && (cheats_allowed || !s->data->cheat_flags.edit_stats)) { p->disp.stats.char_stats.atp = std::stoul(tokens.at(1)); - } else if (tokens.at(0) == "mst" && (cheats_allowed || !s->cheat_flags.edit_stats)) { + } else if (tokens.at(0) == "mst" && (cheats_allowed || !s->data->cheat_flags.edit_stats)) { p->disp.stats.char_stats.mst = std::stoul(tokens.at(1)); - } else if (tokens.at(0) == "evp" && (cheats_allowed || !s->cheat_flags.edit_stats)) { + } else if (tokens.at(0) == "evp" && (cheats_allowed || !s->data->cheat_flags.edit_stats)) { p->disp.stats.char_stats.evp = std::stoul(tokens.at(1)); - } else if (tokens.at(0) == "hp" && (cheats_allowed || !s->cheat_flags.edit_stats)) { + } else if (tokens.at(0) == "hp" && (cheats_allowed || !s->data->cheat_flags.edit_stats)) { p->disp.stats.char_stats.hp = std::stoul(tokens.at(1)); - } else if (tokens.at(0) == "dfp" && (cheats_allowed || !s->cheat_flags.edit_stats)) { + } else if (tokens.at(0) == "dfp" && (cheats_allowed || !s->data->cheat_flags.edit_stats)) { p->disp.stats.char_stats.dfp = std::stoul(tokens.at(1)); - } else if (tokens.at(0) == "ata" && (cheats_allowed || !s->cheat_flags.edit_stats)) { + } else if (tokens.at(0) == "ata" && (cheats_allowed || !s->data->cheat_flags.edit_stats)) { p->disp.stats.char_stats.ata = std::stoul(tokens.at(1)); - } else if (tokens.at(0) == "lck" && (cheats_allowed || !s->cheat_flags.edit_stats)) { + } else if (tokens.at(0) == "lck" && (cheats_allowed || !s->data->cheat_flags.edit_stats)) { p->disp.stats.char_stats.lck = std::stoul(tokens.at(1)); - } else if (tokens.at(0) == "meseta" && (cheats_allowed || !s->cheat_flags.edit_stats)) { + } else if (tokens.at(0) == "meseta" && (cheats_allowed || !s->data->cheat_flags.edit_stats)) { p->disp.stats.meseta = std::stoul(tokens.at(1)); - } else if (tokens.at(0) == "exp" && (cheats_allowed || !s->cheat_flags.edit_stats)) { + } else if (tokens.at(0) == "exp" && (cheats_allowed || !s->data->cheat_flags.edit_stats)) { p->disp.stats.exp = std::stoul(tokens.at(1)); - } else if (tokens.at(0) == "level" && (cheats_allowed || !s->cheat_flags.edit_stats)) { + } else if (tokens.at(0) == "level" && (cheats_allowed || !s->data->cheat_flags.edit_stats)) { p->disp.stats.level = std::stoul(tokens.at(1)) - 1; - p->recompute_stats(s->level_table(a.c->version()), true); - } else if (((tokens.at(0) == "material") || (tokens.at(0) == "mat")) && !is_v1_or_v2(a.c->version()) && (cheats_allowed || !s->cheat_flags.reset_materials)) { + p->recompute_stats(s->data->level_table(a.c->version()), true); + } else if (((tokens.at(0) == "material") || (tokens.at(0) == "mat")) && !is_v1_or_v2(a.c->version()) && (cheats_allowed || !s->data->cheat_flags.reset_materials)) { if (tokens.at(1) == "reset") { const auto& which = tokens.at(2); if (which == "power") { @@ -949,7 +949,7 @@ ChatCommandDefinition cc_edit( } else { throw precondition_failed("$C6Invalid subcommand"); } - p->recompute_stats(s->level_table(a.c->version()), false); + p->recompute_stats(s->data->level_table(a.c->version()), false); } else if (tokens.at(0) == "namecolor") { p->disp.visual.sh.name_color = std::stoul(tokens.at(1), nullptr, 16); } else if (tokens.at(0) == "language" || tokens.at(0) == "lang") { @@ -965,7 +965,7 @@ ChatCommandDefinition cc_edit( sys->language = new_language; } } else if (tokens.at(0) == "secid") { - if (!cheats_allowed && (p->disp.stats.level > 0) && s->cheat_flags.edit_section_id) { + if (!cheats_allowed && (p->disp.stats.level > 0) && s->data->cheat_flags.edit_section_id) { throw precondition_failed("$C6You cannot change\nyour Section ID\nafter level 1"); } uint8_t secid = section_id_for_name(tokens.at(1)); @@ -991,7 +991,7 @@ ChatCommandDefinition cc_edit( p->disp.visual.sh.extra_model = npc; p->disp.visual.sh.validation_flags |= 0x02; } - } else if (tokens.at(0) == "tech" && (cheats_allowed || !s->cheat_flags.edit_stats)) { + } else if (tokens.at(0) == "tech" && (cheats_allowed || !s->data->cheat_flags.edit_stats)) { uint8_t level = std::stoul(tokens.at(2)) - 1; if (tokens.at(1) == "all") { for (size_t x = 0; x < 0x14; x++) { @@ -1106,7 +1106,7 @@ ChatCommandDefinition cc_exit( auto s = a.c->require_server_state(); std::shared_ptr fn; try { - fn = s->client_functions->get("ExitAnywhere", a.c->specific_version); + fn = s->data->client_functions->get("ExitAnywhere", a.c->specific_version); } catch (const std::out_of_range&) { } if (fn) { @@ -1154,7 +1154,7 @@ ChatCommandDefinition cc_infhp( send_text_message(a.c, "$C6Infinite HP disabled"); } else { auto s = a.c->require_server_state(); - a.check_cheats_enabled_or_allowed(s->cheat_flags.infinite_hp_tp); + a.check_cheats_enabled_or_allowed(s->data->cheat_flags.infinite_hp_tp); a.c->set_flag(Client::Flag::INFINITE_HP_ENABLED); co_await send_remove_negative_conditions(a.c); if (a.c->proxy_session) { @@ -1202,7 +1202,7 @@ ChatCommandDefinition cc_inftp( send_text_message(a.c, "$C6Infinite TP disabled"); } else { auto s = a.c->require_server_state(); - a.check_cheats_enabled_or_allowed(s->cheat_flags.infinite_hp_tp); + a.check_cheats_enabled_or_allowed(s->data->cheat_flags.infinite_hp_tp); a.c->set_flag(Client::Flag::INFINITE_TP_ENABLED); send_text_message(a.c, "$C6Infinite TP enabled"); } @@ -1214,7 +1214,7 @@ ChatCommandDefinition cc_item( +[](const Args& a) -> asio::awaitable { a.check_is_game(true); auto s = a.c->require_server_state(); - a.check_cheats_enabled_or_allowed(s->cheat_flags.create_items); + a.check_cheats_enabled_or_allowed(s->data->cheat_flags.create_items); ItemData item; bool was_enqueued = false; @@ -1225,12 +1225,12 @@ ChatCommandDefinition cc_item( a.check_is_leader(); if (a.text.starts_with("!")) { - item = s->parse_item_description(a.c->version(), a.text.substr(1)); + item = s->data->parse_item_description(a.c->version(), a.text.substr(1)); a.c->proxy_session->next_drop_item = item; was_enqueued = true; } else { - item = s->parse_item_description(a.c->version(), a.text); + item = s->data->parse_item_description(a.c->version(), a.text); item.id = phosg::random_object() | 0x80000000; send_drop_stacked_item_to_channel(s, a.c->channel, item, a.c->floor, a.c->pos); @@ -1239,7 +1239,7 @@ ChatCommandDefinition cc_item( } else { auto l = a.c->require_lobby(); - item = s->parse_item_description(a.c->version(), a.text); + item = s->data->parse_item_description(a.c->version(), a.text); item.id = l->generate_item_id(0xFF); if ((l->drop_mode == ServerDropMode::SERVER_PRIVATE) || (l->drop_mode == ServerDropMode::SERVER_DUPLICATE)) { @@ -1251,7 +1251,7 @@ ChatCommandDefinition cc_item( } } - std::string name = s->describe_item(a.c->version(), item, ItemNameIndex::Flag::INCLUDE_PSO_COLOR_ESCAPES); + std::string name = s->data->describe_item(a.c->version(), item, ItemNameIndex::Flag::INCLUDE_PSO_COLOR_ESCAPES); if (was_enqueued) { send_text_message(a.c, "$C7Next item:\n" + name); } else { @@ -1340,7 +1340,7 @@ ChatCommandDefinition cc_killcount( auto s = a.c->require_server_state(); for (size_t z : item_indexes) { const auto& item = p->inventory.items[z]; - std::string name = s->describe_item( + std::string name = s->data->describe_item( a.c->version(), item.data, ItemNameIndex::Flag::INCLUDE_PSO_COLOR_ESCAPES | ItemNameIndex::Flag::NAME_ONLY); send_text_message_fmt(a.c, "{}$C7: {} kills", name, item.data.get_kill_count()); } @@ -1530,8 +1530,8 @@ ChatCommandDefinition cc_loadchar( auto l = a.c->require_lobby(); size_t index = stoull(a.text, nullptr, 0) - 1; - if (index >= s->num_backup_character_slots) { - throw precondition_failed("$C6Player index must\nbe in range 1-{}", s->num_backup_character_slots); + if (index >= s->data->num_backup_character_slots) { + throw precondition_failed("$C6Player index must\nbe in range 1-{}", s->data->num_backup_character_slots); } std::shared_ptr ep3_char; @@ -1563,7 +1563,7 @@ ChatCommandDefinition cc_loadchar( auto send_set_extended_player_info = [&a, &s](const CharT& char_file) -> asio::awaitable { co_await prepare_client_for_patches(a.c); try { - auto fn = s->client_functions->get("SetExtendedPlayerInfo", a.c->specific_version); + auto fn = s->data->client_functions->get("SetExtendedPlayerInfo", a.c->specific_version); co_await send_function_call(a.c, fn, {}, &char_file, sizeof(CharT)); auto l = a.c->lobby.lock(); if (l) { @@ -1699,7 +1699,7 @@ ChatCommandDefinition cc_makeobj( co_await prepare_client_for_patches(a.c); auto s = a.c->require_server_state(); - auto fn = s->client_functions->get("CreateObject", a.c->specific_version); + auto fn = s->data->client_functions->get("CreateObject", a.c->specific_version); co_await send_function_call(a.c, fn, label_writes); }); @@ -1760,8 +1760,8 @@ ChatCommandDefinition cc_minlevel( auto s = a.c->require_server_state(); bool cheats_allowed = (l->check_flag(Lobby::Flag::CHEATS_ENABLED) || a.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(a.c->version(), l->episode, l->difficulty); + if (!cheats_allowed && s->data->cheat_flags.insufficient_minimum_level) { + size_t default_min_level = s->data->default_min_level_for_game(a.c->version(), l->episode, l->difficulty); if (new_min_level < default_min_level) { throw precondition_failed("$C6Cannot set minimum\nlevel below {}", default_min_level + 1); } @@ -1777,7 +1777,7 @@ ChatCommandDefinition cc_next( +[](const Args& a) -> asio::awaitable { a.check_is_game(true); auto s = a.c->require_server_state(); - a.check_cheats_enabled_or_allowed(s->cheat_flags.warp); + a.check_cheats_enabled_or_allowed(s->data->cheat_flags.warp); auto episode = a.c->proxy_session ? a.c->proxy_session->lobby_episode : a.c->require_lobby()->episode; size_t limit = FloorDefinition::limit_for_episode(episode); @@ -1838,7 +1838,7 @@ ChatCommandDefinition cc_patch( try { auto s = a.c->require_server_state(); // Note: We can't look this up before prepare_client_for_patches because specific_version may not be set - auto fn = s->client_functions->get(patch_name, a.c->specific_version); + auto fn = s->data->client_functions->get(patch_name, a.c->specific_version); switch (fn->visibility) { case ClientFunctionIndex::Function::Visibility::DEBUG_ONLY: @@ -2060,7 +2060,7 @@ ChatCommandDefinition cc_qfread( uint8_t counter_index; uint32_t mask; try { - const auto& def = s->quest_counter_fields.at(a.text); + const auto& def = s->data->quest_counter_fields.at(a.text); counter_index = def.first; mask = def.second; } catch (const std::out_of_range&) { @@ -2189,7 +2189,7 @@ ChatCommandDefinition cc_quest( a.check_is_game(true); auto s = a.c->require_server_state(); - auto q = s->quest_index->get(stoul(a.text)); + auto q = s->data->quest_index->get(stoul(a.text)); if (!q) { throw precondition_failed("$C6Quest not found"); } @@ -2229,7 +2229,7 @@ ChatCommandDefinition cc_fastkill( send_text_message(a.c, "$C6Fast kills disabled"); } else { auto s = a.c->require_server_state(); - a.check_cheats_enabled_or_allowed(s->cheat_flags.fast_kills); + a.check_cheats_enabled_or_allowed(s->data->cheat_flags.fast_kills); a.c->set_flag(Client::Flag::FAST_KILLS_ENABLED); send_text_message(a.c, "$C6Fast kills enabled"); } @@ -2251,7 +2251,7 @@ ChatCommandDefinition cc_rand( auto s = a.c->require_server_state(); auto l = a.c->require_lobby(); a.check_is_game(false); - a.check_cheats_enabled_or_allowed(s->cheat_flags.override_random_seed); + a.check_cheats_enabled_or_allowed(s->data->cheat_flags.override_random_seed); if (a.text.empty()) { a.c->override_random_seed = -1; @@ -2288,7 +2288,7 @@ ChatCommandDefinition cc_readmem( std::shared_ptr fn; try { auto s = a.c->require_server_state(); - fn = s->client_functions->get("ReadMemoryWord", a.c->specific_version); + fn = s->data->client_functions->get("ReadMemoryWord", a.c->specific_version); } catch (const std::out_of_range&) { throw precondition_failed("Invalid patch name"); } @@ -2332,7 +2332,7 @@ ChatCommandDefinition cc_savefiles( a.check_is_proxy(true); auto s = a.c->require_server_state(); - if (!s->proxy_allow_save_files) { + if (!s->data->proxy_allow_save_files) { send_text_message(a.c, "$C6Save files is not\nallowed"); } else if (a.c->check_flag(Client::Flag::PROXY_SAVE_FILES)) { a.c->clear_flag(Client::Flag::PROXY_SAVE_FILES); @@ -2406,7 +2406,7 @@ ChatCommandDefinition cc_secid( {"$secid"}, +[](const Args& a) -> asio::awaitable { auto s = a.c->require_server_state(); - a.check_cheats_enabled_or_allowed(s->cheat_flags.override_section_id); + a.check_cheats_enabled_or_allowed(s->data->cheat_flags.override_section_id); uint8_t new_override_section_id; if (a.text.empty()) { @@ -2439,7 +2439,7 @@ ChatCommandDefinition cc_setassist( a.check_is_game(true); a.check_is_ep3(true); auto s = a.c->require_server_state(); - a.check_cheats_enabled_in_game(s->cheat_flags.ep3_replace_assist); + a.check_cheats_enabled_in_game(s->data->cheat_flags.ep3_replace_assist); auto l = a.c->require_lobby(); if (l->episode != Episode::EP3) { @@ -2486,7 +2486,7 @@ ChatCommandDefinition cc_server_info( {"$si"}, +[](const Args& a) -> asio::awaitable { auto s = a.c->require_server_state(); - std::string uptime_str = phosg::format_duration(phosg::now() - s->creation_time); + std::string uptime_str = phosg::format_duration(phosg::now() - s->data->creation_time); send_text_message_fmt(a.c, "Uptime: $C6{}$C7\nLobbies: $C6{}$C7\nClients: $C6{}$C7(g) $C6{}$C7(p)", uptime_str, @@ -2847,7 +2847,7 @@ ChatCommandDefinition cc_unset( a.check_is_game(true); a.check_is_ep3(true); auto s = a.c->require_server_state(); - a.check_cheats_enabled_in_game(s->cheat_flags.ep3_unset_field_character); + a.check_cheats_enabled_in_game(s->data->cheat_flags.ep3_unset_field_character); auto l = a.c->require_lobby(); if (l->episode != Episode::EP3) { throw std::logic_error("non-Ep3 client in Ep3 game"); @@ -2875,7 +2875,7 @@ ChatCommandDefinition cc_variations( a.check_is_proxy(false); a.check_is_game(false); auto s = a.c->require_server_state(); - a.check_cheats_enabled_in_game(s->cheat_flags.override_variations); + a.check_cheats_enabled_in_game(s->data->cheat_flags.override_variations); a.c->override_variations = std::make_unique(); for (size_t z = 0; z < std::min(a.c->override_variations->entries.size() * 2, a.text.size()); z++) { @@ -2894,7 +2894,7 @@ ChatCommandDefinition cc_variations( static void command_warp(const Args& a, bool is_warpall) { a.check_is_game(true); auto s = a.c->require_server_state(); - a.check_cheats_enabled_or_allowed(s->cheat_flags.warp); + a.check_cheats_enabled_or_allowed(s->data->cheat_flags.warp); uint32_t floor = std::stoul(a.text, nullptr, 0); if (!is_warpall && (a.c->floor == floor)) { @@ -2968,7 +2968,8 @@ ChatCommandDefinition cc_what( } else { auto s = a.c->require_server_state(); send_text_message( - a.c, s->describe_item(a.c->version(), nearest_fi->data, ItemNameIndex::Flag::INCLUDE_PSO_COLOR_ESCAPES)); + a.c, + s->data->describe_item(a.c->version(), nearest_fi->data, ItemNameIndex::Flag::INCLUDE_PSO_COLOR_ESCAPES)); } co_return; }); @@ -3002,7 +3003,7 @@ static void whatobj_whatene_fn(const Args& a, bool include_objs, bool include_en VectorXYZF worldspace_pos; if (l->episode != Episode::EP3) { try { - const auto& room = s->room_layout_index->get_room(area, layout_var, def.set_entry->room); + const auto& room = s->data->room_layout_index->get_room(area, layout_var, def.set_entry->room); // This is the order in which the game does the rotations; not sure why worldspace_pos = def.set_entry->pos.rotate_x(room.angle.x).rotate_z(room.angle.z).rotate_y(room.angle.y) + room.position; } catch (const std::out_of_range&) { @@ -3141,7 +3142,7 @@ ChatCommandDefinition cc_writemem( try { auto s = a.c->require_server_state(); - auto fn = s->client_functions->get("WriteMemory", a.c->specific_version); + auto fn = s->data->client_functions->get("WriteMemory", a.c->specific_version); std::unordered_map label_writes{{"dest_addr", addr}, {"size", data.size()}}; co_await send_function_call(a.c, fn, label_writes, data.data(), data.size()); } catch (const std::out_of_range&) { @@ -3181,7 +3182,7 @@ ChatCommandDefinition cc_nativecall( try { auto s = a.c->require_server_state(); - auto fn = s->client_functions->get("CallNativeFunction", a.c->specific_version); + auto fn = s->data->client_functions->get("CallNativeFunction", a.c->specific_version); co_await send_function_call(a.c, fn, label_writes); } catch (const std::out_of_range&) { throw precondition_failed("Invalid patch name"); diff --git a/src/Client.cc b/src/Client.cc index c6e7567c..8c5c7538 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -191,7 +191,7 @@ Client::Client( // Don't print data sent to patch clients to the logs. The patch server protocol is fully understood and data logs // for patch clients are generally more annoying than helpful at this point. auto s = server->get_state(); - if (is_patch(this->version()) && s->hide_download_commands) { + if (is_patch(this->version()) && s->data->hide_download_commands) { this->channel->terminal_recv_color = phosg::TerminalFormat::END; this->channel->terminal_send_color = phosg::TerminalFormat::END; } else { @@ -200,7 +200,7 @@ Client::Client( } this->set_flags_for_version(this->version(), -1); - if (is_v1_or_v2(this->version()) ? s->default_rare_notifs_enabled_v1_v2 : s->default_rare_notifs_enabled_v3_v4) { + if (is_v1_or_v2(this->version()) ? s->data->default_rare_notifs_enabled_v1_v2 : s->data->default_rare_notifs_enabled_v3_v4) { this->set_drop_notification_mode(ItemDropNotificationMode::RARES_ONLY); } this->specific_version = default_specific_version_for_version(this->version(), -1); @@ -210,7 +210,7 @@ Client::Client( // Don't print data sent to patch clients to the logs. The patch server protocol is fully understood and data logs // for patch clients are generally more annoying than helpful at this point. - if ((s->hide_download_commands) && + if ((s->data->hide_download_commands) && ((this->version() == Version::PC_PATCH) || (this->version() == Version::BB_PATCH))) { this->channel->terminal_recv_color = phosg::TerminalFormat::END; this->channel->terminal_send_color = phosg::TerminalFormat::END; @@ -267,7 +267,7 @@ void Client::reschedule_save_game_data_timer() { void Client::reschedule_ping_and_timeout_timers() { auto s = this->require_server_state(); if (!is_patch(this->version())) { - this->send_ping_timer.expires_after(std::chrono::microseconds(s->client_ping_interval_usecs)); + this->send_ping_timer.expires_after(std::chrono::microseconds(s->data->client_ping_interval_usecs)); this->send_ping_timer.async_wait([this](std::error_code ec) { if (!ec) { this->log.info_f("Sending ping command"); @@ -282,7 +282,7 @@ void Client::reschedule_ping_and_timeout_timers() { }); } - this->idle_timeout_timer.expires_after(std::chrono::microseconds(s->client_idle_timeout_usecs)); + this->idle_timeout_timer.expires_after(std::chrono::microseconds(s->data->client_idle_timeout_usecs)); this->idle_timeout_timer.async_wait([this](std::error_code ec) { if (!ec) { this->log.info_f("Idle timeout expired"); @@ -295,7 +295,7 @@ void Client::convert_account_to_temporary_if_nte() { // If the session is a prototype version and the account was created and we should use a temporary account instead, // delete the permanent account and replace it with a temporary account. auto s = this->require_server_state(); - if (s->use_temp_accounts_for_prototypes && this->login->account_was_created && is_any_nte(this->version())) { + if (s->data->use_temp_accounts_for_prototypes && this->login->account_was_created && is_any_nte(this->version())) { this->log.info_f("Client is a prototype version and the account was created during this session; converting permanent account to temporary account"); this->login->account->is_temporary = true; this->login->account->delete_file(); @@ -431,14 +431,14 @@ bool Client::can_use_chat_commands() const { if (this->login->account->check_flag(Account::Flag::ALWAYS_ENABLE_CHAT_COMMANDS)) { return true; } - return this->require_server_state()->enable_chat_commands; + return this->require_server_state()->data->enable_chat_commands; } void Client::set_login(std::shared_ptr login) { this->login = login; auto s = this->require_server_state(); - if (!s->allow_same_account_concurrent_logins) { + if (!s->data->allow_same_account_concurrent_logins) { auto it = s->client_for_account.find(login->account->account_id); if ((it != s->client_for_account.end()) && (it->second.get() != this)) { if (it->second->channel) { @@ -796,8 +796,8 @@ std::shared_ptr Client::bank_file(bool allow_load) { } auto s = this->require_server_state(); - this->bank_data->max_items = s->bb_max_bank_items; - this->bank_data->max_meseta = s->bb_max_bank_meseta; + this->bank_data->max_items = s->data->bb_max_bank_items; + this->bank_data->max_meseta = s->data->bb_max_bank_meseta; this->update_bank_data_after_load(this->bank_data); } return this->bank_data; @@ -1000,11 +1000,11 @@ void Client::load_all_files() { if (!this->system_data) { this->system_data = std::make_shared(); auto s = this->require_server_state(); - if (s->bb_default_keyboard_config) { - this->system_data->key_config = *s->bb_default_keyboard_config; + if (s->data->bb_default_keyboard_config) { + this->system_data->key_config = *s->data->bb_default_keyboard_config; } - if (s->bb_default_joystick_config) { - this->system_data->joystick_config = *s->bb_default_joystick_config; + if (s->data->bb_default_joystick_config) { + this->system_data->joystick_config = *s->data->bb_default_joystick_config; } this->log.info_f("Created new system data"); } @@ -1014,7 +1014,7 @@ void Client::load_all_files() { } auto s = this->require_server_state(); - auto stack_limits = s->item_stack_limits(this->version()); + auto stack_limits = s->data->item_stack_limits(this->version()); this->blocked_senders.clear(); for (size_t z = 0; z < this->guild_card_data->blocked_senders.size(); z++) { @@ -1039,7 +1039,7 @@ void Client::load_all_files() { } void Client::update_character_data_after_load(std::shared_ptr charfile) { - charfile->import_tethealla_material_usage(this->require_server_state()->level_table(this->version())); + charfile->import_tethealla_material_usage(this->require_server_state()->data->level_table(this->version())); Language lang = this->language(); this->log.info_f("Overriding language fields in save files with {}", name_for_language(lang)); @@ -1049,7 +1049,7 @@ void Client::update_character_data_after_load(std::shared_ptr bank) { auto s = this->require_server_state(); - auto limits = s->item_stack_limits(this->version()); + auto limits = s->data->item_stack_limits(this->version()); for (auto& item : bank->items) { if (item.data.is_stackable(*limits)) { if (item.data.data1[5] != item.amount) { @@ -1121,7 +1121,7 @@ void Client::print_inventory() const { for (size_t x = 0; x < p->inventory.num_items; x++) { const auto& item = p->inventory.items[x]; auto hex = item.data.hex(); - auto name = s->describe_item(this->version(), item.data); + auto name = s->data->describe_item(this->version(), item.data); this->log.info_f("[PlayerInventory] {:2}: [+{:08X}] {} ({})", x, item.flags, hex, name); } } @@ -1135,7 +1135,7 @@ void Client::print_bank() const { const auto& item = this->bank_data->items[x]; const char* present_token = item.present ? "" : " (missing present flag)"; auto hex = item.data.hex(); - auto name = s->describe_item(this->version(), item.data); + auto name = s->data->describe_item(this->version(), item.data); this->log.info_f("[PlayerBank] {:3}: {} ({}) (x{}){}", x, hex, name, item.amount, present_token); } } else { diff --git a/src/DNSServer.cc b/src/DNSServer.cc index 1cbc1dde..d1879720 100644 --- a/src/DNSServer.cc +++ b/src/DNSServer.cc @@ -14,8 +14,7 @@ #include "NetworkAddresses.hh" #include "ServerState.hh" -DNSServer::DNSServer(std::shared_ptr state) - : state(state) {} +DNSServer::DNSServer(std::shared_ptr state) : state(state) {} void DNSServer::listen(const std::string& addr, int port) { if (port == 0) { @@ -62,11 +61,11 @@ asio::awaitable DNSServer::dns_server_task(std::shared_ptrstate->banned_ipv4_ranges->check(sender_addr)) { + } else if (!this->state->data->banned_ipv4_ranges->check(sender_addr)) { input.resize(bytes); uint32_t connect_address = is_local_address(sender_addr) - ? this->state->local_address - : this->state->external_address; + ? this->state->data->local_address + : this->state->data->external_address; std::string response = this->response_for_query(input, connect_address); co_await sock->async_send_to(asio::buffer(response.data(), response.size()), sender_ep, asio::use_awaitable); } diff --git a/src/DNSServer.hh b/src/DNSServer.hh index 5ffcdecb..f3d35bbb 100644 --- a/src/DNSServer.hh +++ b/src/DNSServer.hh @@ -8,7 +8,7 @@ #include "IPV4RangeSet.hh" -struct ServerState; +class ServerState; class DNSServer { public: diff --git a/src/DataIndex.cc b/src/DataIndex.cc new file mode 100644 index 00000000..c4cfda2e --- /dev/null +++ b/src/DataIndex.cc @@ -0,0 +1,2037 @@ +#include "DataIndex.hh" + +#include + +#include +#include +#include +#include +#include +#include + +#include "Compression.hh" +#include "GameServer.hh" +#include "IPStackSimulator.hh" +#include "ImageEncoder.hh" +#include "Loggers.hh" +#include "NetworkAddresses.hh" +#include "SendCommands.hh" +#include "Text.hh" +#include "TextIndex.hh" + +#ifdef PHOSG_WINDOWS +static constexpr bool IS_WINDOWS = true; +#else +static constexpr bool IS_WINDOWS = false; +#endif + +DataIndex::CheatFlags::CheatFlags(const phosg::JSON& json) : CheatFlags() { + std::unordered_set enabled_keys; + for (const auto& it : json.as_list()) { + enabled_keys.emplace(it->as_string()); + } + + this->create_items = enabled_keys.count("CreateItems"); + this->edit_section_id = enabled_keys.count("EditSectionID"); + this->edit_stats = enabled_keys.count("EditStats"); + this->ep3_replace_assist = enabled_keys.count("Ep3ReplaceAssist"); + this->ep3_unset_field_character = enabled_keys.count("Ep3UnsetFieldCharacter"); + this->infinite_hp_tp = enabled_keys.count("InfiniteHPTP"); + this->fast_kills = enabled_keys.count("FastKills"); + this->insufficient_minimum_level = enabled_keys.count("InsufficientMinimumLevel"); + this->override_random_seed = enabled_keys.count("OverrideRandomSeed"); + this->override_section_id = enabled_keys.count("OverrideSectionID"); + this->override_variations = enabled_keys.count("OverrideVariations"); + this->proxy_override_drops = enabled_keys.count("ProxyOverrideDrops"); + this->reset_materials = enabled_keys.count("ResetMaterials"); + this->warp = enabled_keys.count("Warp"); +} + +DataIndex::DataIndex::QuestF960Result::QuestF960Result( + const phosg::JSON& json, std::shared_ptr name_index, const ItemData::StackLimits& limits) { + static const std::array day_names = { + "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; + this->meseta_cost = json.get_int("MesetaCost", 0); + this->base_probability = json.get_int("BaseProbability", 0); + this->probability_upgrade = json.get_int("ProbabilityUpgrade", 0); + for (size_t day = 0; day < 7; day++) { + for (const auto& item_it : json.get_list(day_names[day])) { + if (item_it->is_int()) { + this->results[day].emplace_back(ItemData::from_primary_identifier(limits, item_it->as_int())); + } else { + try { + this->results[day].emplace_back(name_index->parse_item_description(item_it->as_string())); + } catch (const std::exception& e) { + config_log.warning_f( + "Cannot parse item description \"{}\": {} (skipping entry)", item_it->as_string(), e.what()); + } + } + } + } +} + +DataIndex::DataIndex(const std::string& config_filename) : config_filename(config_filename) {} + +uint32_t DataIndex::connect_address_for_client(std::shared_ptr c) const { + { + auto ipss_channel = dynamic_pointer_cast(c->channel); + if (ipss_channel) { + auto ipss_c = ipss_channel->ipss_client.lock(); + if (!ipss_c) { + throw std::runtime_error("IPSS client is expired"); + } + return IPStackSimulator::connect_address_for_remote_address(ipss_c->ipv4_addr); + } + } + + { + auto socket_channel = dynamic_pointer_cast(c->channel); + if (socket_channel) { + uint32_t addr = ipv4_addr_for_asio_addr(socket_channel->remote_addr.address()); + uint32_t ret = is_local_address(addr) ? this->local_address : this->external_address; + return ret ? ret : addr; + } + } + + { + auto peer_channel = dynamic_pointer_cast(c->channel); + if (peer_channel) { + // This is used during replays; the "client" will ignore this and reconnect via another PeerChannel + return 0xEEEEEEEE; + } + } + + throw std::runtime_error("no connect address available"); +} + +uint16_t DataIndex::game_server_port_for_version(Version v) const { + switch (v) { + case Version::DC_NTE: + case Version::DC_11_2000: + case Version::DC_V1: + case Version::DC_V2: + case Version::GC_NTE: + case Version::GC_V3: + case Version::GC_EP3_NTE: + case Version::GC_EP3: + return this->name_to_port_config.at("gc-us3").port; + case Version::PC_NTE: + case Version::PC_V2: + return this->name_to_port_config.at("pc").port; + case Version::XB_V3: + return this->name_to_port_config.at("xb").port; + case Version::BB_V4: + return this->name_to_port_config.at("bb-data1").port; + default: + throw std::runtime_error("unknown version"); + } +} + +std::shared_ptr DataIndex::information_menu(Version version) const { + if (is_v1_or_v2(version)) { + return this->information_menu_v2; + } else if (is_v3(version)) { + return this->information_menu_v3; + } + throw std::out_of_range("no information menu exists for this version"); +} + +std::shared_ptr DataIndex::proxy_destinations_menu(Version version) const { + switch (version) { + case Version::DC_NTE: + case Version::DC_11_2000: + case Version::DC_V1: + case Version::DC_V2: + return this->proxy_destinations_menu_dc; + case Version::PC_NTE: + case Version::PC_V2: + return this->proxy_destinations_menu_pc; + case Version::GC_NTE: + case Version::GC_V3: + case Version::GC_EP3_NTE: + case Version::GC_EP3: + return this->proxy_destinations_menu_gc; + case Version::XB_V3: + return this->proxy_destinations_menu_xb; + default: + throw std::out_of_range("no proxy destinations menu exists for this version"); + } +} + +const std::vector>& DataIndex::proxy_destinations(Version version) const { + switch (version) { + case Version::DC_NTE: + case Version::DC_11_2000: + case Version::DC_V1: + case Version::DC_V2: + case Version::GC_NTE: + return this->proxy_destinations_dc; + case Version::PC_NTE: + case Version::PC_V2: + return this->proxy_destinations_pc; + case Version::GC_V3: + case Version::GC_EP3_NTE: + case Version::GC_EP3: + return this->proxy_destinations_gc; + case Version::XB_V3: + return this->proxy_destinations_xb; + default: + throw std::out_of_range("no proxy destinations menu exists for this version"); + } +} + +const std::vector& DataIndex::public_lobby_search_order(Version version, bool is_client_customization) const { + static_assert(NUM_VERSIONS == 14, "Don\'t forget to update the public lobby search orders in config.json"); + if (is_client_customization && !this->client_customization_public_lobby_search_order.empty()) { + return this->client_customization_public_lobby_search_order; + } + return this->public_lobby_search_orders.at(static_cast(version)); +} + +std::shared_ptr> DataIndex::information_contents_for_client(std::shared_ptr c) const { + return is_v1_or_v2(c->version()) ? this->information_contents_v2 : this->information_contents_v3; +} + +size_t DataIndex::default_min_level_for_game(Version version, Episode episode, Difficulty difficulty) const { + const auto& min_levels = is_v4(version) + ? this->min_levels_v4 + : is_v3(version) + ? this->min_levels_v3 + : this->min_levels_v1_v2; + switch (episode) { + case Episode::EP1: + return min_levels[0].at(static_cast(difficulty)); + case Episode::EP2: + return min_levels[1].at(static_cast(difficulty)); + case Episode::EP3: + return 0; + case Episode::EP4: + return min_levels[2].at(static_cast(difficulty)); + default: + throw std::runtime_error("invalid episode"); + } +} + +std::shared_ptr DataIndex::set_data_table( + Version version, Episode episode, GameMode mode, Difficulty difficulty) const { + bool use_ult_tables = ((episode == Episode::EP1) && (difficulty == Difficulty::ULTIMATE) && !is_v1(version) && (version != Version::PC_NTE)); + if (mode == GameMode::SOLO && is_v4(version)) { + return use_ult_tables ? this->bb_solo_set_data_table_ep1_ult : this->bb_solo_set_data_table; + } + + const auto& tables = use_ult_tables ? this->set_data_tables_ep1_ult : this->set_data_tables; + auto ret = tables.at(static_cast(version)); + if (ret == nullptr) { + throw std::runtime_error("no set data table exists for this version"); + } + return ret; +} + +std::shared_ptr DataIndex::level_table(Version version) const { + switch (version) { + case Version::DC_NTE: + case Version::DC_11_2000: + case Version::DC_V1: + case Version::DC_V2: + case Version::PC_NTE: + case Version::PC_V2: + case Version::GC_NTE: // TODO: Does NTE use the v2 table, the v3 table, or neither? + return this->level_table_v1_v2; + case Version::GC_V3: + case Version::GC_EP3_NTE: + case Version::GC_EP3: + case Version::XB_V3: + return this->level_table_v3; + case Version::BB_V4: + return this->level_table_v4; + default: + throw std::logic_error("level table not available for version"); + } +} + +std::shared_ptr DataIndex::item_parameter_table(Version version) const { + auto ret = this->item_parameter_tables.at(static_cast(version)); + if (ret == nullptr) { + throw std::runtime_error("no item parameter table exists for this version"); + } + return ret; +} + +std::shared_ptr DataIndex::item_parameter_table_for_encode(Version version) const { + return this->item_parameter_table(is_v1(version) ? Version::PC_V2 : version); +} + +std::shared_ptr DataIndex::mag_metadata_table(Version version) const { + if (version == Version::DC_NTE) { + return this->mag_metadata_table_dc_nte; + } else if (version == Version::DC_11_2000) { + return this->mag_metadata_table_dc_11_2000; + } else if (is_v1(version)) { + return this->mag_metadata_table_v1; + } else if (is_v2(version)) { + return this->mag_metadata_table_v2; + } else if (!is_v4(version)) { + return this->mag_metadata_table_v3; + } else { + return this->mag_metadata_table_v4; + } +} + +std::shared_ptr DataIndex::item_stack_limits(Version version) const { + auto ret = this->item_stack_limits_tables.at(static_cast(version)); + if (ret == nullptr) { + throw std::runtime_error("no item stack limits table exists for this version"); + } + return ret; +} + +std::shared_ptr DataIndex::item_name_index_opt(Version version) const { + return this->item_name_indexes.at(static_cast(version)); +} + +std::shared_ptr DataIndex::item_name_index(Version version) const { + auto ret = this->item_name_index_opt(version); + if (ret == nullptr) { + throw std::runtime_error("no item name index exists for this version"); + } + return ret; +} + +std::string DataIndex::describe_item(Version version, const ItemData& item, uint8_t flags) const { + if (is_v1(version)) { + ItemData encoded = item; + encoded.encode_for_version(version, this->item_parameter_table(version)); + return this->item_name_index(version)->describe_item(encoded, flags); + } else { + return this->item_name_index(version)->describe_item(item, flags); + } +} + +ItemData DataIndex::parse_item_description(Version version, const std::string& description) const { + return this->item_name_index(version)->parse_item_description(description); +} + +std::shared_ptr DataIndex::common_item_set(Version logic_version, std::shared_ptr q) const { + if (q && !q->meta.common_item_set_name.empty()) { + try { + return this->common_item_sets.at(q->meta.common_item_set_name); + } catch (const std::out_of_range&) { + throw std::runtime_error(std::format("common item set {} for quest {} does not exist", + q->meta.common_item_set_name, q->meta.name)); + } + } else if (is_v1_or_v2(logic_version) && (logic_version != Version::GC_NTE)) { + // TODO: We should probably have a v1 common item set at some point too + return this->common_item_sets.at("common-table-v1-v2"); + } else if ((logic_version == Version::GC_NTE) || is_v3(logic_version) || is_v4(logic_version)) { + return this->common_item_sets.at("common-table-v3-v4"); + } else { + throw std::runtime_error(std::format( + "no default common item set is available for {}", phosg::name_for_enum(logic_version))); + } +} + +std::shared_ptr DataIndex::rare_item_set(Version logic_version, std::shared_ptr q) const { + if (q && !q->meta.rare_item_set_name.empty()) { + try { + return this->rare_item_sets.at(q->meta.rare_item_set_name); + } catch (const std::out_of_range&) { + throw std::runtime_error(std::format("rare item set {} for quest {} does not exist", + q->meta.rare_item_set_name, q->meta.name)); + } + } else if (is_v1(logic_version)) { + return this->rare_item_sets.at("rare-table-v1"); + } else if (is_v2(logic_version) && (logic_version != Version::GC_NTE)) { + return this->rare_item_sets.at("rare-table-v2"); + } else if (is_v3(logic_version) || (logic_version == Version::GC_NTE)) { + return this->rare_item_sets.at("rare-table-v3"); + } else if (is_v4(logic_version)) { + return this->rare_item_sets.at("rare-table-v4"); + } else { + throw std::runtime_error(std::format("no default rare item set is available for {}", phosg::name_for_enum(logic_version))); + } +} + +void DataIndex::set_port_configuration(const std::vector& port_configs) { + this->name_to_port_config.clear(); + this->number_to_port_config.clear(); + + bool any_port_is_pc_console_detect = false; + for (const auto& pc : port_configs) { + if (!this->name_to_port_config.emplace(pc.name, pc).second) { + // Note: This is a logic_error instead of a runtime_error because port_configs comes from a JSON map, so the + // names should already all be unique. In contrast, the user can define port configurations with the same number + // while still writing valid JSON, so only one of these cases can reasonably occur as a result of user behavior. + throw std::logic_error("duplicate name in port configuration"); + } + if (!this->number_to_port_config.emplace(pc.port, pc).second) { + throw std::runtime_error("duplicate number in port configuration"); + } + if (pc.behavior == ServerBehavior::PC_CONSOLE_DETECT) { + any_port_is_pc_console_detect = true; + } + } + + if (any_port_is_pc_console_detect) { + if (!this->name_to_port_config.count("pc")) { + throw std::runtime_error("pc port is not defined, but some ports use the pc_console_detect behavior"); + } + if (!this->name_to_port_config.count("gc-us3")) { + throw std::runtime_error("gc-us3 port is not defined, but some ports use the pc_console_detect behavior"); + } + } +} + +std::shared_ptr DataIndex::load_bb_file(const std::string& filename) const { + + if (this->bb_patch_file_index) { + // First, look in the patch tree's data directory + std::string patch_index_path = "./data/" + filename; + try { + return this->bb_patch_file_index->get(patch_index_path)->load_data(); + } catch (const std::out_of_range&) { + } + } + + if (this->bb_data_gsl) { + // Second, look in the patch tree's data.gsl file + try { + // TODO: It's kinda not great that we copy the data here; find a way to avoid doing this (also in the below case) + return std::make_shared(this->bb_data_gsl->get_copy(filename)); + } catch (const std::out_of_range&) { + } + + // Third, look in data.gsl without the filename extension + size_t dot_offset = filename.rfind('.'); + if (dot_offset != std::string::npos) { + std::string no_ext_gsl_filename = filename.substr(0, dot_offset); + try { + return std::make_shared(this->bb_data_gsl->get_copy(no_ext_gsl_filename)); + } catch (const std::out_of_range&) { + } + } + } + + // Finally, look in system/blueburst + return std::make_shared(phosg::load_file("system/blueburst/" + filename)); +} + +std::shared_ptr DataIndex::load_map_file(Version version, const std::string& filename) const { + if (version == Version::BB_V4) { + try { + return this->load_bb_file(filename); + } catch (const std::exception& e) { + } + } else if (version == Version::PC_V2) { + try { + return std::make_shared(phosg::load_file("system/patch-pc/Media/PSO/" + filename)); + } catch (const std::exception& e) { + } + } + try { + std::string path = std::format("system/maps/{}/{}", file_path_token_for_version(version), filename); + return std::make_shared(phosg::load_file(path)); + } catch (const std::exception& e) { + } + return nullptr; +} + +std::pair DataIndex::parse_port_spec(const phosg::JSON& json) const { + if (json.is_list()) { + std::string addr = json.at(0).as_string(); + try { + addr = string_for_address(this->all_addresses.at(addr)); + } catch (const std::out_of_range&) { + } + return std::make_pair(addr, json.at(1).as_int()); + } else { + return std::make_pair("", json.as_int()); + } +} + +std::vector DataIndex::parse_port_configuration(const phosg::JSON& json) const { + std::vector ret; + for (const auto& item_json_it : json.as_dict()) { + const auto& item_list = item_json_it.second; + PortConfiguration& pc = ret.emplace_back(); + pc.name = item_json_it.first; + auto spec = this->parse_port_spec(item_list->at(0)); + pc.addr = std::move(spec.first); + pc.port = spec.second; + pc.version = phosg::enum_for_name(item_list->at(1).as_string()); + pc.behavior = phosg::enum_for_name(item_list->at(2).as_string()); + } + return ret; +} + +void DataIndex::collect_network_addresses() { + config_log.info_f("Reading network addresses"); + this->all_addresses = get_local_addresses(); + for (const auto& it : this->all_addresses) { + config_log.info_f("Found interface: {} = {}", it.first, string_for_address(it.second)); + } +} + +void DataIndex::load_config_early() { + if (this->config_filename.empty()) { + throw std::logic_error("configuration filename is missing"); + } + + config_log.info_f("Loading configuration"); + this->config_json = std::make_shared(phosg::JSON::parse(phosg::load_file(this->config_filename))); + + auto parse_behavior_switch = [&](const std::string& json_key, BehaviorSwitch default_value) -> DataIndex::BehaviorSwitch { + try { + std::string behavior = this->config_json->get_string(json_key); + if (behavior == "Off") { + return DataIndex::BehaviorSwitch::OFF; + } else if (behavior == "OffByDefault") { + return DataIndex::BehaviorSwitch::OFF_BY_DEFAULT; + } else if (behavior == "OnByDefault") { + return DataIndex::BehaviorSwitch::ON_BY_DEFAULT; + } else if (behavior == "On") { + return DataIndex::BehaviorSwitch::ON; + } else { + throw std::runtime_error("invalid value for " + json_key); + } + } catch (const std::out_of_range&) { + return default_value; + } + }; + + this->name = this->config_json->at("ServerName").as_string(); + this->num_worker_threads = this->config_json->at("WorkerThreads").as_int(); + + if (!this->one_time_config_loaded) { + try { + this->username = this->config_json->at("User").as_string(); + if (this->username == "$SUDO_USER") { + const char* user_from_env = getenv("SUDO_USER"); + if (!user_from_env) { + throw std::runtime_error("configuration specifies $SUDO_USER, but variable is not defined"); + } + this->username = user_from_env; + } + } catch (const std::out_of_range&) { + } + + this->set_port_configuration(parse_port_configuration(this->config_json->at("PortConfiguration"))); + try { + auto spec = this->parse_port_spec(this->config_json->at("DNSServerPort")); + this->dns_server_addr = std::move(spec.first); + this->dns_server_port = spec.second; + } catch (const std::out_of_range&) { + } + try { + for (const auto& item : this->config_json->at("IPStackListen").as_list()) { + if (item->is_int()) { + this->ip_stack_addresses.emplace_back(std::format("0.0.0.0:{}", item->as_int())); + } else if (!IS_WINDOWS) { + this->ip_stack_addresses.emplace_back(item->as_string()); + } else { + config_log.warning_f("Unix sockets are not supported on Windows; skipping address {}", item->as_string()); + } + } + } catch (const std::out_of_range&) { + } + try { + for (const auto& item : this->config_json->at("PPPStackListen").as_list()) { + if (item->is_int()) { + this->ppp_stack_addresses.emplace_back(std::format("0.0.0.0:{}", item->as_int())); + } else if (!IS_WINDOWS) { + this->ppp_stack_addresses.emplace_back(item->as_string()); + } else { + config_log.warning_f("Unix sockets are not supported on Windows; skipping address {}", item->as_string()); + } + } + } catch (const std::out_of_range&) { + } + try { + for (const auto& item : this->config_json->at("PPPRawListen").as_list()) { + if (item->is_int()) { + this->ppp_raw_addresses.emplace_back(std::format("0.0.0.0:{}", item->as_int())); + } else if (!IS_WINDOWS) { + this->ppp_raw_addresses.emplace_back(item->as_string()); + } else { + config_log.warning_f("Unix sockets are not supported on Windows; skipping address {}", item->as_string()); + } + } + } catch (const std::out_of_range&) { + } + try { + for (const auto& item : this->config_json->at("HTTPListen").as_list()) { + if (item->is_int()) { + this->http_addresses.emplace_back(std::format("0.0.0.0:{}", item->as_int())); + } else if (!IS_WINDOWS) { + this->http_addresses.emplace_back(item->as_string()); + } else { + config_log.warning_f("Unix sockets are not supported on Windows; skipping address {}", item->as_string()); + } + } + } catch (const std::out_of_range&) { + } + + this->one_time_config_loaded = true; + } + + try { + auto local_address_str = this->config_json->at("LocalAddress").as_string(); + try { + this->local_address = this->all_addresses.at(local_address_str); + config_log.info_f("Added local address: {} ({})", string_for_address(this->local_address), local_address_str); + } catch (const std::out_of_range&) { + this->local_address = address_for_string(local_address_str.c_str()); + config_log.info_f("Added local address: {}", local_address_str); + } + this->all_addresses.erase(""); + this->all_addresses.emplace("", this->local_address); + } catch (const std::out_of_range&) { + for (const auto& it : this->all_addresses) { + // Choose any local interface except the loopback interface + if (!is_loopback_address(it.second) && is_local_address(it.second)) { + this->local_address = it.second; + } + } + if (this->local_address) { + config_log.warning_f("Local address not specified; using {} as default", string_for_address(this->local_address)); + } else { + config_log.warning_f("Local address not specified and no default is available"); + } + } + + try { + auto external_address_str = this->config_json->at("ExternalAddress").as_string(); + try { + this->external_address = this->all_addresses.at(external_address_str); + config_log.info_f("Added external address: {} ({})", + string_for_address(this->external_address), external_address_str); + } catch (const std::out_of_range&) { + this->external_address = address_for_string(external_address_str.c_str()); + config_log.info_f("Added external address: {}", external_address_str); + } + this->all_addresses.erase(""); + this->all_addresses.emplace("", this->external_address); + } catch (const std::out_of_range&) { + for (const auto& it : this->all_addresses) { + // Choose any non-local address, if any exist + if (!is_local_address(it.second)) { + this->external_address = it.second; + break; + } + } + if (this->external_address) { + config_log.warning_f("External address not specified; using {} as default", + string_for_address(this->external_address)); + } else { + config_log.warning_f( + "External address not specified and no default is available; only local clients will be able to connect"); + } + } + + try { + this->banned_ipv4_ranges = std::make_shared(this->config_json->at("BannedIPV4Ranges")); + } catch (const std::out_of_range&) { + this->banned_ipv4_ranges = std::make_shared(); + } + + this->client_ping_interval_usecs = this->config_json->get_int("ClientPingInterval", 30000000); + this->client_idle_timeout_usecs = this->config_json->get_int("ClientIdleTimeout", 60000000); + this->patch_client_idle_timeout_usecs = this->config_json->get_int("PatchClientIdleTimeout", 300000000); + + this->ip_stack_debug = this->config_json->get_bool("IPStackDebug", false); + this->allow_unregistered_users = this->config_json->get_bool("AllowUnregisteredUsers", false); + this->allow_pc_nte = this->config_json->get_bool("AllowPCNTE", false); + this->allow_same_account_concurrent_logins = this->config_json->get_bool("AllowSameAccountConcurrentLogins", false); + this->use_temp_accounts_for_prototypes = this->config_json->get_bool("UseTemporaryAccountsForPrototypes", true); + this->notify_server_for_max_level_achieved = this->config_json->get_bool("NotifyServerForMaxLevelAchieved", false); + this->allowed_drop_modes_v1_v2_normal = this->config_json->get_int("AllowedDropModesV1V2Normal", 0x1F); + this->allowed_drop_modes_v1_v2_battle = this->config_json->get_int("AllowedDropModesV1V2Battle", 0x07); + this->allowed_drop_modes_v1_v2_challenge = this->config_json->get_int("AllowedDropModesV1V2Challenge", 0x07); + this->allowed_drop_modes_v3_normal = this->config_json->get_int("AllowedDropModesV3Normal", 0x1F); + this->allowed_drop_modes_v3_battle = this->config_json->get_int("AllowedDropModesV3Battle", 0x07); + this->allowed_drop_modes_v3_challenge = this->config_json->get_int("AllowedDropModesV3Challenge", 0x07); + this->allowed_drop_modes_v4_normal = this->config_json->get_int("AllowedDropModesV4Normal", 0x1D); + this->allowed_drop_modes_v4_battle = this->config_json->get_int("AllowedDropModesV4Battle", 0x05); + this->allowed_drop_modes_v4_challenge = this->config_json->get_int("AllowedDropModesV4Challenge", 0x05); + this->default_drop_mode_v1_v2_normal = this->config_json->get_enum("DefaultDropModeV1V2Normal", ServerDropMode::CLIENT); + this->default_drop_mode_v1_v2_battle = this->config_json->get_enum("DefaultDropModeV1V2Battle", ServerDropMode::CLIENT); + this->default_drop_mode_v1_v2_challenge = this->config_json->get_enum("DefaultDropModeV1V2Challenge", ServerDropMode::CLIENT); + this->default_drop_mode_v3_normal = this->config_json->get_enum("DefaultDropModeV3Normal", ServerDropMode::CLIENT); + this->default_drop_mode_v3_battle = this->config_json->get_enum("DefaultDropModeV3Battle", ServerDropMode::CLIENT); + this->default_drop_mode_v3_challenge = this->config_json->get_enum("DefaultDropModeV3Challenge", ServerDropMode::CLIENT); + this->default_drop_mode_v4_normal = this->config_json->get_enum("DefaultDropModeV4Normal", ServerDropMode::SERVER_SHARED); + this->default_drop_mode_v4_battle = this->config_json->get_enum("DefaultDropModeV4Battle", ServerDropMode::SERVER_SHARED); + this->default_drop_mode_v4_challenge = this->config_json->get_enum("DefaultDropModeV4Challenge", ServerDropMode::SERVER_SHARED); + if ((this->default_drop_mode_v4_normal == ServerDropMode::CLIENT) || + (this->default_drop_mode_v4_battle == ServerDropMode::CLIENT) || + (this->default_drop_mode_v4_challenge == ServerDropMode::CLIENT)) { + throw std::runtime_error("default V4 drop mode cannot be CLIENT"); + } + if ((this->allowed_drop_modes_v4_normal & (1 << static_cast(ServerDropMode::CLIENT))) || + (this->allowed_drop_modes_v4_battle & (1 << static_cast(ServerDropMode::CLIENT))) || (this->allowed_drop_modes_v4_challenge & (1 << static_cast(ServerDropMode::CLIENT)))) { + throw std::runtime_error("CLIENT drop mode cannot be allowed in V4"); + } + + auto parse_quest_flag_rewrites = [&json = this->config_json](const char* key) -> std::unordered_map { + std::unordered_map ret; + try { + for (const auto& it : json->get_dict(key)) { + if (!it.first.starts_with("F_")) { + throw std::runtime_error("invalid flag reference: " + it.first); + } + uint16_t flag = stoul(it.first.substr(2), nullptr, 16); + if (it.second->is_bool()) { + ret.emplace(flag, it.second->as_bool() ? "true" : "false"); + } else { + ret.emplace(flag, it.second->as_string()); + } + } + } catch (const std::out_of_range&) { + } + return ret; + }; + this->quest_flag_rewrites_v1_v2 = parse_quest_flag_rewrites("QuestFlagRewritesV1V2"); + this->quest_flag_rewrites_v3 = parse_quest_flag_rewrites("QuestFlagRewritesV3"); + this->quest_flag_rewrites_v4 = parse_quest_flag_rewrites("QuestFlagRewritesV4"); + + this->quest_counter_fields.clear(); + try { + for (const auto& it : this->config_json->get_dict("QuestCounterFields")) { + const auto& def = it.second->as_list(); + this->quest_counter_fields.emplace(it.first, std::make_pair(def.at(0)->as_int(), def.at(1)->as_int())); + } + } catch (const std::out_of_range&) { + } + + this->persistent_game_idle_timeout_usecs = this->config_json->get_int("PersistentGameIdleTimeout", 0); + this->cheat_mode_behavior = parse_behavior_switch("CheatModeBehavior", BehaviorSwitch::OFF_BY_DEFAULT); + this->default_switch_assist_enabled = this->config_json->get_bool("EnableSwitchAssistByDefault", false); + this->use_game_creator_section_id = this->config_json->get_bool("UseGameCreatorSectionID", false); + this->rare_notifs_enabled_for_client_drops = this->config_json->get_bool("RareNotificationsEnabledForClientDrops", false); + this->default_rare_notifs_enabled_v1_v2 = this->config_json->get_bool("RareNotificationsEnabledByDefault", false); + this->default_rare_notifs_enabled_v3_v4 = this->default_rare_notifs_enabled_v1_v2; + this->default_rare_notifs_enabled_v1_v2 = this->config_json->get_bool("RareNotificationsEnabledByDefaultV1V2", this->default_rare_notifs_enabled_v1_v2); + this->default_rare_notifs_enabled_v3_v4 = this->config_json->get_bool("RareNotificationsEnabledByDefaultV3V4", this->default_rare_notifs_enabled_v3_v4); + this->enable_send_function_call_quest_numbers.clear(); + try { + for (const auto& it : this->config_json->get_dict("EnableSendFunctionCallQuestNumbers")) { + if (it.first.size() != 4) { + throw std::runtime_error(std::format( + "specific_version {} in EnableSendFunctionCallQuestNumbers is not a 4-byte string", + it.first)); + } + uint32_t specific_version = phosg::StringReader(it.first).get_u32b(); + int64_t quest_num = it.second->as_int(); + this->enable_send_function_call_quest_numbers.emplace(specific_version, quest_num); + } + } catch (const std::out_of_range&) { + } + this->enable_v3_v4_protected_subcommands = this->config_json->get_bool("EnableV3V4ProtectedSubcommands", false); + + auto parse_int_list = +[](const phosg::JSON& json) -> std::vector { + std::vector ret; + for (const auto& item : json.as_list()) { + ret.emplace_back(item->as_int()); + } + return ret; + }; + + this->ep3_infinite_meseta = this->config_json->get_bool("Episode3InfiniteMeseta", false); + try { + this->ep3_defeat_player_meseta_rewards = parse_int_list(this->config_json->at("Episode3DefeatPlayerMeseta")); + } catch (const std::out_of_range&) { + this->ep3_defeat_player_meseta_rewards = {300, 400, 500, 600, 700}; + } + try { + this->ep3_defeat_com_meseta_rewards = parse_int_list(this->config_json->get("Episode3DefeatCOMMeseta", phosg::JSON::list())); + } catch (const std::out_of_range&) { + this->ep3_defeat_com_meseta_rewards = {100, 200, 300, 400, 500}; + } + this->ep3_final_round_meseta_bonus = this->config_json->get_int("Episode3FinalRoundMesetaBonus", 300); + this->ep3_jukebox_is_free = this->config_json->get_bool("Episode3JukeboxIsFree", false); + this->ep3_behavior_flags = this->config_json->get_int("Episode3BehaviorFlags", 0); + this->ep3_card_auction_points = this->config_json->get_int("CardAuctionPoints", 0); + this->hide_download_commands = this->config_json->get_bool("HideDownloadCommands", true); + this->censor_credentials = this->config_json->get_bool("CensorCredentials", true); + this->proxy_allow_save_files = this->config_json->get_bool("ProxyAllowSaveFiles", true); + + try { + const auto& i = this->config_json->at("CardAuctionSize"); + if (i.is_int()) { + this->ep3_card_auction_min_size = i.as_int(); + this->ep3_card_auction_max_size = this->ep3_card_auction_min_size; + } else { + this->ep3_card_auction_min_size = i.at(0).as_int(); + this->ep3_card_auction_max_size = i.at(1).as_int(); + } + } catch (const std::out_of_range&) { + this->ep3_card_auction_min_size = 0; + this->ep3_card_auction_max_size = 0; + } + + this->ep3_lobby_banners.clear(); + size_t banner_index = 0; + for (const auto& it : this->config_json->get("Episode3LobbyBanners", phosg::JSON::list()).as_list()) { + std::string path = "system/ep3/banners/" + it->at(2).as_string(); + + std::string compressed_gvm_data; + std::string decompressed_gvm_data; + std::string lower_path = phosg::tolower(path); + if (lower_path.ends_with(".gvm.prs")) { + compressed_gvm_data = phosg::load_file(path); + } else if (lower_path.ends_with(".gvm")) { + decompressed_gvm_data = phosg::load_file(path); + } else if (lower_path.ends_with(".bmp")) { + auto img = phosg::ImageRGBA8888N::from_file_data(phosg::load_file(path)); + decompressed_gvm_data = encode_gvm( + img, + has_any_transparent_pixels(img) ? GVRDataFormat::RGB5A3 : GVRDataFormat::RGB565, + std::format("bnr{}", banner_index), + 0x80 | banner_index); + banner_index++; + } else { + throw std::runtime_error(std::format("banner {} is in an unknown format", path)); + } + + size_t decompressed_size = decompressed_gvm_data.empty() + ? prs_decompress_size(compressed_gvm_data) + : decompressed_gvm_data.size(); + if (decompressed_size > 0x37000) { + throw std::runtime_error(std::format( + "banner {} is too large (0x{:X} bytes; maximum size is 0x37000 bytes)", path, decompressed_size)); + } + + if (compressed_gvm_data.empty()) { + compressed_gvm_data = prs_compress_optimal(decompressed_gvm_data); + } + if (compressed_gvm_data.size() > 0x3800) { + throw std::runtime_error(std::format( + "banner {} cannot be compressed small enough (0x{:X} bytes; maximum size is 0x3800 bytes compressed)", + it->at(2).as_string(), compressed_gvm_data.size())); + } + config_log.info_f( + "Loaded Episode 3 lobby banner {} (0x{:X} -> 0x{:X} bytes)", + path, decompressed_size, compressed_gvm_data.size()); + this->ep3_lobby_banners.emplace_back( + Ep3LobbyBannerEntry{.type = static_cast(it->at(0).as_int()), + .which = static_cast(it->at(1).as_int()), + .data = std::move(compressed_gvm_data)}); + } + + { + auto parse_ep3_ex_result_cmd = [&](const phosg::JSON& src) -> std::shared_ptr { + auto ret = std::make_shared(); + const auto& win_json = src.at("Win"); + for (size_t z = 0; z < std::min(win_json.size(), 10); z++) { + ret->win_entries[z].threshold = win_json.at(z).at(0).as_int(); + ret->win_entries[z].value = win_json.at(z).at(1).as_int(); + } + const auto& lose_json = src.at("Lose"); + for (size_t z = 0; z < std::min(lose_json.size(), 10); z++) { + ret->lose_entries[z].threshold = lose_json.at(z).at(0).as_int(); + ret->lose_entries[z].value = lose_json.at(z).at(1).as_int(); + } + return ret; + }; + const auto& categories_json = this->config_json->at("Episode3EXResultValues"); + this->ep3_default_ex_values = parse_ep3_ex_result_cmd(categories_json.at("Default")); + try { + this->ep3_tournament_ex_values = parse_ep3_ex_result_cmd(categories_json.at("Tournament")); + } catch (const std::out_of_range&) { + this->ep3_tournament_ex_values = this->ep3_default_ex_values; + } + try { + this->ep3_tournament_ex_values = parse_ep3_ex_result_cmd(categories_json.at("TournamentFinalMatch")); + } catch (const std::out_of_range&) { + this->ep3_tournament_final_round_ex_values = this->ep3_tournament_ex_values; + } + } + + try { + const auto& stack_limits_tables_json = this->config_json->at("ItemStackLimits"); + for (size_t v_s = NUM_PATCH_VERSIONS; v_s < NUM_VERSIONS; v_s++) { + try { + Version v = static_cast(v_s); + this->item_stack_limits_tables[v_s] = std::make_shared( + v, stack_limits_tables_json.at(v_s - NUM_PATCH_VERSIONS)); + } catch (const std::out_of_range&) { + } + } + } catch (const std::out_of_range&) { + } + + this->bb_max_bank_items = this->config_json->get_int("BBMaxBankItems", 200); + this->bb_max_bank_meseta = this->config_json->get_int("BBMaxBankMeseta", 999999); + + for (size_t v_s = NUM_PATCH_VERSIONS; v_s < NUM_VERSIONS; v_s++) { + if (!this->item_stack_limits_tables[v_s]) { + Version v = static_cast(v_s); + if ((v == Version::DC_NTE) || (v == Version::DC_11_2000)) { + this->item_stack_limits_tables[v_s] = std::make_shared( + v, ItemData::StackLimits::DEFAULT_TOOL_LIMITS_DC_NTE, 999999); + } else if (v_s < static_cast(Version::GC_NTE)) { + this->item_stack_limits_tables[v_s] = std::make_shared( + v, ItemData::StackLimits::DEFAULT_TOOL_LIMITS_V1_V2, 999999); + } else { + this->item_stack_limits_tables[v_s] = std::make_shared( + v, ItemData::StackLimits::DEFAULT_TOOL_LIMITS_V3_V4, 999999); + } + } + } + + this->bb_global_exp_multiplier = this->config_json->get_float("BBGlobalEXPMultiplier", 1.0f); + this->exp_share_multiplier = this->config_json->get_float("BBEXPShareMultiplier", 0.5f); + this->server_global_drop_rate_multiplier = this->config_json->get_float("ServerGlobalDropRateMultiplier", 1.0f); + + if (this->is_debug) { + set_all_log_levels(phosg::LogLevel::L_DEBUG); + } else { + set_log_levels_from_json(this->config_json->get("LogLevels", phosg::JSON::dict())); + } + + try { + this->run_shell_behavior = this->config_json->at("RunInteractiveShell").as_bool() + ? DataIndex::RunShellBehavior::ALWAYS + : DataIndex::RunShellBehavior::NEVER; + } catch (const std::out_of_range&) { + } + + try { + const auto& groups = this->config_json->get_list("CompatibilityGroups"); + this->compatibility_groups.fill(0); + for (size_t v_s = 0; v_s < groups.size(); v_s++) { + this->compatibility_groups[v_s] = groups[v_s]->as_int(); + } + } catch (const std::out_of_range&) { + static_assert(NUM_VERSIONS == 14, "Don't forget to update the default compatibility groups"); + this->compatibility_groups = { + 0x0000, // PC_PATCH + 0x0000, // BB_PATCH + 0x0004, // DC_NTE compatible only with itself + 0x0008, // DC_11_2000 compatible only with itself + 0x00B0, // DC_V1 compatible with DC_V1, DC_V2, and PC_V2 + 0x00B0, // DC_V2 compatible with DC_V1, DC_V2, and PC_V2 + 0x0040, // PC_NTE compatible only with itself + 0x00B0, // PC_V2 compatible with DC_V1, DC_V2, and PC_V2 + 0x0100, // GC_NTE compatible only with itself + 0x1200, // GC_V3 compatible with GC_V3 and XB_V3 + 0x0400, // GC_EP3_NTE compatible only with itself + 0x0800, // GC_EP3 compatible only with itself + 0x1200, // XB_V3 compatible with GC_V3 and XB_V3 + 0x2000, // BB_V4 compatible only with itself + }; + } + + this->enable_chat_commands = this->config_json->get_bool("EnableChatCommands", true); + try { + const auto& s = this->config_json->get_string("ChatCommandSentinel"); + if (s.size() != 1) { + throw std::runtime_error("ChatCommandSentinel must be a string of length 1"); + } + this->chat_command_sentinel = s[0]; + } catch (const std::out_of_range&) { + } + this->num_backup_character_slots = this->config_json->get_int("BackupCharacterSlots", 16); + + this->version_name_colors.reset(); + this->client_customization_name_color = 0; + try { + const auto& colors_json = this->config_json->get_list("VersionNameColors"); + if (colors_json.size() != NUM_NON_PATCH_VERSIONS) { + throw std::runtime_error("VersionNameColors list length is incorrect"); + } + auto new_colors = std::make_unique>(); + for (size_t z = 0; z < NUM_NON_PATCH_VERSIONS; z++) { + new_colors->at(z) = colors_json.at(z)->as_int(); + } + this->version_name_colors = std::move(new_colors); + } catch (const std::out_of_range&) { + } + try { + this->client_customization_name_color = this->config_json->get_int("ClientCustomizationNameColor"); + } catch (const std::out_of_range&) { + } + + for (auto& order : this->public_lobby_search_orders) { + order.clear(); + } + this->client_customization_public_lobby_search_order.clear(); + try { + const auto& orders_json = this->config_json->get_list("LobbySearchOrders"); + for (size_t v_s = 0; v_s < orders_json.size(); v_s++) { + auto& order = this->public_lobby_search_orders.at(v_s); + const auto& order_json = orders_json.at(v_s); + for (const auto& it : order_json->as_list()) { + order.emplace_back(it->as_int()); + } + } + } catch (const std::out_of_range&) { + } + try { + const auto& order_json = this->config_json->get_list("ClientCustomizationLobbySearchOrder"); + auto& order = this->client_customization_public_lobby_search_order; + for (const auto& it : order_json) { + order.emplace_back(it->as_int()); + } + } catch (const std::out_of_range&) { + } + + this->pre_lobby_event = 0; + try { + auto v = this->config_json->at("MenuEvent"); + this->pre_lobby_event = v.is_int() ? v.as_int() : event_for_name(v.as_string()); + } catch (const std::out_of_range&) { + } + const auto& events_json = this->config_json->get_list("LobbyEvents"); + this->per_lobby_events.clear(); + try { + for (size_t z = 0; z < events_json.size(); z++) { + const auto& v = events_json.at(z); + per_lobby_events.emplace_back(v->is_int() ? v->as_int() : event_for_name(v->as_string())); + } + } catch (const std::out_of_range&) { + } + + this->ep3_menu_song = this->config_json->get_int("Episode3MenuSong", -1); + + try { + this->quest_category_index = std::make_shared(this->config_json->at("QuestCategories")); + } catch (const std::exception& e) { + throw std::runtime_error(std::format( + "QuestCategories is missing or invalid in config ({}); see config.example.json for an example", e.what())); + } + + config_log.info_f("Creating menus"); + + auto information_menu_v2 = std::make_shared(MenuID::INFORMATION, "Information"); + auto information_menu_v3 = std::make_shared(MenuID::INFORMATION, "Information"); + std::shared_ptr> information_contents_v2 = std::make_shared>(); + std::shared_ptr> information_contents_v3 = std::make_shared>(); + + information_menu_v2->items.emplace_back(InformationMenuItemID::GO_BACK, "Go back", + "Return to the\nmain menu", MenuItem::Flag::INVISIBLE_IN_INFO_MENU); + information_menu_v3->items.emplace_back(InformationMenuItemID::GO_BACK, "Go back", + "Return to the\nmain menu", MenuItem::Flag::INVISIBLE_IN_INFO_MENU); + { + auto blank_json = phosg::JSON::list(); + const phosg::JSON& default_json = this->config_json->get("InformationMenuContents", blank_json); + const phosg::JSON& v2_json = this->config_json->get("InformationMenuContentsV1V2", default_json); + const phosg::JSON& v3_json = this->config_json->get("InformationMenuContentsV3", default_json); + + uint32_t item_id = 0; + for (const auto& item : v2_json.as_list()) { + std::string name = item->get_string(0); + std::string short_desc = item->get_string(1); + information_menu_v2->items.emplace_back(item_id, name, short_desc, 0); + information_contents_v2->emplace_back(item->get_string(2)); + item_id++; + } + + item_id = 0; + for (const auto& item : v3_json.as_list()) { + std::string name = item->get_string(0); + std::string short_desc = item->get_string(1); + information_menu_v3->items.emplace_back(item_id, name, short_desc, MenuItem::Flag::REQUIRES_MESSAGE_BOXES); + information_contents_v3->emplace_back(item->get_string(2)); + item_id++; + } + } + this->information_menu_v2 = information_menu_v2; + this->information_menu_v3 = information_menu_v3; + this->information_contents_v2 = information_contents_v2; + this->information_contents_v3 = information_contents_v3; + + auto generate_proxy_destinations_menu = [&](std::vector>& ret_pds, const char* key) -> std::shared_ptr { + auto ret = std::make_shared(MenuID::PROXY_DESTINATIONS, "Proxy server"); + ret_pds.clear(); + + try { + std::map sorted_jsons; + for (const auto& it : this->config_json->at(key).as_dict()) { + sorted_jsons.emplace(it.first, *it.second); + } + + ret->items.emplace_back(ProxyDestinationsMenuItemID::GO_BACK, "Go back", "Return to the\nmain menu", 0); + ret->items.emplace_back(ProxyDestinationsMenuItemID::OPTIONS, "Options", "Set proxy session\noptions", 0); + + uint32_t item_id = 0; + for (const auto& item : sorted_jsons) { + const std::string& netloc_str = item.second.as_string(); + const std::string& description = "$C7Remote server:\n$C6" + netloc_str; + ret->items.emplace_back(item_id, item.first, description, 0); + ret_pds.emplace_back(phosg::parse_netloc(netloc_str)); + item_id++; + } + } catch (const std::out_of_range&) { + } + return ret; + }; + + this->proxy_destinations_menu_dc = generate_proxy_destinations_menu(this->proxy_destinations_dc, "ProxyDestinations-DC"); + this->proxy_destinations_menu_pc = generate_proxy_destinations_menu(this->proxy_destinations_pc, "ProxyDestinations-PC"); + this->proxy_destinations_menu_gc = generate_proxy_destinations_menu(this->proxy_destinations_gc, "ProxyDestinations-GC"); + this->proxy_destinations_menu_xb = generate_proxy_destinations_menu(this->proxy_destinations_xb, "ProxyDestinations-XB"); + + try { + const std::string& netloc_str = this->config_json->get_string("ProxyDestination-Patch"); + this->proxy_destination_patch = phosg::parse_netloc(netloc_str); + config_log.info_f("Patch server proxy is enabled with destination {}", netloc_str); + } catch (const std::out_of_range&) { + this->proxy_destination_patch.reset(); + } + try { + const std::string& netloc_str = this->config_json->get_string("ProxyDestination-BB"); + this->proxy_destination_bb = phosg::parse_netloc(netloc_str); + config_log.info_f("BB proxy is enabled with destination {}", netloc_str); + } catch (const std::out_of_range&) { + this->proxy_destination_bb.reset(); + } + + this->welcome_message = this->config_json->get_string("WelcomeMessage", ""); + this->pc_patch_server_message = this->config_json->get_string("PCPatchServerMessage", ""); + this->bb_patch_server_message = this->config_json->get_string("BBPatchServerMessage", ""); + + this->team_reward_defs_json = nullptr; + try { + this->team_reward_defs_json = std::move(this->config_json->at("TeamRewards")); + } catch (const std::out_of_range&) { + } + + std::shared_ptr prev = MapState::DEFAULT_RARE_ENEMIES; + for (Difficulty difficulty : ALL_DIFFICULTIES_V234) { + size_t diff_index = static_cast(difficulty); + try { + std::string key = "RareEnemyRates-"; + key += token_name_for_difficulty(difficulty); + this->rare_enemy_rates_by_difficulty[diff_index] = std::make_shared( + this->config_json->at(key)); + prev = this->rare_enemy_rates_by_difficulty[diff_index]; + } catch (const std::out_of_range&) { + this->rare_enemy_rates_by_difficulty[diff_index] = prev; + } + } + try { + this->rare_enemy_rates_challenge = std::make_shared(this->config_json->at("RareEnemyRates-Challenge")); + } catch (const std::out_of_range&) { + this->rare_enemy_rates_challenge = MapState::DEFAULT_RARE_ENEMIES; + } + + this->min_levels_v1_v2[0] = DEFAULT_MIN_LEVELS_V123; + this->min_levels_v1_v2[1] = DEFAULT_MIN_LEVELS_V123; + this->min_levels_v1_v2[2] = DEFAULT_MIN_LEVELS_V123; + this->min_levels_v3[0] = DEFAULT_MIN_LEVELS_V123; + this->min_levels_v3[1] = DEFAULT_MIN_LEVELS_V123; + this->min_levels_v3[2] = DEFAULT_MIN_LEVELS_V123; + this->min_levels_v4[0] = DEFAULT_MIN_LEVELS_V4_EP1; + this->min_levels_v4[1] = DEFAULT_MIN_LEVELS_V4_EP2; + this->min_levels_v4[2] = DEFAULT_MIN_LEVELS_V4_EP4; + auto populate_min_levels = [&](std::array, 3>& dest, const char* key_name) -> void { + try { + for (const auto& ep_it : this->config_json->get_dict(key_name)) { + std::array levels({0, 0, 0, 0}); + for (size_t z = 0; z < 4; z++) { + levels[z] = ep_it.second->get_int(z) - 1; + } + switch (episode_for_token_name(ep_it.first)) { + case Episode::EP1: + dest[0] = levels; + break; + case Episode::EP2: + dest[1] = levels; + break; + case Episode::EP4: + dest[2] = levels; + break; + default: + throw std::runtime_error("unknown episode"); + } + } + } catch (const std::out_of_range&) { + } + }; + populate_min_levels(this->min_levels_v1_v2, "V1V2MinimumLevels"); + populate_min_levels(this->min_levels_v3, "V3MinimumLevels"); + populate_min_levels(this->min_levels_v4, "BBMinimumLevels"); + + this->bb_required_patches.clear(); + try { + for (const auto& it : this->config_json->get_list("BBRequiredPatches")) { + this->bb_required_patches.emplace(it->as_string()); + } + } catch (const std::out_of_range&) { + } + this->auto_patches.clear(); + try { + for (const auto& it : this->config_json->get_list("AutoPatches")) { + this->auto_patches.emplace(it->as_string()); + } + } catch (const std::out_of_range&) { + } + + try { + this->cheat_flags = CheatFlags(this->config_json->at("CheatingBehaviors")); + } catch (const std::out_of_range&) { + this->cheat_flags = CheatFlags(); + } +} + +void DataIndex::load_config_late() { + this->ep3_card_auction_pool.clear(); + try { + for (const auto& it : this->config_json->get_dict("CardAuctionPool")) { + uint16_t card_id; + try { + card_id = this->ep3_card_index->definition_for_name_normalized(it.first)->def.card_id; + } catch (const std::out_of_range&) { + throw std::runtime_error(std::format("Ep3 card \"{}\" in auction pool does not exist", it.first)); + } + this->ep3_card_auction_pool.emplace_back( + CardAuctionPoolEntry{ + .probability = static_cast(it.second->at(0).as_int()), + .card_id = card_id, + .min_price = static_cast(it.second->at(1).as_int())}); + } + } catch (const std::out_of_range&) { + } + + for (auto& trap_card_ids : this->ep3_trap_card_ids) { + trap_card_ids.clear(); + } + if (this->ep3_card_index) { + try { + const auto& ep3_trap_cards_json = this->config_json->get_list("Episode3TrapCards"); + if (!ep3_trap_cards_json.empty()) { + if (ep3_trap_cards_json.size() != 5) { + throw std::runtime_error("Episode3TrapCards must be a list of 5 lists"); + } + for (size_t trap_type = 0; trap_type < 5; trap_type++) { + auto& trap_card_ids = this->ep3_trap_card_ids[trap_type]; + for (const auto& card_it : ep3_trap_cards_json.at(trap_type)->as_list()) { + if (card_it->is_int()) { + int64_t card_id = card_it->as_int(); + try { + const auto& card = this->ep3_card_index->definition_for_id(card_id); + if (card->def.type != Episode3::CardType::ASSIST) { + throw std::runtime_error(std::format( + "Ep3 card \"{}\" ({:04X}) in trap card list is not an assist card", + card->def.en_name.decode(), card->def.card_id)); + } + trap_card_ids.emplace_back(card->def.card_id); + } catch (const std::out_of_range&) { + throw std::runtime_error(std::format("Ep3 card {:04X} in trap card list does not exist", card_id)); + } + } else { + const std::string& card_name = card_it->as_string(); + try { + const auto& card = this->ep3_card_index->definition_for_name_normalized(card_name); + if (card->def.type != Episode3::CardType::ASSIST) { + throw std::runtime_error(std::format( + "Ep3 card \"{}\" ({:04X}) in trap card list is not an assist card", + card->def.en_name.decode(), card->def.card_id)); + } + trap_card_ids.emplace_back(card->def.card_id); + } catch (const std::out_of_range&) { + throw std::runtime_error(std::format("Ep3 card \"{}\" in trap card list does not exist", card_name)); + } + } + } + } + } + } catch (const std::out_of_range&) { + } + } else { + config_log.warning_f("Episode 3 card definitions missing; cannot set trap card IDs from config"); + } + + this->quest_F95E_results.clear(); + this->quest_F95F_results.clear(); + this->quest_F960_success_results.clear(); + this->quest_F960_failure_results = QuestF960Result(); + if (this->item_name_index(Version::BB_V4)) { + try { + for (const auto& type_it : this->config_json->get_list("QuestF95EResultItems")) { + auto& type_res = this->quest_F95E_results.emplace_back(); + for (const auto& difficulty_it : type_it->as_list()) { + auto& difficulty_res = type_res.emplace_back(); + for (const auto& item_it : difficulty_it->as_list()) { + if (item_it->is_int()) { + difficulty_res.emplace_back(ItemData::from_primary_identifier( + *this->item_stack_limits(Version::BB_V4), item_it->as_int())); + } else { + try { + difficulty_res.emplace_back(this->parse_item_description(Version::BB_V4, item_it->as_string())); + } catch (const std::exception& e) { + config_log.warning_f("Cannot parse item description \"{}\": {} (skipping entry)", item_it->as_string(), e.what()); + } + } + } + } + } + } catch (const std::out_of_range&) { + } + try { + for (const auto& it : this->config_json->get_list("QuestF95FResultItems")) { + auto& list = it->as_list(); + size_t price = list.at(0)->as_int(); + const auto& desc = list.at(1); + if (desc->is_int()) { + this->quest_F95F_results.emplace_back(std::make_pair( + price, ItemData::from_primary_identifier(*this->item_stack_limits(Version::BB_V4), desc->as_int()))); + } else { + try { + this->quest_F95F_results.emplace_back(std::make_pair( + price, this->parse_item_description(Version::BB_V4, list.at(1)->as_string()))); + } catch (const std::exception& e) { + config_log.warning_f("Cannot parse item description \"{}\": {} (skipping entry)", list.at(1)->as_string(), e.what()); + } + } + } + } catch (const std::out_of_range&) { + } + try { + auto name_index = this->item_name_index(Version::BB_V4); + auto stack_limits = this->item_stack_limits(Version::BB_V4); + this->quest_F960_failure_results = QuestF960Result( + this->config_json->at("QuestF960FailureResultItems"), name_index, *stack_limits); + for (const auto& it : this->config_json->get_list("QuestF960SuccessResultItems")) { + this->quest_F960_success_results.emplace_back(*it, name_index, *stack_limits); + } + } catch (const std::out_of_range&) { + } + + auto parse_primary_identifier_list = [&](const char* key, Version v) -> std::unordered_set { + std::unordered_set ret; + try { + for (const auto& pi_json : this->config_json->get_list(key)) { + if (pi_json->is_int()) { + ret.emplace(pi_json->as_int()); + } else { + try { + auto item = this->parse_item_description(v, pi_json->as_string()); + ret.emplace(item.primary_identifier()); + } catch (const std::exception& e) { + config_log.warning_f("Cannot parse item description \"{}\": {} (skipping entry)", pi_json->as_string(), e.what()); + } + } + } + } catch (const std::out_of_range&) { + } + return ret; + }; + this->notify_game_for_item_primary_identifiers_v1_v2 = parse_primary_identifier_list( + "NotifyGameForItemPrimaryIdentifiersV1V2", Version::PC_V2); + this->notify_game_for_item_primary_identifiers_v3 = parse_primary_identifier_list( + "NotifyGameForItemPrimaryIdentifiersV3", Version::GC_V3); + this->notify_game_for_item_primary_identifiers_v4 = parse_primary_identifier_list( + "NotifyGameForItemPrimaryIdentifiersV4", Version::BB_V4); + this->notify_server_for_item_primary_identifiers_v1_v2 = parse_primary_identifier_list( + "NotifyServerForItemPrimaryIdentifiersV1V2", Version::PC_V2); + this->notify_server_for_item_primary_identifiers_v3 = parse_primary_identifier_list( + "NotifyServerForItemPrimaryIdentifiersV3", Version::GC_V3); + this->notify_server_for_item_primary_identifiers_v4 = parse_primary_identifier_list( + "NotifyServerForItemPrimaryIdentifiersV4", Version::BB_V4); + + } else { + config_log.warning_f("BB item name index is missing; cannot load quest reward lists from config"); + } +} + +void DataIndex::load_bb_private_keys() { + std::vector> new_keys; + for (const auto& item : std::filesystem::directory_iterator("system/blueburst/keys")) { + std::string filename = item.path().filename().string(); + if (!filename.ends_with(".nsk")) { + continue; + } + new_keys.emplace_back(std::make_shared( + phosg::load_object_file("system/blueburst/keys/" + filename))); + config_log.debug_f("Loaded Blue Burst key file: {}", filename); + } + this->bb_private_keys = std::move(new_keys); +} + +void DataIndex::load_bb_system_defaults() { + try { + this->bb_default_keyboard_config = std::make_shared>( + phosg::load_object_file>("system/blueburst/default-keyboard-config.bin")); + config_log.info_f("Default Blue Burst keyboard config is present"); + } catch (const phosg::cannot_open_file&) { + } + try { + this->bb_default_joystick_config = std::make_shared>( + phosg::load_object_file>("system/blueburst/default-joystick-config.bin")); + config_log.info_f("Default Blue Burst joystick config is present"); + } catch (const phosg::cannot_open_file&) { + } +} + +void DataIndex::load_patch_indexes() { + std::shared_ptr bb_data_gsl; + std::shared_ptr pc_patch_file_index; + std::shared_ptr bb_patch_file_index; + + if (std::filesystem::is_directory("system/patch-pc")) { + config_log.info_f("Indexing PSO PC patch files"); + pc_patch_file_index = std::make_shared("system/patch-pc"); + } else { + config_log.info_f("PSO PC patch files not present"); + } + if (std::filesystem::is_directory("system/patch-bb")) { + config_log.info_f("Indexing PSO BB patch files"); + bb_patch_file_index = std::make_shared("system/patch-bb"); + try { + auto gsl_file = bb_patch_file_index->get("./data/data.gsl"); + bb_data_gsl = std::make_shared(gsl_file->load_data(), false); + config_log.info_f("data.gsl found in BB patch files"); + } catch (const std::out_of_range&) { + config_log.info_f("data.gsl is not present in BB patch files"); + } + } else { + config_log.info_f("PSO BB patch files not present"); + } + + this->bb_data_gsl = std::move(bb_data_gsl); + this->pc_patch_file_index = std::move(pc_patch_file_index); + this->bb_patch_file_index = std::move(bb_patch_file_index); +} + +void DataIndex::load_maps() { + using SDT = SetDataTable; + + config_log.info_f("Loading map layouts"); + auto new_room_layout_index = std::make_shared( + phosg::JSON::parse(phosg::load_file("system/maps/room-layout-index.json"))); + + config_log.info_f("Loading Episode 3 Morgue maps"); + std::unordered_map> new_map_file_for_source_hash; + std::map, NUM_VERSIONS>> new_map_files_for_free_play_key; + { + // TODO: Ep3 NTE loads map_city00_on, but it appears there are variants. Figure this out and load those maps too. + auto objects_data = this->load_map_file(Version::GC_EP3, "map_city_on_battle_o.dat"); + auto enemies_data = this->load_map_file(Version::GC_EP3, "map_city_on_battle_e.dat"); + if (objects_data || enemies_data) { + uint32_t free_play_key = this->free_play_key(Episode::EP3, GameMode::NORMAL, Difficulty::NORMAL, 0, 0, 0); + auto map_file = std::make_shared(0, objects_data, enemies_data, nullptr); + new_map_file_for_source_hash.emplace(map_file->source_hash(), map_file); + new_map_files_for_free_play_key[free_play_key].at(static_cast(Version::GC_EP3)) = map_file; + config_log.info_f("Episode 3 map files loaded with free play key {:08X}", free_play_key); + } else { + config_log.info_f("Episode 3 map files not found; skipping"); + } + } + + config_log.info_f("Loading free play map files"); + for (Version v : ALL_ARPG_SEMANTIC_VERSIONS) { + for (Episode episode : ALL_EPISODES_V4) { + if ((episode == Episode::EP2 && is_v1_or_v2(v) && (v != Version::GC_NTE)) || + (episode == Episode::EP4 && !is_v4(v))) { + continue; + } + + for (GameMode mode : ALL_GAME_MODES_V4) { + if ((mode == GameMode::BATTLE) && is_pre_v1(v)) { + continue; + } + if ((mode == GameMode::CHALLENGE) && is_v1(v)) { + continue; + } + if ((mode == GameMode::SOLO && !is_v4(v))) { + continue; + } + for (Difficulty difficulty : ALL_DIFFICULTIES_V234) { + if ((difficulty == Difficulty::ULTIMATE) && is_v1(v)) { + continue; + } + auto sdt = this->set_data_table(v, episode, mode, difficulty); + for (uint8_t floor = 0; floor < 0x12; floor++) { + auto variation_maxes = sdt->num_free_play_variations_for_floor(episode, mode == GameMode::SOLO, floor); + for (size_t var_layout = 0; var_layout < variation_maxes.layout; var_layout++) { + for (size_t var_entities = 0; var_entities < variation_maxes.entities; var_entities++) { + uint32_t free_play_key = this->free_play_key(episode, mode, difficulty, floor, var_layout, var_entities); + + auto objects_filename = sdt->map_filename_for_variation( + episode, mode, floor, var_layout, var_entities, SDT::FilenameType::OBJECT_SETS); + auto enemies_filename = sdt->map_filename_for_variation( + episode, mode, floor, var_layout, var_entities, SDT::FilenameType::ENEMY_SETS); + auto events_filename = sdt->map_filename_for_variation( + episode, mode, floor, var_layout, var_entities, SDT::FilenameType::EVENTS); + auto objects_data = objects_filename.empty() ? nullptr : this->load_map_file(v, objects_filename); + auto enemies_data = enemies_filename.empty() ? nullptr : this->load_map_file(v, enemies_filename); + auto events_data = enemies_filename.empty() ? nullptr : this->load_map_file(v, events_filename); + + if (objects_data || enemies_data || events_data) { + // TODO: This is ugly; the hash computation probably should be factored into MapFile + uint64_t source_hash = ((objects_data ? phosg::fnv1a64(*objects_data) : 0) ^ + (enemies_data ? phosg::fnv1a64(*enemies_data) : 0) ^ + (events_data ? phosg::fnv1a64(*events_data) : 0)); + std::shared_ptr map_file; + try { + map_file = new_map_file_for_source_hash.at(source_hash); + } catch (const std::out_of_range&) { + map_file = std::make_shared(floor, objects_data, enemies_data, events_data); + if (map_file->source_hash() != source_hash) { + throw std::logic_error("incorrect source hash"); + } + new_map_file_for_source_hash.emplace(map_file->source_hash(), map_file); + } + + // Uncomment for debugging + // config_log.info_f("Maps for {} {} {} {} {:02X} {:02} {:02} ({:08X} => {:016X}): objects={}({})+0x{:X} enemies={}({})+0x{:X} events={}({})+0x{:X}", + // phosg::name_for_enum(v), + // name_for_episode(episode), + // name_for_mode(mode), + // name_for_difficulty(difficulty), + // floor, + // var_layout, + // var_entities, + // free_play_key, + // map_file->source_hash(), + // objects_filename.empty() ? "(none)" : objects_filename, + // objects_data ? "present" : "missing", + // map_file->count_object_sets(), + // enemies_filename.empty() ? "(none)" : enemies_filename, + // enemies_data ? "present" : "missing", + // map_file->count_enemy_sets(), + // events_filename.empty() ? "(none)" : events_filename, + // events_data ? "present" : "missing", + // map_file->count_events()); + + new_map_files_for_free_play_key[free_play_key].at(static_cast(v)) = map_file; + } + } + } + } + } + } + } + } + + this->map_file_for_source_hash = std::move(new_map_file_for_source_hash); + this->map_files_for_free_play_key = std::move(new_map_files_for_free_play_key); + this->room_layout_index = new_room_layout_index; + this->supermap_for_source_hash_sum.clear(); + this->supermap_for_free_play_key.clear(); +} + +std::shared_ptr DataIndex::get_free_play_supermap( + Episode episode, GameMode mode, Difficulty difficulty, uint8_t floor, uint32_t layout, uint32_t entities) { + uint32_t free_play_key = this->free_play_key(episode, mode, difficulty, floor, layout, entities); + try { + return this->supermap_for_free_play_key.at(free_play_key); + } catch (const std::out_of_range&) { + } + + const std::array, NUM_VERSIONS>* map_files; + try { + map_files = &this->map_files_for_free_play_key.at(free_play_key); + } catch (const std::out_of_range&) { + static_game_data_log.info_f("No maps exist for key {:08X}; cannot construct supermap", free_play_key); + this->supermap_for_free_play_key.emplace(free_play_key, nullptr); + return nullptr; + } + + uint64_t source_hash_sum = 0; + for (auto map_file : *map_files) { + source_hash_sum += map_file ? map_file->source_hash() : 0; + } + + // Uncomment for debugging + // phosg::fwrite_fmt(stderr, "SuperMap for {} {} {} {:02X} {:02X} {:02X} ({:08X}): {:016X} from", + // name_for_episode(episode), + // name_for_mode(mode), + // name_for_difficulty(difficulty), + // floor, + // layout, + // entities, + // free_play_key, + // source_hash_sum); + // for (const auto& map_file : it.second) { + // if (map_file) { + // phosg::fwrite_fmt(stderr, " {:016X}", map_file->source_hash()); + // } else { + // phosg::fwrite_fmt(stderr, " ----------------"); + // } + // } + // fputc('\n', stderr); + + std::shared_ptr supermap; + try { + supermap = this->supermap_for_source_hash_sum.at(source_hash_sum); + static_game_data_log.info_f("Linking existing free play supermap {:016X} for key {:08X}", source_hash_sum, free_play_key); + } catch (const std::out_of_range&) { + supermap = std::make_shared(*map_files, SetDataTableBase::default_floor_to_area(Version::BB_V4, episode)); + this->supermap_for_source_hash_sum.emplace(source_hash_sum, supermap); + static_game_data_log.info_f("Constructed free play supermap {:016X} for key {:08X}", source_hash_sum, free_play_key); + } + this->supermap_for_free_play_key.emplace(free_play_key, supermap); + return supermap; +} + +std::vector> DataIndex::supermaps_for_variations( + Episode episode, GameMode mode, Difficulty difficulty, const Variations& variations) { + std::vector> ret; + for (size_t floor = 0; floor < 0x12; floor++) { + Variations::Entry e; + if (floor < variations.entries.size()) { + e = variations.entries[floor]; + } + ret.push_back(this->get_free_play_supermap(episode, mode, difficulty, floor, e.layout, e.entities)); + if (ret.back()) { + static_game_data_log.info_f("Using supermap {:08X} for floor {:02X} layout {:X} entities {:X}", + this->free_play_key(episode, mode, difficulty, floor, e.layout, e.entities), + floor, e.layout, e.entities); + } else { + static_game_data_log.info_f("No supermap available for floor {:02X} layout {:X} entities {:X}", + floor, e.layout, e.entities); + } + } + return ret; +} + +void DataIndex::load_set_data_tables() { + config_log.info_f("Loading set data tables"); + + std::array, NUM_VERSIONS> new_tables; + std::array, NUM_VERSIONS> new_tables_ep1_ult; + std::shared_ptr new_table_bb_solo; + std::shared_ptr new_table_bb_solo_ep1_ult; + + auto load_table = [&](Version version) -> void { + auto data = this->load_map_file(version, "SetDataTableOn.rel"); + new_tables[static_cast(version)] = std::make_shared(version, *data); + if (!is_v1(version) && (version != Version::PC_NTE)) { + auto data_ep1_ult = this->load_map_file(version, "SetDataTableOnUlti.rel"); + new_tables_ep1_ult[static_cast(version)] = std::make_shared(version, *data_ep1_ult); + } + }; + + new_tables[static_cast(Version::DC_NTE)] = std::make_shared(); + new_tables[static_cast(Version::DC_11_2000)] = std::make_shared(); + load_table(Version::DC_V1); + load_table(Version::DC_V2); + load_table(Version::PC_NTE); + load_table(Version::PC_V2); + load_table(Version::GC_NTE); + load_table(Version::GC_V3); + load_table(Version::XB_V3); + load_table(Version::BB_V4); + + auto bb_solo_data = this->load_map_file(Version::BB_V4, "SetDataTableOff.rel"); + new_table_bb_solo = std::make_shared(Version::BB_V4, *bb_solo_data); + auto bb_solo_data_ep1_ult = this->load_map_file(Version::BB_V4, "SetDataTableOffUlti.rel"); + new_table_bb_solo_ep1_ult = std::make_shared(Version::BB_V4, *bb_solo_data_ep1_ult); + + this->set_data_tables = std::move(new_tables); + this->set_data_tables_ep1_ult = std::move(new_tables_ep1_ult); + this->bb_solo_set_data_table = std::move(new_table_bb_solo); + this->bb_solo_set_data_table_ep1_ult = std::move(new_table_bb_solo_ep1_ult); +} + +void DataIndex::load_battle_params() { + config_log.info_f("Loading JSON battle parameters"); + this->battle_params = std::make_shared(phosg::JSON::parse(phosg::load_file( + "system/tables/battle-params.json"))); +} + +void DataIndex::load_level_tables() { + config_log.info_f("Loading level tables"); + this->level_table_v1_v2 = std::make_shared(phosg::JSON::parse(phosg::load_file( + "system/tables/level-table-v1-v2.json"))); + this->level_table_v3 = std::make_shared(phosg::JSON::parse(phosg::load_file( + "system/tables/level-table-v3.json"))); + this->level_table_v4 = std::make_shared(phosg::JSON::parse(phosg::load_file( + "system/tables/level-table-v4.json"))); +} + +void DataIndex::load_text_index() { + this->text_index = std::make_shared("system/text-sets", [&](Version version, const std::string& filename) -> std::shared_ptr { + try { + if (version == Version::BB_V4) { + return this->load_bb_file(filename); + } else { + return this->pc_patch_file_index->get("Media/PSO/" + filename)->load_data(); + } + } catch (const std::out_of_range&) { + return nullptr; + } catch (const phosg::cannot_open_file&) { + return nullptr; + } + }); +} + +void DataIndex::load_word_select_table() { + config_log.info_f("Loading Word Select table"); + + std::vector> name_alias_lists; + auto json = phosg::JSON::parse(phosg::load_file("system/text-sets/ws-name-alias-lists.json")); + for (const auto& coll_it : json.as_list()) { + auto& coll = name_alias_lists.emplace_back(); + for (const auto& str_it : coll_it->as_list()) { + coll.emplace_back(str_it->as_string()); + } + } + + const std::vector* pc_unitxt_collection = nullptr; + const std::vector* bb_unitxt_collection = nullptr; + std::unique_ptr pc_unitxt_data; + if (this->text_index) { + config_log.debug_f("(Word select) Using PC_V2 unitxt_e.prs from text index"); + pc_unitxt_collection = &this->text_index->get(Version::PC_V2, Language::ENGLISH, 35); + } else { + config_log.debug_f("(Word select) Loading PC_V2 unitxt_e.prs"); + pc_unitxt_data = std::make_unique(phosg::load_file("system/text-sets/pc-v2/unitxt_e.prs")); + pc_unitxt_collection = &pc_unitxt_data->get(35); + } + config_log.debug_f("(Word select) Loading BB_V4 unitxt_ws_e.prs"); + auto bb_unitxt_data = std::make_unique(phosg::load_file("system/text-sets/bb-v4/unitxt_ws_e.prs")); + bb_unitxt_collection = &bb_unitxt_data->get(0); + + config_log.debug_f("(Word select) Loading DC_NTE data"); + WordSelectSet dc_nte_ws(phosg::load_file("system/text-sets/dc-nte/ws_data.bin"), Version::DC_NTE, nullptr, true); + config_log.debug_f("(Word select) Loading DC_11_2000 data"); + WordSelectSet dc_112000_ws(phosg::load_file("system/text-sets/dc-11-2000/ws_data.bin"), Version::DC_11_2000, nullptr, false); + config_log.debug_f("(Word select) Loading DC_V1 data"); + WordSelectSet dc_v1_ws(phosg::load_file("system/text-sets/dc-v1/ws_data.bin"), Version::DC_V1, nullptr, false); + config_log.debug_f("(Word select) Loading DC_V2 data"); + WordSelectSet dc_v2_ws(phosg::load_file("system/text-sets/dc-v2/ws_data.bin"), Version::DC_V2, nullptr, false); + config_log.debug_f("(Word select) Loading PC_NTE data"); + WordSelectSet pc_nte_ws(phosg::load_file("system/text-sets/pc-nte/ws_data.bin"), Version::PC_NTE, pc_unitxt_collection, false); + config_log.debug_f("(Word select) Loading PC_V2 data"); + WordSelectSet pc_v2_ws(phosg::load_file("system/text-sets/pc-v2/ws_data.bin"), Version::PC_V2, pc_unitxt_collection, false); + config_log.debug_f("(Word select) Loading GC_NTE data"); + WordSelectSet gc_nte_ws(phosg::load_file("system/text-sets/gc-nte/ws_data.bin"), Version::GC_NTE, nullptr, false); + config_log.debug_f("(Word select) Loading GC_V3 data"); + WordSelectSet gc_v3_ws(phosg::load_file("system/text-sets/gc-v3/ws_data.bin"), Version::GC_V3, nullptr, false); + config_log.debug_f("(Word select) Loading GC_EP3_NTE data"); + WordSelectSet gc_ep3_nte_ws(phosg::load_file("system/text-sets/gc-ep3-nte/ws_data.bin"), Version::GC_EP3_NTE, nullptr, false); + config_log.debug_f("(Word select) Loading GC_EP3 data"); + WordSelectSet gc_ep3_ws(phosg::load_file("system/text-sets/gc-ep3/ws_data.bin"), Version::GC_EP3, nullptr, false); + config_log.debug_f("(Word select) Loading XB_V3 data"); + WordSelectSet xb_v3_ws(phosg::load_file("system/text-sets/xb-v3/ws_data.bin"), Version::XB_V3, nullptr, false); + config_log.debug_f("(Word select) Loading BB_V4 data"); + WordSelectSet bb_v4_ws(phosg::load_file("system/text-sets/bb-v4/ws_data.bin"), Version::BB_V4, bb_unitxt_collection, false); + + config_log.debug_f("(Word select) Generating table"); + this->word_select_table = std::make_shared( + dc_nte_ws, dc_112000_ws, dc_v1_ws, dc_v2_ws, + pc_nte_ws, pc_v2_ws, gc_nte_ws, gc_v3_ws, + gc_ep3_nte_ws, gc_ep3_ws, xb_v3_ws, bb_v4_ws, + name_alias_lists); +} + +std::shared_ptr DataIndex::create_item_name_index_for_version( + std::shared_ptr pmt, + std::shared_ptr limits, + std::shared_ptr text_index) const { + switch (limits->version) { + case Version::DC_NTE: + return std::make_shared(pmt, limits, text_index->get(Version::DC_NTE, Language::JAPANESE, 2)); + case Version::DC_11_2000: + return std::make_shared(pmt, limits, text_index->get(Version::DC_11_2000, Language::ENGLISH, 2)); + case Version::DC_V1: + return std::make_shared(pmt, limits, text_index->get(Version::DC_V1, Language::ENGLISH, 2)); + case Version::DC_V2: + return std::make_shared(pmt, limits, text_index->get(Version::DC_V2, Language::ENGLISH, 3)); + case Version::PC_NTE: + return std::make_shared(pmt, limits, text_index->get(Version::PC_NTE, Language::ENGLISH, 3)); + case Version::PC_V2: + return std::make_shared(pmt, limits, text_index->get(Version::PC_V2, Language::ENGLISH, 3)); + case Version::GC_NTE: + return std::make_shared(pmt, limits, text_index->get(Version::GC_NTE, Language::ENGLISH, 0)); + case Version::GC_V3: + return std::make_shared(pmt, limits, text_index->get(Version::GC_V3, Language::ENGLISH, 0)); + case Version::XB_V3: + return std::make_shared(pmt, limits, text_index->get(Version::XB_V3, Language::ENGLISH, 0)); + case Version::BB_V4: + return std::make_shared(pmt, limits, text_index->get(Version::BB_V4, Language::ENGLISH, 1)); + default: + return nullptr; + } +} + +void DataIndex::load_item_name_indexes() { + config_log.info_f("Generating item name indexes"); + for (size_t v_s = NUM_PATCH_VERSIONS; v_s < NUM_VERSIONS; v_s++) { + Version v = static_cast(v_s); + config_log.debug_f("Generating item name index for {}", phosg::name_for_enum(v)); + this->item_name_indexes[v_s] = this->create_item_name_index_for_version( + this->item_parameter_table(v), this->item_stack_limits(v), this->text_index); + } + this->item_name_indexes[static_cast(Version::GC_EP3)] = this->item_name_indexes[static_cast(Version::GC_V3)]; + this->item_name_indexes[static_cast(Version::GC_EP3_NTE)] = this->item_name_indexes[static_cast(Version::GC_V3)]; +} + +void DataIndex::load_drop_tables() { + config_log.info_f("Loading item sets"); + + std::unordered_map> new_rare_item_sets; + std::unordered_map> new_common_item_sets; + for (const auto& item : std::filesystem::directory_iterator("system/tables")) { + std::string filename = item.path().filename().string(); + + if (filename.starts_with("common-table-") || filename.starts_with("ItemPT-")) { + std::string path = "system/tables/" + filename; + size_t ext_offset = filename.rfind('.'); + std::string basename = (ext_offset == std::string::npos) ? filename : filename.substr(0, ext_offset); + + if (filename.ends_with(".json")) { + config_log.info_f("Loading JSON common item table {}", filename); + new_common_item_sets.emplace(basename, std::make_shared(phosg::JSON::parse(phosg::load_file(path)))); + } else if (filename.ends_with(".afs")) { + std::string ct_filename; + if (filename.starts_with("ItemPT-")) { + ct_filename = "ItemCT-" + filename.substr(7); + } else if (filename.starts_with("common-table-")) { + ct_filename = "challenge-common-table-" + filename.substr(13); + } else { + throw std::runtime_error(std::format("cannot determine challenge table filename for common table file: {}", filename)); + } + auto data = std::make_shared(phosg::load_file(path)); + std::shared_ptr ct_data; + try { + std::string ct_path = "system/tables/" + ct_filename; + ct_data = std::make_shared(phosg::load_file(ct_path)); + config_log.info_f("Loading AFS common item table {} with challenge table {}", filename, ct_filename); + } catch (const phosg::cannot_open_file&) { + config_log.info_f("Loading AFS common item table {} without challenge table", filename); + } + new_common_item_sets.emplace(basename, std::make_shared(data, ct_data)); + } else if (filename.ends_with(".gsl")) { + config_log.info_f("Loading little-endian GSL common item table {}", filename); + auto data = std::make_shared(phosg::load_file(path)); + new_common_item_sets.emplace(basename, std::make_shared(data, false)); + } else if (filename.ends_with(".gslb")) { + config_log.info_f("Loading big-endian GSL common item table {}", filename); + auto data = std::make_shared(phosg::load_file(path)); + new_common_item_sets.emplace(basename, std::make_shared(data, true)); + } else { + throw std::runtime_error(std::format("unknown format for common table file: {}", filename)); + } + + } else if (filename.starts_with("rare-table-") || filename.starts_with("ItemRT-")) { + std::string path = "system/tables/" + filename; + size_t ext_offset = filename.rfind('.'); + std::string basename = (ext_offset == std::string::npos) ? filename : filename.substr(0, ext_offset); + + std::shared_ptr rare_set; + if (filename.ends_with("-v1.json")) { + config_log.info_f("Loading v1 JSON rare item table {}", filename); + rare_set = std::make_shared(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::DC_V1)); + } else if (filename.ends_with("-v2.json")) { + config_log.info_f("Loading v2 JSON rare item table {}", filename); + rare_set = std::make_shared(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::PC_V2)); + } else if (filename.ends_with("-v3.json")) { + config_log.info_f("Loading v3 JSON rare item table {}", filename); + rare_set = std::make_shared(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::GC_V3)); + } else if (filename.ends_with("-v4.json")) { + config_log.info_f("Loading v4 JSON rare item table {}", filename); + rare_set = std::make_shared(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::BB_V4)); + + } else if (filename.ends_with(".afs")) { + config_log.info_f("Loading AFS rare item table {}", filename); + auto data = std::make_shared(phosg::load_file(path)); + rare_set = std::make_shared(AFSArchive(data), false); + + } else if (filename.ends_with(".gsl")) { + config_log.info_f("Loading GSL rare item table {}", filename); + auto data = std::make_shared(phosg::load_file(path)); + rare_set = std::make_shared(GSLArchive(data, false), false); + + } else if (filename.ends_with(".gslb")) { + config_log.info_f("Loading GSL rare item table {}", filename); + auto data = std::make_shared(phosg::load_file(path)); + rare_set = std::make_shared(GSLArchive(data, true), true); + + } else if (filename.ends_with(".rel")) { + config_log.info_f("Loading REL rare item table {}", filename); + rare_set = std::make_shared(phosg::load_file(path), true); + + } else { + throw std::runtime_error(std::format("unknown format for rare table file: {}", filename)); + } + + if (this->server_global_drop_rate_multiplier != 1.0) { + rare_set->multiply_all_rates(this->server_global_drop_rate_multiplier); + } + new_rare_item_sets.emplace(basename, std::move(rare_set)); + } + } + + config_log.info_f("Loading armor table"); + auto armor_json = phosg::JSON::parse(phosg::load_file("system/tables/armor-shop-random-set.json")); + auto new_armor_random_set = std::make_shared(armor_json); + + config_log.info_f("Loading tool table"); + auto tool_json = phosg::JSON::parse(phosg::load_file("system/tables/tool-shop-random-set.json")); + auto new_tool_random_set = std::make_shared(tool_json); + + config_log.info_f("Loading weapon tables"); + std::array, 4> new_weapon_random_sets; + const char* filenames[4] = { + "system/tables/weapon-shop-random-set-normal.json", + "system/tables/weapon-shop-random-set-hard.json", + "system/tables/weapon-shop-random-set-very-hard.json", + "system/tables/weapon-shop-random-set-ultimate.json", + }; + for (size_t z = 0; z < 4; z++) { + new_weapon_random_sets[z] = std::make_shared( + phosg::JSON::parse(phosg::load_file(filenames[z]))); + } + + config_log.info_f("Loading tekker adjustment set"); + auto tekker_data = phosg::JSON::parse(phosg::load_file("system/tables/tekker-adjustment-set.json")); + auto new_tekker_adjustment_set = std::make_shared(tekker_data); + + this->rare_item_sets = std::move(new_rare_item_sets); + this->common_item_sets = std::move(new_common_item_sets); + this->armor_random_set = std::move(new_armor_random_set); + this->tool_random_set = std::move(new_tool_random_set); + this->weapon_random_sets = std::move(new_weapon_random_sets); + this->tekker_adjustment_set = std::move(new_tekker_adjustment_set); +} + +void DataIndex::load_item_definitions() { + std::array, NUM_VERSIONS> new_item_parameter_tables; + config_log.info_f("Loading item definition tables"); + for (size_t v_s = NUM_PATCH_VERSIONS; v_s < NUM_VERSIONS; v_s++) { + Version v = static_cast(v_s); + std::string json_path = std::format("system/tables/item-parameter-table-{}.json", file_path_token_for_version(v)); + try { + config_log.debug_f("Loading item definition table {}", json_path); + new_item_parameter_tables[v_s] = ItemParameterTable::from_json(phosg::JSON::parse(phosg::load_file(json_path))); + } catch (const std::exception& e) { + std::string path = std::format("system/tables/ItemPMT-{}.prs", file_path_token_for_version(v)); + config_log.debug_f("Cannot load {} ({}); loading item definition table {}", json_path, e.what(), path); + auto data = std::make_shared(prs_decompress(phosg::load_file(path))); + new_item_parameter_tables[v_s] = ItemParameterTable::from_binary(data, v); + } + } + + auto json = phosg::JSON::parse(phosg::load_file("system/tables/translation-table.json")); + auto new_item_translation_table = std::make_shared(json, new_item_parameter_tables); + + config_log.info_f("Creating DC NTE mag metadata table"); + auto new_table_dc_nte = MagMetadataTable::from_binary(nullptr, Version::DC_NTE); + config_log.info_f("Loading DC 11/2000 mag metadata table"); + auto new_table_11_2000 = MagMetadataTable::from_json(phosg::JSON::parse(phosg::load_file( + "system/tables/mag-metadata-table-dc-11-2000.json"))); + config_log.info_f("Loading v1 mag metadata table"); + auto new_table_v1 = MagMetadataTable::from_json(phosg::JSON::parse(phosg::load_file( + "system/tables/mag-metadata-table-v1.json"))); + config_log.info_f("Loading v2 mag metadata table"); + auto new_table_v2 = MagMetadataTable::from_json(phosg::JSON::parse(phosg::load_file( + "system/tables/mag-metadata-table-v2.json"))); + config_log.info_f("Loading v3 mag metadata table"); + auto new_table_v3 = MagMetadataTable::from_json(phosg::JSON::parse(phosg::load_file( + "system/tables/mag-metadata-table-v3.json"))); + config_log.info_f("Loading v4 mag metadata table"); + auto new_table_v4 = MagMetadataTable::from_json(phosg::JSON::parse(phosg::load_file( + "system/tables/mag-metadata-table-v4.json"))); + + this->item_parameter_tables = std::move(new_item_parameter_tables); + this->item_translation_table = std::move(new_item_translation_table); + this->mag_metadata_table_dc_nte = std::move(new_table_dc_nte); + this->mag_metadata_table_dc_11_2000 = std::move(new_table_11_2000); + this->mag_metadata_table_v1 = std::move(new_table_v1); + this->mag_metadata_table_v2 = std::move(new_table_v2); + this->mag_metadata_table_v3 = std::move(new_table_v3); + this->mag_metadata_table_v4 = std::move(new_table_v4); +} + +void DataIndex::load_ep3_cards() { + config_log.info_f("Loading Episode 3 card definitions"); + this->ep3_card_index = std::make_shared( + "system/ep3/card-definitions.mnr", + "system/ep3/card-definitions.mnrd", + "system/ep3/card-text.mnr", + "system/ep3/card-text.mnrd", + "system/ep3/card-dice-text.mnr", + "system/ep3/card-dice-text.mnrd"); + config_log.info_f("Loading Episode 3 trial card definitions"); + this->ep3_card_index_trial = std::make_shared( + "system/ep3/card-definitions-trial.mnr", + "system/ep3/card-definitions-trial.mnrd", + "system/ep3/card-text-trial.mnr", + "system/ep3/card-text-trial.mnrd", + "system/ep3/card-dice-text-trial.mnr", + "system/ep3/card-dice-text-trial.mnrd"); + config_log.info_f("Loading Episode 3 COM decks"); + this->ep3_com_deck_index = std::make_shared("system/ep3/com-decks.json"); +} + +void DataIndex::load_ep3_maps(bool raise_on_any_failure) { + config_log.info_f("Collecting Episode 3 maps"); + this->ep3_map_index = std::make_shared("system/ep3/maps", raise_on_any_failure); +} + +void DataIndex::load_quest_index(bool raise_on_any_failure) { + config_log.info_f("Collecting quests"); + this->quest_index = std::make_shared("system/quests", this->quest_category_index, raise_on_any_failure); +} + +void DataIndex::compile_functions(bool raise_on_any_failure) { + config_log.info_f("Compiling client functions"); + this->client_functions = std::make_shared("system/client-functions", raise_on_any_failure); +} + +void DataIndex::load_dol_files() { + config_log.info_f("Loading DOL files"); + this->dol_file_index = std::make_shared("system/dol"); +} + +void DataIndex::generate_bb_stream_file() { + config_log.info_f("Generating BB stream file"); + auto sf = std::make_shared(); + + auto add_file = [&](const std::string& filename, const void* data, size_t size) -> void { + auto& e = sf->entries.emplace_back(); + e.offset = sf->data.size(); + e.filename = filename; + e.size = size; + e.checksum = phosg::crc32(data, size); + sf->data.append(reinterpret_cast(data), size); + config_log.debug_f( + "[BBStreamFile] Added file {} at offset {:08X} ({:08X} bytes) with checksum {:08X}; total size is now {:08X}", + filename, e.offset, e.size, e.checksum, sf->data.size()); + }; + + auto level_table_data = prs_compress_optimal(this->level_table_v4->serialize_binary_v4()); + auto pmt_data = prs_compress_optimal(this->item_parameter_table(Version::BB_V4)->serialize_binary(Version::BB_V4)); + auto mag_data = prs_compress_optimal(this->mag_metadata_table(Version::BB_V4)->serialize_binary(Version::BB_V4)); + + const auto& bps = *this->battle_params; + add_file("BattleParamEntry.dat", &bps.get_table(true, Episode::EP1), sizeof(BattleParamsIndex::Table)); + add_file("BattleParamEntry_lab.dat", &bps.get_table(true, Episode::EP2), sizeof(BattleParamsIndex::Table)); + add_file("BattleParamEntry_ep4.dat", &bps.get_table(true, Episode::EP4), sizeof(BattleParamsIndex::Table)); + add_file("BattleParamEntry_on.dat", &bps.get_table(false, Episode::EP1), sizeof(BattleParamsIndex::Table)); + add_file("BattleParamEntry_lab_on.dat", &bps.get_table(false, Episode::EP2), sizeof(BattleParamsIndex::Table)); + add_file("BattleParamEntry_ep4_on.dat", &bps.get_table(false, Episode::EP4), sizeof(BattleParamsIndex::Table)); + add_file("PlyLevelTbl.prs", level_table_data.data(), level_table_data.size()); + add_file("ItemMagEdit.prs", mag_data.data(), mag_data.size()); + add_file("ItemPMT.prs", pmt_data.data(), pmt_data.size()); + + this->bb_stream_file = sf; +} + +void DataIndex::load_all() { + this->collect_network_addresses(); + this->load_config_early(); + this->load_bb_private_keys(); + this->load_bb_system_defaults(); + this->load_patch_indexes(); + this->load_ep3_cards(); + this->load_ep3_maps(); + this->compile_functions(); + this->load_dol_files(); + this->load_set_data_tables(); + this->load_maps(); + this->load_battle_params(); + this->load_level_tables(); + this->load_text_index(); + this->load_word_select_table(); + this->load_item_definitions(); + this->load_item_name_indexes(); + this->load_drop_tables(); + this->load_config_late(); + this->load_quest_index(); + this->generate_bb_stream_file(); +} diff --git a/src/DataIndex.hh b/src/DataIndex.hh new file mode 100644 index 00000000..bdee954c --- /dev/null +++ b/src/DataIndex.hh @@ -0,0 +1,412 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Account.hh" +#include "Client.hh" +#include "ClientFunctionIndex.hh" +#include "CommonItemSet.hh" +#include "DNSServer.hh" +#include "DOLFileIndex.hh" +#include "Episode3/DataIndexes.hh" +#include "Episode3/Tournament.hh" +#include "GSLArchive.hh" +#include "IPV4RangeSet.hh" +#include "ItemNameIndex.hh" +#include "ItemParameterTable.hh" +#include "ItemTranslationTable.hh" +#include "LevelTable.hh" +#include "Lobby.hh" +#include "MagMetadataTable.hh" +#include "Menu.hh" +#include "Quest.hh" +#include "ShopRandomSets.hh" +#include "TeamIndex.hh" +#include "TekkerAdjustmentSet.hh" +#include "WordSelectTable.hh" + +struct DataIndex { + // This structure contains everything which is essentially immutable during the server's uptime - e.g. configuration, + // tables, item definitions, etc. which are only changed by reloading them from disk. Mutable structures, like + // AccountIndex and TeamIndex, are on ServerState instead. + + struct PortConfiguration { + std::string name; + std::string addr; // Blank = listen on all interfaces (default) + uint16_t port; + Version version; + ServerBehavior behavior; + }; + + struct CheatFlags { + // This structure describes which behaviors are considered cheating (that is, require cheat mode to be enabled or the + // user to have the CHEAT_ANYWHERE account flag). A false value here means that that particular behavior is NOT + // cheating, so cheat mode is NOT required. + bool create_items = true; + bool edit_section_id = true; + bool edit_stats = true; + bool ep3_replace_assist = true; + bool ep3_unset_field_character = true; + bool infinite_hp_tp = true; + bool fast_kills = true; + bool insufficient_minimum_level = true; + bool override_random_seed = true; + bool override_section_id = true; + bool override_variations = true; + bool proxy_override_drops = true; + bool reset_materials = false; + bool warp = true; + + CheatFlags() = default; + explicit CheatFlags(const phosg::JSON& json); + }; + + struct BBStreamFile { + struct Entry { + uint32_t offset; + uint32_t size; + uint32_t checksum; // crc32 + std::string filename; + }; + std::vector entries; + std::string data; + }; + + enum class RunShellBehavior { + DEFAULT = 0, + ALWAYS, + NEVER, + }; + enum class BehaviorSwitch { + OFF = 0, + OFF_BY_DEFAULT, + ON_BY_DEFAULT, + ON, + }; + + static inline bool behavior_enabled(BehaviorSwitch b) { + return (b == BehaviorSwitch::ON_BY_DEFAULT) || (b == BehaviorSwitch::ON); + } + static inline bool behavior_can_be_overridden(BehaviorSwitch b) { + return (b == BehaviorSwitch::OFF_BY_DEFAULT) || (b == BehaviorSwitch::ON_BY_DEFAULT); + } + + uint64_t creation_time; + + std::string config_filename; + std::shared_ptr config_json; + bool one_time_config_loaded = false; + + size_t num_worker_threads = 0; + + std::string name; + std::unordered_map name_to_port_config; + std::unordered_map number_to_port_config; + std::string username; + std::string dns_server_addr; + uint16_t dns_server_port = 0; + std::vector ip_stack_addresses; + std::vector ppp_stack_addresses; + std::vector ppp_raw_addresses; + std::vector http_addresses; + uint64_t client_ping_interval_usecs = 30000000; + uint64_t client_idle_timeout_usecs = 60000000; + uint64_t patch_client_idle_timeout_usecs = 300000000; + bool is_debug = false; + bool ip_stack_debug = false; + bool allow_unregistered_users = false; + bool allow_pc_nte = false; + bool use_temp_accounts_for_prototypes = true; + bool allow_same_account_concurrent_logins = true; + std::array compatibility_groups = {}; + bool enable_chat_commands = true; + char chat_command_sentinel = '\0'; // 0 = default (@ on 11/2000; $ on all other versions) + size_t num_backup_character_slots = 16; + std::unique_ptr> version_name_colors; + uint32_t client_customization_name_color = 0x00000000; + uint8_t allowed_drop_modes_v1_v2_normal = 0x1F; + uint8_t allowed_drop_modes_v1_v2_battle = 0x07; + uint8_t allowed_drop_modes_v1_v2_challenge = 0x07; + uint8_t allowed_drop_modes_v3_normal = 0x1F; + uint8_t allowed_drop_modes_v3_battle = 0x07; + uint8_t allowed_drop_modes_v3_challenge = 0x07; + uint8_t allowed_drop_modes_v4_normal = 0x1D; // CLIENT not allowed + uint8_t allowed_drop_modes_v4_battle = 0x05; + uint8_t allowed_drop_modes_v4_challenge = 0x05; + ServerDropMode default_drop_mode_v1_v2_normal = ServerDropMode::CLIENT; + ServerDropMode default_drop_mode_v1_v2_battle = ServerDropMode::CLIENT; + ServerDropMode default_drop_mode_v1_v2_challenge = ServerDropMode::CLIENT; + ServerDropMode default_drop_mode_v3_normal = ServerDropMode::CLIENT; + ServerDropMode default_drop_mode_v3_battle = ServerDropMode::CLIENT; + ServerDropMode default_drop_mode_v3_challenge = ServerDropMode::CLIENT; + ServerDropMode default_drop_mode_v4_normal = ServerDropMode::SERVER_SHARED; + ServerDropMode default_drop_mode_v4_battle = ServerDropMode::SERVER_SHARED; + ServerDropMode default_drop_mode_v4_challenge = ServerDropMode::SERVER_SHARED; + std::unordered_map quest_flag_rewrites_v1_v2; + std::unordered_map quest_flag_rewrites_v3; + std::unordered_map quest_flag_rewrites_v4; + std::unordered_map> quest_counter_fields; // For $qfread command + uint64_t persistent_game_idle_timeout_usecs = 0; + std::unordered_map enable_send_function_call_quest_numbers; + bool enable_v3_v4_protected_subcommands = false; + bool ep3_infinite_meseta = false; + std::vector ep3_defeat_player_meseta_rewards = {400, 500, 600, 700, 800}; + std::vector ep3_defeat_com_meseta_rewards = {100, 200, 300, 400, 500}; + uint32_t ep3_final_round_meseta_bonus = 300; + bool ep3_jukebox_is_free = false; + uint32_t ep3_behavior_flags = 0; + bool hide_download_commands = true; + bool censor_credentials = true; + RunShellBehavior run_shell_behavior = RunShellBehavior::DEFAULT; + BehaviorSwitch cheat_mode_behavior = BehaviorSwitch::OFF_BY_DEFAULT; + bool default_switch_assist_enabled = false; + bool use_game_creator_section_id = false; + bool rare_notifs_enabled_for_client_drops = false; + bool default_rare_notifs_enabled_v1_v2 = false; + bool default_rare_notifs_enabled_v3_v4 = false; + std::unordered_set notify_game_for_item_primary_identifiers_v1_v2; + std::unordered_set notify_game_for_item_primary_identifiers_v3; + std::unordered_set notify_game_for_item_primary_identifiers_v4; + std::unordered_set notify_server_for_item_primary_identifiers_v1_v2; + std::unordered_set notify_server_for_item_primary_identifiers_v3; + std::unordered_set notify_server_for_item_primary_identifiers_v4; + bool notify_server_for_max_level_achieved = false; + std::vector> bb_private_keys; + std::shared_ptr> bb_default_keyboard_config; + std::shared_ptr> bb_default_joystick_config; + std::shared_ptr client_functions; + std::shared_ptr pc_patch_file_index; + std::shared_ptr bb_patch_file_index; + std::unordered_map> map_file_for_source_hash; + std::map, NUM_VERSIONS>> map_files_for_free_play_key; + std::unordered_map> supermap_for_source_hash_sum; + std::unordered_map> supermap_for_free_play_key; + std::shared_ptr room_layout_index; + std::shared_ptr bb_stream_file; + std::shared_ptr dol_file_index; + std::shared_ptr ep3_card_index; + std::shared_ptr ep3_card_index_trial; + std::shared_ptr ep3_map_index; + std::shared_ptr ep3_com_deck_index; + std::shared_ptr ep3_default_ex_values; + std::shared_ptr ep3_tournament_ex_values; + std::shared_ptr ep3_tournament_final_round_ex_values; + std::shared_ptr quest_category_index; + std::shared_ptr quest_index; + std::shared_ptr level_table_v1_v2; + std::shared_ptr level_table_v3; + std::shared_ptr level_table_v4; + std::shared_ptr battle_params; + std::shared_ptr bb_data_gsl; + std::unordered_map> common_item_sets; + std::unordered_map> rare_item_sets; + std::shared_ptr armor_random_set; + std::shared_ptr tool_random_set; + std::array, 4> weapon_random_sets; // Keyed on difficulty + std::shared_ptr tekker_adjustment_set; + std::array, NUM_VERSIONS> item_parameter_tables; + std::shared_ptr item_translation_table; + std::array, NUM_VERSIONS> item_stack_limits_tables; + size_t bb_max_bank_items = 200; + size_t bb_max_bank_meseta = 999999; + std::shared_ptr mag_metadata_table_dc_nte; + std::shared_ptr mag_metadata_table_dc_11_2000; + std::shared_ptr mag_metadata_table_v1; + std::shared_ptr mag_metadata_table_v2; + std::shared_ptr mag_metadata_table_v3; + std::shared_ptr mag_metadata_table_v4; + std::shared_ptr text_index; + std::array, NUM_VERSIONS> item_name_indexes; + std::shared_ptr word_select_table; + std::array, NUM_VERSIONS> set_data_tables; + std::array, NUM_VERSIONS> set_data_tables_ep1_ult; + std::shared_ptr bb_solo_set_data_table; + std::shared_ptr bb_solo_set_data_table_ep1_ult; + std::array, 4> rare_enemy_rates_by_difficulty; + std::shared_ptr rare_enemy_rates_challenge; + std::array, 3> min_levels_v1_v2; // Indexed as [episode][difficulty] + std::array, 3> min_levels_v3; // Indexed as [episode][difficulty] + std::array, 3> min_levels_v4; // Indexed as [episode][difficulty] + std::unordered_set bb_required_patches; + std::unordered_set auto_patches; + CheatFlags cheat_flags; + + struct QuestF960Result { + uint32_t meseta_cost = 0; + uint32_t base_probability = 0; + uint32_t probability_upgrade = 0; + std::array, 7> results; + + QuestF960Result() = default; + QuestF960Result( + const phosg::JSON& json, std::shared_ptr name_index, const ItemData::StackLimits& limits); + }; + + // Indexed as [type][difficulty][random_choice] + std::vector>> quest_F95E_results; + std::vector> quest_F95F_results; // [(num_photon_tickets, item)] + std::vector quest_F960_success_results; + QuestF960Result quest_F960_failure_results; + float bb_global_exp_multiplier = 1.0f; + float exp_share_multiplier = 0.5f; + float server_global_drop_rate_multiplier = 1.0f; + + uint16_t ep3_card_auction_points = 0; + uint16_t ep3_card_auction_min_size = 0; + uint16_t ep3_card_auction_max_size = 0; + struct CardAuctionPoolEntry { + uint64_t probability; + uint16_t card_id; + uint16_t min_price; + }; + std::vector ep3_card_auction_pool; + std::array, 5> ep3_trap_card_ids; + struct Ep3LobbyBannerEntry { + uint32_t type = 1; + uint32_t which; // See B9 documentation in CommandFormats.hh + std::string data; + }; + std::vector ep3_lobby_banners; + + std::shared_ptr banned_ipv4_ranges; + + phosg::JSON team_reward_defs_json; + + std::shared_ptr information_menu_v2; + std::shared_ptr information_menu_v3; + std::shared_ptr> information_contents_v2; + std::shared_ptr> information_contents_v3; + std::shared_ptr proxy_destinations_menu_dc; + std::shared_ptr proxy_destinations_menu_pc; + std::shared_ptr proxy_destinations_menu_gc; + std::shared_ptr proxy_destinations_menu_xb; + std::vector> proxy_destinations_dc; + std::vector> proxy_destinations_pc; + std::vector> proxy_destinations_gc; + std::vector> proxy_destinations_xb; + std::optional> proxy_destination_patch; + std::optional> proxy_destination_bb; + std::string welcome_message; + std::string pc_patch_server_message; + std::string bb_patch_server_message; + + std::array, NUM_VERSIONS> public_lobby_search_orders; + std::vector client_customization_public_lobby_search_order; + uint8_t pre_lobby_event = 0; + std::vector per_lobby_events; + int32_t ep3_menu_song = -1; + + std::map all_addresses; + uint32_t local_address = 0; + uint32_t external_address = 0; + + bool proxy_allow_save_files = true; + + explicit DataIndex(const std::string& config_filename = ""); + + uint32_t connect_address_for_client(std::shared_ptr c) const; + uint16_t game_server_port_for_version(Version v) const; + + std::shared_ptr information_menu(Version version) const; + std::shared_ptr proxy_destinations_menu(Version version) const; + const std::vector>& proxy_destinations(Version version) const; + + std::shared_ptr set_data_table( + Version version, Episode episode, GameMode mode, Difficulty difficulty) const; + + inline std::shared_ptr weapon_random_set(Difficulty difficulty) const { + return this->weapon_random_sets.at(static_cast(difficulty)); + } + inline std::shared_ptr rare_enemy_rates(Difficulty difficulty) const { + return this->rare_enemy_rates_by_difficulty.at(static_cast(difficulty)); + } + + std::shared_ptr level_table(Version version) const; + std::shared_ptr item_parameter_table(Version version) const; + std::shared_ptr item_parameter_table_for_encode(Version version) const; + std::shared_ptr mag_metadata_table(Version version) const; + std::shared_ptr item_stack_limits(Version version) const; + std::shared_ptr item_name_index_opt(Version version) const; // Returns null if missing + std::shared_ptr item_name_index(Version version) const; // Throws if missing + std::string describe_item(Version version, const ItemData& item, uint8_t flags = 0) const; + ItemData parse_item_description(Version version, const std::string& description) const; + + std::shared_ptr common_item_set(Version logic_version, std::shared_ptr q) const; + std::shared_ptr rare_item_set(Version logic_version, std::shared_ptr q) const; + + const std::vector& public_lobby_search_order(Version version, bool is_client_customization) const; + inline const std::vector& public_lobby_search_order(std::shared_ptr c) const { + return this->public_lobby_search_order(c->version(), c->check_flag(Client::Flag::IS_CLIENT_CUSTOMIZATION)); + } + + inline uint32_t name_color_for_client(Version v, bool is_client_customization) const { + if (is_client_customization && this->client_customization_name_color) { + return this->client_customization_name_color; + } + return this->version_name_colors ? this->version_name_colors->at(static_cast(v) - NUM_PATCH_VERSIONS) : 0; + } + inline uint32_t name_color_for_client(std::shared_ptr c) const { + return this->name_color_for_client(c->version(), c->check_flag(Client::Flag::IS_CLIENT_CUSTOMIZATION)); + } + + std::shared_ptr> information_contents_for_client(std::shared_ptr c) const; + + size_t default_min_level_for_game(Version version, Episode episode, Difficulty difficulty) const; + + void set_port_configuration(const std::vector& port_configs); + + std::shared_ptr load_bb_file(const std::string& patch_index_filename) const; + std::shared_ptr load_map_file(Version version, const std::string& filename) const; + + std::pair parse_port_spec(const phosg::JSON& json) const; + std::vector parse_port_configuration(const phosg::JSON& json) const; + + static constexpr uint32_t free_play_key( + Episode episode, GameMode mode, Difficulty difficulty, uint8_t floor, uint32_t layout, uint32_t entities) { + return (static_cast(episode) << 28) | + (static_cast(mode) << 26) | + (static_cast(difficulty) << 24) | + (static_cast(floor) << 16) | + (static_cast(layout) << 8) | + (static_cast(entities) << 0); + } + std::shared_ptr get_free_play_supermap( + Episode episode, GameMode mode, Difficulty difficulty, uint8_t floor, uint32_t layout, uint32_t entities); + std::vector> supermaps_for_variations( + Episode episode, GameMode mode, Difficulty difficulty, const Variations& variations); + + void collect_network_addresses(); + void load_config_early(); + void load_config_late(); + void load_bb_private_keys(); + void load_bb_system_defaults(); + void load_patch_indexes(); + void load_maps(); + void load_battle_params(); + void load_level_tables(); + void load_text_index(); + std::shared_ptr create_item_name_index_for_version( + std::shared_ptr pmt, + std::shared_ptr limits, + std::shared_ptr text_index) const; + void load_item_name_indexes(); + void load_drop_tables(); + void load_item_definitions(); + void load_set_data_tables(); + void load_word_select_table(); + void load_ep3_cards(); + void load_ep3_maps(bool raise_on_any_failure = false); + void load_quest_index(bool raise_on_any_failure = false); + void compile_functions(bool raise_on_any_failure = false); + void load_dol_files(); + void generate_bb_stream_file(); + + void load_all(); +}; diff --git a/src/Episode3/Tournament.hh b/src/Episode3/Tournament.hh index 68cdf45a..7c56de9b 100644 --- a/src/Episode3/Tournament.hh +++ b/src/Episode3/Tournament.hh @@ -13,7 +13,7 @@ struct Lobby; class Client; -struct ServerState; +class ServerState; namespace Episode3 { diff --git a/src/GameServer.cc b/src/GameServer.cc index 14bf5fbe..dd8fe848 100644 --- a/src/GameServer.cc +++ b/src/GameServer.cc @@ -114,7 +114,7 @@ std::vector> GameServer::get_clients_by_identifier(const std::shared_ptr GameServer::create_client( std::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 (this->state->data->banned_ipv4_ranges->check(addr)) { if (client_sock.is_open()) { client_sock.close(); } @@ -129,7 +129,7 @@ std::shared_ptr GameServer::create_client( "", phosg::TerminalFormat::FG_YELLOW, phosg::TerminalFormat::FG_GREEN, - this->state->censor_credentials, + this->state->data->censor_credentials, false); auto c = std::make_shared(this->shared_from_this(), channel, listen_sock->behavior); this->log.info_f("Client connected: C-{:X} via {}", c->id, listen_sock->name); diff --git a/src/HTTPServer.cc b/src/HTTPServer.cc index 20072588..f7380b74 100644 --- a/src/HTTPServer.cc +++ b/src/HTTPServer.cc @@ -55,7 +55,7 @@ HTTPServer::HTTPServer(std::shared_ptr state) this->router.add(HTTPRequest::Method::GET, "/y/clients", [this](ArgsT&&) -> RetT { auto res = std::make_shared(phosg::JSON::list()); for (const auto& c : this->state->game_server->all_clients()) { - auto item_name_index = this->state->item_name_index_opt(c->version()); + auto item_name_index = this->state->data->item_name_index_opt(c->version()); const char* drop_notifications_mode = "unknown"; switch (c->get_drop_notification_mode()) { @@ -299,7 +299,7 @@ HTTPServer::HTTPServer(std::shared_ptr state) for (const auto& [_, l] : this->state->id_to_lobby) { auto leader = l->clients[l->leader_id]; Version v = leader ? leader->version() : Version::BB_V4; - auto item_name_index = this->state->item_name_index_opt(v); + auto item_name_index = this->state->data->item_name_index_opt(v); auto client_ids_json = phosg::JSON::list(); for (size_t z = 0; z < l->max_clients; z++) { @@ -559,17 +559,17 @@ HTTPServer::HTTPServer(std::shared_ptr state) lobby_count++; } } - uint64_t uptime_usecs = phosg::now() - this->state->creation_time; + uint64_t uptime_usecs = phosg::now() - this->state->data->creation_time; return phosg::JSON::dict({ - {"StartTimeUsecs", this->state->creation_time}, - {"StartTime", phosg::format_time(this->state->creation_time)}, + {"StartTimeUsecs", this->state->data->creation_time}, + {"StartTime", phosg::format_time(this->state->data->creation_time)}, {"UptimeUsecs", uptime_usecs}, {"Uptime", phosg::format_duration(uptime_usecs)}, {"LobbyCount", lobby_count}, {"GameCount", game_count}, {"ClientCount", this->state->game_server->all_clients().size() - ProxySession::num_proxy_sessions}, {"ProxySessionCount", ProxySession::num_proxy_sessions}, - {"ServerName", this->state->name}, + {"ServerName", this->state->data->name}, }); }; @@ -578,7 +578,7 @@ HTTPServer::HTTPServer(std::shared_ptr state) }); this->router.add(HTTPRequest::Method::GET, "/y/config", [this](ArgsT&&) -> RetT { - co_return this->state->config_json; + co_return this->state->data->config_json; }); this->router.add(HTTPRequest::Method::GET, "/y/summary", [this, generate_server_info_json](ArgsT&&) -> RetT { @@ -644,14 +644,18 @@ HTTPServer::HTTPServer(std::shared_ptr state) }); this->router.add(HTTPRequest::Method::GET, "/y/data/ep3/cards", [this](ArgsT&& args) -> RetT { - auto& index = args.req.query_params.count("trial") ? this->state->ep3_card_index_trial : this->state->ep3_card_index; + auto& index = args.req.query_params.count("trial") + ? this->state->data->ep3_card_index_trial + : this->state->data->ep3_card_index; co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> std::shared_ptr { return std::make_shared(index->definitions_json()); }); }); this->router.add(HTTPRequest::Method::GET, "/y/data/ep3/card/:card_id", [this](ArgsT&& args) -> RetT { - auto& index = args.req.query_params.count("trial") ? this->state->ep3_card_index_trial : this->state->ep3_card_index; + auto& index = args.req.query_params.count("trial") + ? this->state->data->ep3_card_index_trial + : this->state->data->ep3_card_index; uint32_t card_id = args.get_param("card_id"); try { co_return std::make_shared(index->definition_for_id(card_id)->def.json()); @@ -663,7 +667,7 @@ HTTPServer::HTTPServer(std::shared_ptr state) this->router.add(HTTPRequest::Method::GET, "/y/data/ep3/maps", [this](ArgsT&&) -> RetT { co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> std::shared_ptr { auto ret = std::make_shared(phosg::JSON::dict()); - for (const auto& [map_number, map] : this->state->ep3_map_index->all_maps()) { + for (const auto& [map_number, map] : this->state->data->ep3_map_index->all_maps()) { auto languages_json = phosg::JSON::list(); for (const auto& vm : map->all_versions()) { if (vm) { @@ -684,7 +688,7 @@ HTTPServer::HTTPServer(std::shared_ptr state) 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, [&]() -> std::shared_ptr { try { - auto map = this->state->ep3_map_index->map_for_id(args.get_param("map_number", true)); + auto map = this->state->data->ep3_map_index->map_for_id(args.get_param("map_number", true)); auto vm = map->version(language_for_name(args.params.at("language"))); return std::make_shared(vm->map->json(vm->language)); } catch (const std::out_of_range&) { @@ -696,7 +700,7 @@ HTTPServer::HTTPServer(std::shared_ptr state) 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 map = this->state->data->ep3_map_index->map_for_id(args.get_param("map_number")); auto vm = map->version(language_for_name(args.params.at("language"))); std::string data(reinterpret_cast(vm->map.get()), sizeof(Episode3::MapDefinition)); return RawResponse{.content_type = "application/octet-stream", .data = std::move(data)}; @@ -708,7 +712,7 @@ HTTPServer::HTTPServer(std::shared_ptr state) this->router.add(HTTPRequest::Method::GET, "/y/data/common-tables", [this](ArgsT&&) -> RetT { auto ret = std::make_shared(phosg::JSON::list()); - for (const auto& it : this->state->common_item_sets) { + for (const auto& it : this->state->data->common_item_sets) { ret->emplace_back(it.first); } co_return ret; @@ -716,7 +720,7 @@ HTTPServer::HTTPServer(std::shared_ptr state) this->router.add(HTTPRequest::Method::GET, "/y/data/common-table/:table_name", [this](ArgsT&& args) -> RetT { try { - const auto& table = this->state->common_item_sets.at(args.params.at("table_name")); + const auto& table = this->state->data->common_item_sets.at(args.params.at("table_name")); co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> std::shared_ptr { return std::make_shared(table->json()); }); @@ -727,7 +731,7 @@ HTTPServer::HTTPServer(std::shared_ptr state) this->router.add(HTTPRequest::Method::GET, "/y/data/rare-tables", [this](ArgsT&&) -> RetT { auto ret = std::make_shared(phosg::JSON::list()); - for (const auto& it : this->state->rare_item_sets) { + for (const auto& it : this->state->data->rare_item_sets) { ret->emplace_back(it.first); } co_return ret; @@ -736,16 +740,16 @@ HTTPServer::HTTPServer(std::shared_ptr state) this->router.add(HTTPRequest::Method::GET, "/y/data/rare-table/:table_name", [this](ArgsT&& args) -> RetT { try { const auto& table_name = args.params.at("table_name"); - const auto& table = this->state->rare_item_sets.at(table_name); + const auto& table = this->state->data->rare_item_sets.at(table_name); std::shared_ptr name_index; if (table_name.ends_with("-v1")) { - name_index = this->state->item_name_index_opt(Version::DC_V1); + name_index = this->state->data->item_name_index_opt(Version::DC_V1); } else if (table_name.ends_with("-v2")) { - name_index = this->state->item_name_index_opt(Version::PC_V2); + name_index = this->state->data->item_name_index_opt(Version::PC_V2); } else if (table_name.ends_with("-v3")) { - name_index = this->state->item_name_index_opt(Version::GC_V3); + name_index = this->state->data->item_name_index_opt(Version::GC_V3); } else if (table_name.ends_with("-v4")) { - name_index = this->state->item_name_index_opt(Version::BB_V4); + name_index = this->state->data->item_name_index_opt(Version::BB_V4); } co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> std::shared_ptr { return std::make_shared(table->json(name_index)); @@ -757,13 +761,13 @@ HTTPServer::HTTPServer(std::shared_ptr state) this->router.add(HTTPRequest::Method::GET, "/y/data/quests", [this](ArgsT&&) -> RetT { co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> std::shared_ptr { - return std::make_shared(this->state->quest_index->json()); + return std::make_shared(this->state->data->quest_index->json()); }); }); this->router.add(HTTPRequest::Method::GET, "/y/data/quest/:quest_num", [this](ArgsT&& args) -> RetT { uint32_t quest_num = args.get_param("quest_num"); - auto q = this->state->quest_index->get(quest_num); + auto q = this->state->data->quest_index->get(quest_num); if (!q) { throw HTTPError(404, "Quest does not exist"); } diff --git a/src/IPStackSimulator.cc b/src/IPStackSimulator.cc index 5b3f045e..835a7532 100644 --- a/src/IPStackSimulator.cc +++ b/src/IPStackSimulator.cc @@ -145,7 +145,7 @@ void IPSSClient::reschedule_idle_timeout() { throw std::runtime_error("cannot reschedule idle timeout when simulator is missing"); } this->idle_timeout_timer.cancel(); - this->idle_timeout_timer.expires_after(std::chrono::microseconds(sim->get_state()->client_idle_timeout_usecs)); + this->idle_timeout_timer.expires_after(std::chrono::microseconds(sim->get_state()->data->client_idle_timeout_usecs)); this->idle_timeout_timer.async_wait([this, sim](std::error_code ec) { if (!ec) { sim->log.info_f("Idle timeout expired on N-{:X}", this->network_id); @@ -1358,8 +1358,8 @@ asio::awaitable IPStackSimulator::open_server_connection( std::string conn_str = this->str_for_tcp_connection(c, conn); // Figure out which logical port the connection should go to - auto port_config_it = this->state->number_to_port_config.find(conn->server_port); - if (port_config_it == this->state->number_to_port_config.end()) { + auto port_config_it = this->state->data->number_to_port_config.find(conn->server_port); + if (port_config_it == this->state->data->number_to_port_config.end()) { this->log.error_f("TCP connection {} is to undefined port {}", conn_str, conn->server_port); co_await this->close_tcp_connection(c, conn); co_return; @@ -1370,20 +1370,20 @@ asio::awaitable IPStackSimulator::open_server_connection( this->shared_from_this(), c, conn, - port_config->version, + port_config.version, Language::ENGLISH, "", phosg::TerminalFormat::END, phosg::TerminalFormat::END, false, - this->state->censor_credentials); + this->state->data->censor_credentials); if (!this->state->game_server.get()) { this->log.error_f("No server available for TCP connection {}", conn_str); co_await this->close_tcp_connection(c, conn); co_return; } else { - this->state->game_server->connect_channel(conn->server_channel, conn->server_port, port_config->behavior); + this->state->game_server->connect_channel(conn->server_channel, conn->server_port, port_config.behavior); this->log.info_f("Connected TCP connection {} to game server", conn_str); } } @@ -1403,7 +1403,7 @@ asio::awaitable IPStackSimulator::close_tcp_connection( std::shared_ptr IPStackSimulator::create_client( std::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 (this->state->data->banned_ipv4_ranges->check(addr)) { if (client_sock.is_open()) { client_sock.close(); } diff --git a/src/Items.cc b/src/Items.cc index c037421c..0ff03dce 100644 --- a/src/Items.cc +++ b/src/Items.cc @@ -21,7 +21,7 @@ void player_use_item(std::shared_ptr c, size_t item_index, std::shared_p // Nothing to do (it should be deleted) } else if ((primary_identifier & 0xFFFF0000) == 0x03020000) { // Technique disk - auto item_parameter_table = s->item_parameter_table(c->version()); + auto item_parameter_table = s->data->item_parameter_table(c->version()); uint8_t max_level = item_parameter_table->get_max_tech_level(player->disp.visual.sh.char_class, item.data.data1[4]); if (item.data.data1[2] > max_level) { throw std::runtime_error("technique level too high"); @@ -35,7 +35,7 @@ void player_use_item(std::shared_ptr c, size_t item_index, std::shared_p auto& weapon = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::WEAPON)]; // Only enforce grind limits on BB, since the server doesn't have direct control over inventories on other versions - auto item_parameter_table = s->item_parameter_table(c->version()); + auto item_parameter_table = s->data->item_parameter_table(c->version()); auto weapon_def = item_parameter_table->get_weapon(weapon.data.data1[1], weapon.data.data1[2]); if (is_v4 && (weapon.data.data1[3] >= weapon_def.max_grind)) { throw std::runtime_error("weapon already at maximum grind"); @@ -100,9 +100,9 @@ void player_use_item(std::shared_ptr c, size_t item_index, std::shared_p } armor.data.data1[5]++; - } else if (item.data.is_wrapped(*s->item_stack_limits(c->version()))) { + } else if (item.data.is_wrapped(*s->data->item_stack_limits(c->version()))) { // Unwrap present - item.data.unwrap(*s->item_stack_limits(c->version())); + item.data.unwrap(*s->data->item_stack_limits(c->version())); should_delete_item = false; } else if (primary_identifier == 0x00330000) { @@ -131,7 +131,7 @@ void player_use_item(std::shared_ptr c, size_t item_index, std::shared_p } else if ((primary_identifier & 0xFFFF0000) == 0x030C0000) { // Non-combo mag cells auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)]; - uint8_t evolution_number = s->mag_metadata_table(c->version())->get_evolution_number(mag.data.data1[1]); + uint8_t evolution_number = s->data->mag_metadata_table(c->version())->get_evolution_number(mag.data.data1[1]); if (evolution_number < 4) { switch (item.data.data1[2]) { case 0x00: // Cell of MAG 502 @@ -159,7 +159,7 @@ void player_use_item(std::shared_ptr c, size_t item_index, std::shared_p } else if ((primary_identifier & 0xFFFF0000) == 0x03150000) { // Christmas Present, etc. - use unwrap_table + probabilities therein - auto item_parameter_table = s->item_parameter_table(c->version()); + auto item_parameter_table = s->data->item_parameter_table(c->version()); auto table = item_parameter_table->get_event_items(item.data.data1[2]); size_t sum = 0; for (size_t z = 0; z < table.second; z++) { @@ -198,7 +198,7 @@ void player_use_item(std::shared_ptr c, size_t item_index, std::shared_p // The unopened Hunters Report's rank is stored in the kill count field; using the unopened report copies the rank // to data1[2] and replaces the inventory item with a new item with the same ID. The game also moves the item to // the end of the inventory, so we do the same. - const auto& stack_limits = *s->item_stack_limits(c->version()); + const auto& stack_limits = *s->data->item_stack_limits(c->version()); auto report_item = player->remove_item(item.data.id, 1, stack_limits); report_item.data1[2] = report_item.get_kill_count(); player->add_item(report_item, stack_limits); @@ -213,7 +213,7 @@ void player_use_item(std::shared_ptr c, size_t item_index, std::shared_p continue; } try { - auto item_parameter_table = s->item_parameter_table(c->version()); + auto item_parameter_table = s->data->item_parameter_table(c->version()); const auto& combo = item_parameter_table->get_item_combination(item.data, inv_item.data); if (combo.char_class != 0xFF && combo.char_class != player->disp.visual.sh.char_class) { throw std::runtime_error("item combination requires specific char_class"); @@ -260,7 +260,7 @@ void player_use_item(std::shared_ptr c, size_t item_index, std::shared_p if (should_delete_item) { // Allow overdrafting meseta if the client is not BB, since the server isn't informed when meseta is added or // removed from the bank. - player->remove_item(item.data.id, 1, *s->item_stack_limits(c->version())); + player->remove_item(item.data.id, 1, *s->data->item_stack_limits(c->version())); } } @@ -488,8 +488,8 @@ void player_feed_mag(std::shared_ptr c, size_t mag_item_index, size_t fe apply_mag_feed_result( player->inventory.items[mag_item_index].data, player->inventory.items[fed_item_index].data, - s->item_parameter_table(c->version()), - s->mag_metadata_table(c->version()), + s->data->item_parameter_table(c->version()), + s->data->mag_metadata_table(c->version()), player->disp.visual.sh.char_class, player->disp.visual.sh.section_id, !is_v1_or_v2(c->version())); diff --git a/src/Lobby.cc b/src/Lobby.cc index f9b980e2..80e5f29d 100644 --- a/src/Lobby.cc +++ b/src/Lobby.cc @@ -172,7 +172,7 @@ uint8_t Lobby::area_for_floor(Version version, uint8_t floor) const { if (this->quest) { return this->quest->meta.floor_assignments.at(floor).area; } - auto sdt = this->require_server_state()->set_data_table(version, this->episode, this->mode, this->difficulty); + auto sdt = this->require_server_state()->data->set_data_table(version, this->episode, this->mode, this->difficulty); return sdt->default_floor_to_area(this->episode).at(floor); } @@ -215,14 +215,14 @@ void Lobby::create_item_creator(Version logic_version) { effective_section_id = 0x00; } this->item_creator = std::make_shared( - s->common_item_set(logic_version, this->quest), - s->rare_item_set(logic_version, this->quest), - s->armor_random_set, - s->tool_random_set, - s->weapon_random_set(this->difficulty), - s->tekker_adjustment_set, - s->item_parameter_table(logic_version), - s->item_stack_limits(logic_version), + s->data->common_item_set(logic_version, this->quest), + s->data->rare_item_set(logic_version, this->quest), + s->data->armor_random_set, + s->data->tool_random_set, + s->data->weapon_random_set(this->difficulty), + s->data->tekker_adjustment_set, + s->data->item_parameter_table(logic_version), + s->data->item_stack_limits(logic_version), (this->mode == GameMode::SOLO) ? GameMode::NORMAL : this->mode, this->difficulty, effective_section_id, @@ -277,7 +277,7 @@ void Lobby::load_maps() { } else { this->log.info_f("Loading free play supermaps"); auto s = this->require_server_state(); - auto supermaps = s->supermaps_for_variations(this->episode, this->mode, this->difficulty, this->variations); + auto supermaps = s->data->supermaps_for_variations(this->episode, this->mode, this->difficulty, this->variations); this->map_state = std::make_shared( this->lobby_id, this->difficulty, this->event, this->random_seed, this->rare_enemy_rates, this->rand_crypt, supermaps); } @@ -307,13 +307,13 @@ void Lobby::create_ep3_server() { 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, - .behavior_flags = s->ep3_behavior_flags, + .card_index = is_nte ? s->data->ep3_card_index_trial : s->data->ep3_card_index, + .map_index = s->data->ep3_map_index, + .behavior_flags = s->data->ep3_behavior_flags, .opt_rand_stream = nullptr, .rand_crypt = this->rand_crypt, .tournament = tourn, - .trap_card_ids = s->ep3_trap_card_ids, + .trap_card_ids = s->data->ep3_trap_card_ids, .output_queue = nullptr, }; if (is_nte) { diff --git a/src/Lobby.hh b/src/Lobby.hh index b396d724..3c156f98 100644 --- a/src/Lobby.hh +++ b/src/Lobby.hh @@ -20,7 +20,7 @@ #include "StaticGameData.hh" #include "Text.hh" -struct ServerState; +class ServerState; struct Lobby : public std::enable_shared_from_this { struct FloorItem { diff --git a/src/Main.cc b/src/Main.cc index f32b91dc..a7336412 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -2075,10 +2075,10 @@ Action a_print_word_select_table( given, prints the table sorted by token ID for that version. If no version\n\ option is given, prints the token table sorted by canonical name.\n", +[](phosg::Arguments& args) { - auto s = std::make_shared(get_config_filename(args)); - s->load_patch_indexes(); - s->load_text_index(); - s->load_word_select_table(); + auto di = std::make_shared(get_config_filename(args)); + di->load_patch_indexes(); + di->load_text_index(); + di->load_word_select_table(); Version v; try { v = get_cli_version(args); @@ -2086,9 +2086,9 @@ Action a_print_word_select_table( v = Version::UNKNOWN; } if (v != Version::UNKNOWN) { - s->word_select_table->print_index(stdout, v); + di->word_select_table->print_index(stdout, v); } else { - s->word_select_table->print(stdout); + di->word_select_table->print(stdout); } }); @@ -2209,20 +2209,20 @@ Action a_convert_rare_item_set( drop-anything rate; the true drop rates are shown in tooltips.\n", +[](phosg::Arguments& args) { double rate_factor = args.get("multiply", 1.0); - auto s = std::make_shared(get_config_filename(args)); - s->load_config_early(); - s->load_patch_indexes(); - s->load_text_index(); - s->load_item_definitions(); - s->load_item_name_indexes(); - s->load_drop_tables(); + auto di = std::make_shared(get_config_filename(args)); + di->load_config_early(); + di->load_patch_indexes(); + di->load_text_index(); + di->load_item_definitions(); + di->load_item_name_indexes(); + di->load_drop_tables(); std::string input_filename = args.get(1, false); if (input_filename.empty() || (input_filename == "-")) { throw std::runtime_error("input filename must be given"); } auto rs = load_rare_item_set( - input_filename, is_v1(get_cli_version(args, Version::BB_V4)), s->item_name_index(Version::BB_V4)); + input_filename, is_v1(get_cli_version(args, Version::BB_V4)), di->item_name_index(Version::BB_V4)); if (rate_factor != 1.0) { rs->multiply_all_rates(rate_factor); } @@ -2230,9 +2230,9 @@ Action a_convert_rare_item_set( std::string output_filename = args.get(2, false); std::string output_filename_lower = phosg::tolower(output_filename); if (output_filename.empty() || (output_filename == "-")) { - rs->print_all_collections(stdout, s->item_name_index_opt(get_cli_version(args, Version::BB_V4))); + rs->print_all_collections(stdout, di->item_name_index_opt(get_cli_version(args, Version::BB_V4))); } else if (output_filename_lower.ends_with(".json")) { - auto json = rs->json(s->item_name_index_opt(get_cli_version(args, Version::BB_V4))); + auto json = rs->json(di->item_name_index_opt(get_cli_version(args, Version::BB_V4))); std::string data = json.serialize(phosg::JSON::SerializeOption::FORMAT | phosg::JSON::SerializeOption::HEX_INTEGERS | phosg::JSON::SerializeOption::SORT_DICT_KEYS); write_output_data(args, data, nullptr); } else if (output_filename_lower.ends_with(".gsl")) { @@ -2251,8 +2251,8 @@ Action a_convert_rare_item_set( if ((is_v1 && (difficulty == Difficulty::ULTIMATE)) || (!rs->has_entries_for_game_config(mode, episode, difficulty))) { continue; } - auto item_name_index = s->item_name_index(cli_version); - std::string data = rs->serialize_html(mode, episode, difficulty, item_name_index, s->common_item_set(cli_version, nullptr)); + auto item_name_index = di->item_name_index(cli_version); + std::string data = rs->serialize_html(mode, episode, difficulty, item_name_index, di->common_item_set(cli_version, nullptr)); std::string out_filename = output_filename.substr(0, output_filename.size() - 5) + "." + name_for_mode(mode) + "." + abbreviation_for_episode(episode) + "." + abbreviation_for_difficulty(difficulty) + output_filename.substr(output_filename.size() - 5); phosg::save_file(out_filename, data); phosg::log_info_f("... {}", out_filename); @@ -2275,17 +2275,17 @@ Action a_compare_rare_item_set( throw std::runtime_error("two input filenames must be given"); } - auto s = std::make_shared(get_config_filename(args)); - s->load_config_early(); - s->load_patch_indexes(); - s->load_text_index(); - s->load_item_definitions(); - s->load_item_name_indexes(); - s->load_drop_tables(); + auto di = std::make_shared(get_config_filename(args)); + di->load_config_early(); + di->load_patch_indexes(); + di->load_text_index(); + di->load_item_definitions(); + di->load_item_name_indexes(); + di->load_drop_tables(); bool is_v1 = ::is_v1(get_cli_version(args, Version::BB_V4)); - auto rs1 = load_rare_item_set(input_filename1, is_v1, s->item_name_index(Version::BB_V4)); - auto rs2 = load_rare_item_set(input_filename2, is_v1, s->item_name_index(Version::BB_V4)); + auto rs1 = load_rare_item_set(input_filename1, is_v1, di->item_name_index(Version::BB_V4)); + auto rs2 = load_rare_item_set(input_filename2, is_v1, di->item_name_index(Version::BB_V4)); rs1->print_diff(stdout, *rs2); }); @@ -2622,13 +2622,13 @@ Action a_describe_item( std::string description = args.get(1); auto version = get_cli_version(args); - auto s = std::make_shared(get_config_filename(args)); - s->load_config_early(); - s->load_patch_indexes(); - s->load_text_index(); - s->load_item_definitions(); - s->load_item_name_indexes(); - auto name_index = s->item_name_index(version); + auto di = std::make_shared(get_config_filename(args)); + di->load_config_early(); + di->load_patch_indexes(); + di->load_text_index(); + di->load_item_definitions(); + di->load_item_name_indexes(); + auto name_index = di->item_name_index(version); ItemData item = name_index->parse_item_description(description); @@ -2646,7 +2646,7 @@ Action a_describe_item( item.data2[0], item.data2[1], item.data2[2], item.data2[3]); ItemData item_v2 = item; - item_v2.encode_for_version(Version::PC_V2, s->item_parameter_table_for_encode(Version::PC_V2)); + item_v2.encode_for_version(Version::PC_V2, di->item_parameter_table_for_encode(Version::PC_V2)); ItemData item_v2_decoded = item_v2; item_v2_decoded.decode_for_version(Version::PC_V2); @@ -2665,7 +2665,7 @@ Action a_describe_item( } ItemData item_gc = item; - item_gc.encode_for_version(Version::GC_V3, s->item_parameter_table_for_encode(Version::GC_V3)); + item_gc.encode_for_version(Version::GC_V3, di->item_parameter_table_for_encode(Version::GC_V3)); ItemData item_gc_decoded = item_gc; item_gc_decoded.decode_for_version(Version::GC_V3); @@ -2686,24 +2686,24 @@ Action a_describe_item( phosg::log_info_f("Description: {}", desc); phosg::log_info_f("Description (in-game): {}", desc_colored); - size_t purchase_price = s->item_parameter_table(Version::BB_V4)->price_for_item(item); + size_t purchase_price = di->item_parameter_table(Version::BB_V4)->price_for_item(item); size_t sale_price = purchase_price >> 3; phosg::log_info_f("Purchase price: {}; sale price: {}", purchase_price, sale_price); }); Action a_name_all_items( "name-all-items", nullptr, +[](phosg::Arguments& args) { - auto s = std::make_shared(get_config_filename(args)); - s->load_config_early(); - s->load_patch_indexes(); - s->load_text_index(); - s->load_item_definitions(); - s->load_item_name_indexes(); - s->load_ep3_cards(); - s->load_config_late(); + auto di = std::make_shared(get_config_filename(args)); + di->load_config_early(); + di->load_patch_indexes(); + di->load_text_index(); + di->load_item_definitions(); + di->load_item_name_indexes(); + di->load_ep3_cards(); + di->load_config_late(); std::set all_primary_identifiers; - for (const auto& index : s->item_name_indexes) { + for (const auto& index : di->item_name_indexes) { if (index) { for (const auto& it : index->all_by_primary_identifier()) { all_primary_identifiers.emplace(it.first); @@ -2715,10 +2715,10 @@ Action a_name_all_items( for (uint32_t primary_identifier : all_primary_identifiers) { phosg::fwrite_fmt(stdout, "{:08X}\n", primary_identifier); for (Version v : ALL_VERSIONS) { - const auto& index = s->item_name_index_opt(v); + const auto& index = di->item_name_index_opt(v); if (index) { - auto pmt = s->item_parameter_table(v); - ItemData item = ItemData::from_primary_identifier(*s->item_stack_limits(v), primary_identifier); + auto pmt = di->item_parameter_table(v); + ItemData item = ItemData::from_primary_identifier(*di->item_stack_limits(v), primary_identifier); std::string name = index->describe_item(item); try { bool is_rare = pmt->is_item_rare(item); @@ -2736,7 +2736,7 @@ Action a_name_all_items( auto print_header = [&]() -> void { phosg::fwrite_fmt(stdout, "IDENT :"); for (Version v : ALL_VERSIONS) { - const auto& index = s->item_name_index_opt(v); + const auto& index = di->item_name_index_opt(v); if (index) { phosg::fwrite_fmt(stdout, " {:30} ", phosg::name_for_enum(v)); } @@ -2755,10 +2755,10 @@ Action a_name_all_items( phosg::fwrite_fmt(stdout, "{:08X}:", primary_identifier); for (Version v : ALL_VERSIONS) { - const auto& index = s->item_name_index_opt(v); + const auto& index = di->item_name_index_opt(v); if (index) { - auto pmt = s->item_parameter_table(v); - ItemData item = ItemData::from_primary_identifier(*s->item_stack_limits(v), primary_identifier); + auto pmt = di->item_parameter_table(v); + ItemData item = ItemData::from_primary_identifier(*di->item_stack_limits(v), primary_identifier); if (index->exists(item)) { std::string name = index->describe_item(item); bool is_rare = pmt->is_item_rare(item); @@ -2778,10 +2778,10 @@ Action a_print_level_stats( show-level-tables\n\ Print the level tables for each version in a semi-human-readable format.\n", +[](phosg::Arguments& args) { - auto s = std::make_shared(get_config_filename(args)); - s->load_config_early(); - s->load_patch_indexes(); - s->load_level_tables(); + auto di = std::make_shared(get_config_filename(args)); + di->load_config_early(); + di->load_patch_indexes(); + di->load_level_tables(); std::vector level_1_v1_v2; std::vector level_100_v1_v2; @@ -2795,19 +2795,19 @@ Action a_print_level_stats( std::vector level_200_limit_v4; for (size_t z = 0; z < 12; z++) { if (z < 9) { - level_1_v1_v2.emplace_back().char_stats = s->level_table_v1_v2->base_stats_for_class(z); - level_200_limit_v1_v2.emplace_back(s->level_table_v1_v2->max_stats_for_class(z)); - s->level_table_v1_v2->advance_to_level(level_100_v1_v2.emplace_back(level_1_v1_v2.back()), 99, z); - s->level_table_v1_v2->advance_to_level(level_200_v1_v2.emplace_back(level_1_v1_v2.back()), 199, z); + level_1_v1_v2.emplace_back().char_stats = di->level_table_v1_v2->base_stats_for_class(z); + level_200_limit_v1_v2.emplace_back(di->level_table_v1_v2->max_stats_for_class(z)); + di->level_table_v1_v2->advance_to_level(level_100_v1_v2.emplace_back(level_1_v1_v2.back()), 99, z); + di->level_table_v1_v2->advance_to_level(level_200_v1_v2.emplace_back(level_1_v1_v2.back()), 199, z); } - level_1_v3.emplace_back().char_stats = s->level_table_v3->base_stats_for_class(z); - s->level_table_v3->advance_to_level(level_200_v3.emplace_back(level_1_v3.back()), 199, z); - level_200_limit_v3.emplace_back(s->level_table_v3->max_stats_for_class(z)); + level_1_v3.emplace_back().char_stats = di->level_table_v3->base_stats_for_class(z); + di->level_table_v3->advance_to_level(level_200_v3.emplace_back(level_1_v3.back()), 199, z); + level_200_limit_v3.emplace_back(di->level_table_v3->max_stats_for_class(z)); - level_1_v4.emplace_back().char_stats = s->level_table_v4->base_stats_for_class(z); - s->level_table_v4->advance_to_level(level_200_v4.emplace_back(level_1_v3.back()), 199, z); - level_200_limit_v4.emplace_back(s->level_table_v4->max_stats_for_class(z)); + level_1_v4.emplace_back().char_stats = di->level_table_v4->base_stats_for_class(z); + di->level_table_v4->advance_to_level(level_200_v4.emplace_back(level_1_v3.back()), 199, z); + level_200_limit_v4.emplace_back(di->level_table_v4->max_stats_for_class(z)); } auto print_stats_set = [](const std::vector& stats_vec, const char* name) -> void { @@ -2865,10 +2865,10 @@ Action a_show_item_parameter_tables( Print the item parameter tables for each version in a semi-human-readable\n\ format.\n", +[](phosg::Arguments& args) { - auto s = std::make_shared(get_config_filename(args)); - s->load_all(false); + auto di = std::make_shared(get_config_filename(args)); + di->load_all(); for (Version v : ALL_VERSIONS) { - const auto& index = s->item_name_index_opt(v); + const auto& index = di->item_name_index_opt(v); if (index) { phosg::fwrite_fmt(stdout, "======== {}\n", phosg::name_for_enum(v)); index->print_table(stdout); @@ -2882,19 +2882,19 @@ Action a_show_shop_random_sets( Print the tekker and shop generation tables in a semi-human-readable\n\ format.\n", +[](phosg::Arguments& args) { - auto s = std::make_shared(get_config_filename(args)); - s->load_all(false); - s->tekker_adjustment_set->print(stdout); - s->armor_random_set->print(stdout); - s->tool_random_set->print(stdout); + auto di = std::make_shared(get_config_filename(args)); + di->load_all(); + di->tekker_adjustment_set->print(stdout); + di->armor_random_set->print(stdout); + di->tool_random_set->print(stdout); phosg::fwrite_fmt(stdout, "(Normal) "); - s->weapon_random_set(Difficulty::NORMAL)->print(stdout); + di->weapon_random_set(Difficulty::NORMAL)->print(stdout); phosg::fwrite_fmt(stdout, "(Hard) "); - s->weapon_random_set(Difficulty::HARD)->print(stdout); + di->weapon_random_set(Difficulty::HARD)->print(stdout); phosg::fwrite_fmt(stdout, "(Very Hard) "); - s->weapon_random_set(Difficulty::VERY_HARD)->print(stdout); + di->weapon_random_set(Difficulty::VERY_HARD)->print(stdout); phosg::fwrite_fmt(stdout, "(Ultimate) "); - s->weapon_random_set(Difficulty::ULTIMATE)->print(stdout); + di->weapon_random_set(Difficulty::ULTIMATE)->print(stdout); }); Action a_show_ep3_cards( @@ -2905,8 +2905,8 @@ Action a_show_ep3_cards( +[](phosg::Arguments& args) { bool one_line = args.get("one-line"); - auto s = std::make_shared(get_config_filename(args)); - s->load_ep3_cards(); + auto di = std::make_shared(get_config_filename(args)); + di->load_ep3_cards(); std::unique_ptr text_english; try { @@ -2915,10 +2915,10 @@ Action a_show_ep3_cards( } catch (const std::exception& e) { } - auto card_ids = s->ep3_card_index->all_ids(); + auto card_ids = di->ep3_card_index->all_ids(); phosg::log_info_f("{} card definitions", card_ids.size()); for (uint32_t card_id : card_ids) { - auto entry = s->ep3_card_index->definition_for_id(card_id); + auto entry = di->ep3_card_index->definition_for_id(card_id); phosg::fwrite_fmt(stdout, "{}\n", entry->def.str(one_line, text_english.get())); if (!one_line) { if (!entry->debug_tags.empty()) { @@ -2957,14 +2957,14 @@ Action a_generate_ep3_cards_html( bool no_large_images = args.get("no-large-images"); bool no_disassembly = args.get("no-disassembly"); - auto s = std::make_shared(get_config_filename(args)); - s->load_patch_indexes(); - s->load_text_index(); - s->load_ep3_cards(); + auto di = std::make_shared(get_config_filename(args)); + di->load_patch_indexes(); + di->load_text_index(); + di->load_ep3_cards(); std::shared_ptr text_english; try { - text_english = s->text_index->get(Version::GC_EP3, Language::ENGLISH); + text_english = di->text_index->get(Version::GC_EP3, Language::ENGLISH); } catch (const std::out_of_range&) { } @@ -3063,10 +3063,10 @@ Action a_generate_ep3_cards_html( std::vector version_infos; if (include_nte) { - version_infos.emplace_back("NTE", s->ep3_card_index_trial, no_images ? nullptr : "system/ep3/cardtex-trial", no_large_images, num_threads, no_disassembly); + version_infos.emplace_back("NTE", di->ep3_card_index_trial, no_images ? nullptr : "system/ep3/cardtex-trial", no_large_images, num_threads, no_disassembly); } if (include_final) { - version_infos.emplace_back("Final", s->ep3_card_index, no_images ? nullptr : "system/ep3/cardtex", no_large_images, num_threads, no_disassembly); + version_infos.emplace_back("Final", di->ep3_card_index, no_images ? nullptr : "system/ep3/cardtex", no_large_images, num_threads, no_disassembly); } std::deque blocks; @@ -3199,11 +3199,11 @@ Action a_show_ep3_maps( +[](phosg::Arguments& args) { config_log.info_f("Collecting Episode 3 data"); - auto s = std::make_shared(get_config_filename(args)); - s->load_ep3_cards(); - s->load_ep3_maps(); + auto di = std::make_shared(get_config_filename(args)); + di->load_ep3_cards(); + di->load_ep3_maps(); - const auto& all_maps = s->ep3_map_index->all_maps(); + const auto& all_maps = di->ep3_map_index->all_maps(); phosg::log_info_f("{} maps", all_maps.size()); for (const auto& [map_number, map] : all_maps) { const auto& vms = map->all_versions(); @@ -3212,7 +3212,7 @@ Action a_show_ep3_maps( continue; } Language language = static_cast(lang_index); - std::string map_s = vms[lang_index]->map->str(s->ep3_card_index.get(), language); + std::string map_s = vms[lang_index]->map->str(di->ep3_card_index.get(), language); phosg::fwrite_fmt(stdout, "({}) {}\n", char_for_language(language), map_s); } } @@ -3224,22 +3224,22 @@ Action a_show_battle_params( Print the Blue Burst battle parameters from the system/blueburst directory\n\ in a human-readable format.\n", +[](phosg::Arguments& args) { - auto s = std::make_shared(get_config_filename(args)); - s->load_patch_indexes(); - s->load_battle_params(); + auto di = std::make_shared(get_config_filename(args)); + di->load_patch_indexes(); + di->load_battle_params(); phosg::fwrite_fmt(stdout, "Episode 1 multi\n"); - s->battle_params->get_table(false, Episode::EP1).print(stdout, Episode::EP1); + di->battle_params->get_table(false, Episode::EP1).print(stdout, Episode::EP1); phosg::fwrite_fmt(stdout, "Episode 1 solo\n"); - s->battle_params->get_table(true, Episode::EP1).print(stdout, Episode::EP1); + di->battle_params->get_table(true, Episode::EP1).print(stdout, Episode::EP1); phosg::fwrite_fmt(stdout, "Episode 2 multi\n"); - s->battle_params->get_table(false, Episode::EP2).print(stdout, Episode::EP2); + di->battle_params->get_table(false, Episode::EP2).print(stdout, Episode::EP2); phosg::fwrite_fmt(stdout, "Episode 2 solo\n"); - s->battle_params->get_table(true, Episode::EP2).print(stdout, Episode::EP2); + di->battle_params->get_table(true, Episode::EP2).print(stdout, Episode::EP2); phosg::fwrite_fmt(stdout, "Episode 4 multi\n"); - s->battle_params->get_table(false, Episode::EP4).print(stdout, Episode::EP4); + di->battle_params->get_table(false, Episode::EP4).print(stdout, Episode::EP4); phosg::fwrite_fmt(stdout, "Episode 4 solo\n"); - s->battle_params->get_table(true, Episode::EP4).print(stdout, Episode::EP4); + di->battle_params->get_table(true, Episode::EP4).print(stdout, Episode::EP4); }); Action a_check_supermaps( @@ -3253,11 +3253,11 @@ Action a_check_supermaps( bool save_disassembly = args.get("disassemble"); bool generate_enemy_stats = args.get("generate-enemy-stats"); - auto s = std::make_shared(get_config_filename(args)); - s->load_config_early(); - s->load_patch_indexes(); - s->load_set_data_tables(); - s->load_maps(); + auto di = std::make_shared(get_config_filename(args)); + di->load_config_early(); + di->load_patch_indexes(); + di->load_set_data_tables(); + di->load_maps(); auto rand_crypt = std::make_shared(phosg::random_object()); @@ -3273,9 +3273,9 @@ Action a_check_supermaps( abbreviation_for_mode(mode), abbreviation_for_difficulty(difficulty)); - auto sdt = s->set_data_table(Version::BB_V4, episode, mode, difficulty); + auto sdt = di->set_data_table(Version::BB_V4, episode, mode, difficulty); auto variations = sdt->generate_variations(episode, (mode == GameMode::SOLO), rand_crypt); - auto supermaps = s->supermaps_for_variations(episode, mode, difficulty, variations); + auto supermaps = di->supermaps_for_variations(episode, mode, difficulty, variations); auto map_state = std::make_shared( 0, difficulty, event, random_seed, MapState::DEFAULT_RARE_ENEMIES, rand_crypt, supermaps); map_state->verify(); @@ -3288,7 +3288,7 @@ Action a_check_supermaps( } SuperMap::EfficiencyStats all_free_maps_eff; - for (const auto& [key, supermap] : s->supermap_for_free_play_key) { + for (const auto& [key, supermap] : di->supermap_for_free_play_key) { auto episode = static_cast((key >> 28) & 7); auto mode = static_cast((key >> 26) & 3); Difficulty difficulty = static_cast((key >> 24) & 3); @@ -3332,11 +3332,11 @@ Action a_check_supermaps( phosg::fwrite_fmt(stderr, "ALL FREE MAPS: {}\n", all_free_maps_eff.str()); - s->load_quest_index(); + di->load_quest_index(); SuperMap::EfficiencyStats all_quests_eff; uint32_t random_seed = args.get("random-seed", 0, phosg::Arguments::IntFormat::HEX); - for (const auto& it : s->quest_index->quests_by_number) { + for (const auto& it : di->quest_index->quests_by_number) { auto supermap = it.second->get_supermap(random_seed); if (!supermap) { throw std::logic_error("quest does not have a supermap, even with a specified random seed"); @@ -3648,11 +3648,11 @@ Action a_print_free_supermap( } } - auto s = std::make_shared(get_config_filename(args)); - s->load_config_early(); - s->load_patch_indexes(); - s->load_set_data_tables(); - s->load_maps(); + auto di = std::make_shared(get_config_filename(args)); + di->load_config_early(); + di->load_patch_indexes(); + di->load_set_data_tables(); + di->load_maps(); std::shared_ptr rand_crypt; if (args.get("--psov2")) { @@ -3660,8 +3660,8 @@ Action a_print_free_supermap( } else { rand_crypt = std::make_shared(random_seed); } - auto sdt = s->set_data_table(get_cli_version(args, Version::BB_V4), episode, mode, difficulty); - auto supermaps = s->supermaps_for_variations(episode, mode, difficulty, variations); + auto sdt = di->set_data_table(get_cli_version(args, Version::BB_V4), episode, mode, difficulty); + auto supermaps = di->supermaps_for_variations(episode, mode, difficulty, variations); MapState map_state(0, difficulty, event, random_seed, MapState::DEFAULT_RARE_ENEMIES, rand_crypt, supermaps); map_state.verify(); map_state.print(stdout); @@ -3677,13 +3677,13 @@ Action a_check_quests( check_quest_opcode_definitions(); phosg::log_info_f("Opcode definitions OK"); - auto s = std::make_shared(get_config_filename(args)); - s->is_debug = true; - s->load_config_early(); - s->load_patch_indexes(); - s->load_set_data_tables(); - s->load_maps(); - s->load_quest_index(true); + auto di = std::make_shared(get_config_filename(args)); + di->is_debug = true; + di->load_config_early(); + di->load_patch_indexes(); + di->load_set_data_tables(); + di->load_maps(); + di->load_quest_index(true); uint64_t script_time = 0, map_time = 0; if (reassemble_scripts || reassemble_maps) { @@ -3797,7 +3797,7 @@ Action a_check_quests( }; if (num_threads == 1) { - for (const auto& [_, q] : s->quest_index->quests_by_number) { + for (const auto& [_, q] : di->quest_index->quests_by_number) { for (const auto& [_, vq] : q->versions) { check_vq(vq, 0); } @@ -3805,7 +3805,7 @@ Action a_check_quests( } else { std::vector> all_vqs; - for (const auto& [_, q] : s->quest_index->quests_by_number) { + for (const auto& [_, q] : di->quest_index->quests_by_number) { for (const auto& [_, vq] : q->versions) { all_vqs.emplace_back(vq); } @@ -3832,9 +3832,9 @@ Action a_check_ep3_maps( "check-ep3-maps", nullptr, +[](phosg::Arguments& args) { config_log.info_f("Collecting Episode 3 data"); - auto s = std::make_shared(get_config_filename(args)); - s->is_debug = true; - s->load_ep3_maps(true); + auto di = std::make_shared(get_config_filename(args)); + di->is_debug = true; + di->load_ep3_maps(true); }); Action a_check_client_functions( @@ -4029,9 +4029,9 @@ Action a_format_ep3_battle_record( Action a_replay_ep3_battle_commands( "replay-ep3-battle-commands", nullptr, +[](phosg::Arguments& args) { - auto s = std::make_shared(get_config_filename(args)); - s->load_ep3_cards(); - s->load_ep3_maps(); + auto di = std::make_shared(get_config_filename(args)); + di->load_ep3_cards(); + di->load_ep3_maps(); int64_t base_seed = args.get("seed", -1); bool is_trial = (get_cli_version(args, Version::GC_EP3) == Version::GC_EP3_NTE); @@ -4047,8 +4047,8 @@ Action a_replay_ep3_battle_commands( auto run_replay = [&](int64_t seed, size_t) { Episode3::Server::Options options = { - .card_index = s->ep3_card_index, - .map_index = s->ep3_map_index, + .card_index = di->ep3_card_index, + .map_index = di->ep3_map_index, .behavior_flags = 0x0092, .opt_rand_stream = nullptr, .rand_crypt = std::make_shared(seed), @@ -4088,15 +4088,15 @@ Action a_replay_ep3_battle_record( bool use_color = isatty(fileno(stdout)); - auto s = std::make_shared(get_config_filename(args)); - s->load_ep3_cards(); - s->load_ep3_maps(); + auto di = std::make_shared(get_config_filename(args)); + di->load_ep3_cards(); + di->load_ep3_maps(); bool is_nte = rec->get_behavior_flags() & Episode3::BehaviorFlag::IS_TRIAL_EDITION; auto output_queue = std::make_shared>(); Episode3::Server::Options options = { - .card_index = s->ep3_card_index, - .map_index = s->ep3_map_index, + .card_index = di->ep3_card_index, + .map_index = di->ep3_map_index, .behavior_flags = rec->get_behavior_flags() & ~(Episode3::BehaviorFlag::LOG_COMMANDS_IF_LOBBY_MISSING), .opt_rand_stream = std::make_shared(rec->get_random_stream()), .rand_crypt = std::make_shared(), @@ -4220,7 +4220,20 @@ Action a_run_server_replay_log( std::filesystem::create_directories("system/players"); } - const auto& replay_log_filenames = args.get_multi("replay-log"); + const auto& args_replay_log_filenames = args.get_multi("replay-log"); + std::vector replay_log_filenames; + for (auto& log_filename : args_replay_log_filenames) { + if (std::filesystem::is_directory(log_filename)) { + for (const auto& item : std::filesystem::directory_iterator(log_filename)) { + std::string test_filename = item.path().filename().string(); + if (test_filename.ends_with(".test.txt")) { + replay_log_filenames.emplace_back(std::format("{}/{}", log_filename, test_filename)); + } + } + } else { + replay_log_filenames.emplace_back(std::move(log_filename)); + } + } #ifndef PHOSG_WINDOWS signal(SIGPIPE, SIG_IGN); @@ -4229,123 +4242,157 @@ Action a_run_server_replay_log( use_terminal_colors = true; } - auto state = std::make_shared(get_config_filename(args), !replay_log_filenames.empty()); + auto data_index = std::make_shared(get_config_filename(args)); if (args.get("debug")) { - state->is_debug = true; - } - state->load_all(true); - - if (state->dns_server_port) { - if (!state->dns_server_addr.empty()) { - config_log.info_f("Starting DNS server on {}:{}", state->dns_server_addr, state->dns_server_port); - } else { - config_log.info_f("Starting DNS server on port {}", state->dns_server_port); - } - state->dns_server = std::make_shared(state); - state->dns_server->listen(state->dns_server_addr, state->dns_server_port); - } else { - config_log.info_f("DNS server is disabled"); + data_index->is_debug = true; } + data_index->load_all(); + auto state = ServerState::create_shared(data_index, !replay_log_filenames.empty()); std::shared_ptr shell; std::shared_ptr signal_watcher; - std::shared_ptr last_running_replay; + std::map> replay_sessions; if (!replay_log_filenames.empty()) { - config_log.info_f("Starting game server"); + // TODO: Do this properly via a config option, you lazy bum + state->data->dol_file_index = std::make_shared(); state->game_server = std::make_shared(state); - // TODO: Do this properly via a config option, you lazy bum - state->dol_file_index = std::make_shared(); + if (args.get("parallel")) { + size_t completed_count = 0; + auto run_replay = [&](const std::string& log_filename) -> asio::awaitable { + auto replay_state = state->clone_shared(); + replay_state->game_server = std::make_shared(replay_state); - auto run_replays = [&]() -> asio::awaitable { - try { - for (const auto& log_filename : replay_log_filenames) { - phosg::log_info_f("[Replay] {} ...", log_filename); - auto log_f = phosg::fopen_shared(log_filename, "rt"); - last_running_replay = std::make_shared(state, log_f.get()); - co_await last_running_replay->run(); - if (last_running_replay->failed()) { - phosg::log_error_f("[Replay] {} failed", log_filename); - break; - } + phosg::log_info_f("[Replay] Loading {}", log_filename); + auto log_f = phosg::fopen_unique(log_filename, "rt"); + auto replay_session = std::make_shared(replay_state, log_f.get()); + replay_sessions.emplace(log_filename, replay_session); + + phosg::log_info_f("[Replay] {} ...", log_filename); + co_await replay_session->run(); + if (!replay_session->failure_str().empty()) { + phosg::log_error_f("[Replay] {} failed:\n{}", log_filename, replay_session->failure_str()); + } else { phosg::log_info_f("[Replay] {} OK", log_filename); - state->reset_between_replays(); } - phosg::log_info_f("[Replay] All replays complete"); - } catch (const std::exception& e) { - phosg::log_info_f("[Replay] Replays failed: {}", e.what()); + + completed_count++; + if (completed_count == replay_log_filenames.size()) { + phosg::log_info_f("[Replay] All replays complete; exiting"); + state->io_context->stop(); + } + }; + + for (const auto& log_filename : replay_log_filenames) { + asio::co_spawn(*state->io_context, run_replay(log_filename), asio::detached); } - if (!last_running_replay->failed()) { - last_running_replay.reset(); + + } else { + for (const auto& log_filename : replay_log_filenames) { + phosg::log_info_f("[Replay] Loading {}", log_filename); + auto log_f = phosg::fopen_unique(log_filename, "rt"); + replay_sessions.emplace(log_filename, std::make_shared(state, log_f.get())); } - state->io_context->stop(); - }; - asio::co_spawn(*state->io_context, run_replays, asio::detached); + + auto run_replays = [&]() -> asio::awaitable { + try { + for (const auto& [log_filename, replay_session] : replay_sessions) { + phosg::log_info_f("[Replay] {} ...", log_filename); + co_await replay_session->run(); + if (!replay_session->failure_str().empty()) { + phosg::log_error_f("[Replay] {} failed:\n{}", log_filename, replay_session->failure_str()); + break; + } + phosg::log_info_f("[Replay] {} OK", log_filename); + state->reset_between_replays(); + } + phosg::log_info_f("[Replay] All replays complete"); + } catch (const std::exception& e) { + phosg::log_info_f("[Replay] Replays failed: {}", e.what()); + } + state->io_context->stop(); + }; + asio::co_spawn(*state->io_context, run_replays, asio::detached); + } } else { + if (state->data->dns_server_port) { + if (!state->data->dns_server_addr.empty()) { + config_log.info_f("Starting DNS server on {}:{}", state->data->dns_server_addr, state->data->dns_server_port); + } else { + config_log.info_f("Starting DNS server on port {}", state->data->dns_server_port); + } + state->dns_server = std::make_shared(state); + state->dns_server->listen(state->data->dns_server_addr, state->data->dns_server_port); + } else { + config_log.info_f("DNS server is disabled"); + } + config_log.info_f("Opening sockets"); - for (const auto& [_, pc] : state->name_to_port_config) { + for (const auto& [_, pc] : state->data->name_to_port_config) { if (!state->game_server.get()) { config_log.info_f("Starting game server"); state->game_server = std::make_shared(state); } std::string spec = std::format("TG-{}-{}-{}-{}", - pc->port, phosg::name_for_enum(pc->version), pc->name, phosg::name_for_enum(pc->behavior)); - state->game_server->listen(spec, pc->addr, pc->port, pc->version, pc->behavior); + pc.port, phosg::name_for_enum(pc.version), pc.name, phosg::name_for_enum(pc.behavior)); + state->game_server->listen(spec, pc.addr, pc.port, pc.version, pc.behavior); } - if (!state->ip_stack_addresses.empty() || !state->ppp_stack_addresses.empty() || !state->ppp_raw_addresses.empty()) { + if (!state->data->ip_stack_addresses.empty() || + !state->data->ppp_stack_addresses.empty() || + !state->data->ppp_raw_addresses.empty()) { config_log.info_f("Starting IP/PPP stack simulator"); state->ip_stack_simulator = std::make_shared(state); - for (const auto& it : state->ip_stack_addresses) { + for (const auto& it : state->data->ip_stack_addresses) { auto netloc = phosg::parse_netloc(it); std::string spec = (netloc.second == 0) ? ("T-IPS-" + netloc.first) : std::format("T-IPS-{}", netloc.second); state->ip_stack_simulator->listen( spec, netloc.first, netloc.second, VirtualNetworkProtocol::ETHERNET_TAPSERVER); } - for (const auto& it : state->ppp_stack_addresses) { + for (const auto& it : state->data->ppp_stack_addresses) { auto netloc = phosg::parse_netloc(it); std::string spec = (netloc.second == 0) ? ("T-PPPST-" + netloc.first) : std::format("T-PPPST-{}", netloc.second); state->ip_stack_simulator->listen( spec, netloc.first, netloc.second, VirtualNetworkProtocol::HDLC_TAPSERVER); } - for (const auto& it : state->ppp_raw_addresses) { + for (const auto& it : state->data->ppp_raw_addresses) { auto netloc = phosg::parse_netloc(it); std::string spec = (netloc.second == 0) ? ("T-PPPSR-" + netloc.first) : std::format("T-PPPSR-{}", netloc.second); state->ip_stack_simulator->listen( spec, netloc.first, netloc.second, VirtualNetworkProtocol::HDLC_RAW); if (netloc.second) { - if (state->local_address == 0 && state->external_address == 0) { + if (state->data->local_address == 0 && state->data->external_address == 0) { config_log.info_f( "Cannot generate Devolution phone numbers for {} because LocalAddress and ExternalAddress are not specified in the configuration", spec); - } else if (state->local_address == 0) { + } else if (state->data->local_address == 0) { config_log.info_f( "Note: The Devolution phone number for {} is {} (external)", - spec, devolution_phone_number_for_netloc(state->external_address, netloc.second)); - } else if (state->external_address == 0) { + spec, devolution_phone_number_for_netloc(state->data->external_address, netloc.second)); + } else if (state->data->external_address == 0) { config_log.info_f( "Note: The Devolution phone number for {} is {} (local)", - spec, devolution_phone_number_for_netloc(state->local_address, netloc.second)); - } else if (state->local_address == state->external_address) { + spec, devolution_phone_number_for_netloc(state->data->local_address, netloc.second)); + } else if (state->data->local_address == state->data->external_address) { config_log.info_f( "Note: The Devolution phone number for {} is {} (local+external)", - spec, devolution_phone_number_for_netloc(state->local_address, netloc.second)); + spec, devolution_phone_number_for_netloc(state->data->local_address, netloc.second)); } else { config_log.info_f( "Note: The Devolution phone numbers for {} are {} (local) and {} (external)", spec, - devolution_phone_number_for_netloc(state->local_address, netloc.second), - devolution_phone_number_for_netloc(state->external_address, netloc.second)); + devolution_phone_number_for_netloc(state->data->local_address, netloc.second), + devolution_phone_number_for_netloc(state->data->external_address, netloc.second)); } } } } - if (!state->http_addresses.empty() || !state->http_addresses.empty()) { + if (!state->data->http_addresses.empty()) { config_log.info_f("Starting HTTP server"); state->http_server = std::make_shared(state); - for (const auto& it : state->http_addresses) { + for (const auto& it : state->data->http_addresses) { auto netloc = phosg::parse_netloc(it); state->http_server->listen(netloc.first, netloc.second); } @@ -4358,16 +4405,16 @@ Action a_run_server_replay_log( } #ifndef PHOSG_WINDOWS - if (!state->username.empty()) { - config_log.info_f("Switching to user {}", state->username); - drop_privileges(state->username); + if (!state->data->username.empty()) { + config_log.info_f("Switching to user {}", state->data->username); + drop_privileges(state->data->username); } #endif bool should_run_shell; - if (state->run_shell_behavior == ServerState::RunShellBehavior::DEFAULT) { + if (state->data->run_shell_behavior == DataIndex::RunShellBehavior::DEFAULT) { should_run_shell = isatty(fileno(stdin)); - } else if (state->run_shell_behavior == ServerState::RunShellBehavior::ALWAYS) { + } else if (state->data->run_shell_behavior == DataIndex::RunShellBehavior::ALWAYS) { should_run_shell = true; } else { should_run_shell = false; @@ -4384,8 +4431,19 @@ Action a_run_server_replay_log( state->io_context->run(); config_log.info_f("Normal shutdown"); - if (last_running_replay) { - throw std::runtime_error("Replay failed"); + if (!replay_sessions.empty()) { + size_t num_failed_replays = 0; + for (const auto& [log_filename, replay_session] : replay_sessions) { + if (!replay_session->failure_str().empty()) { + config_log.warning_f("Replay failed: {}", log_filename); + num_failed_replays++; + } + } + if (num_failed_replays) { + throw std::runtime_error(std::format("{}/{} replays failed", num_failed_replays, replay_sessions.size())); + } else { + config_log.info_f("All {} replays succeeded", replay_sessions.size()); + } } }); diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index 92f93b83..3540b691 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -980,7 +980,7 @@ static asio::awaitable SC_6x60_6xA2(std::shared_ptr c, Ch c->log.info_f("No item was created"); } else { auto s = c->require_server_state(); - std::string name = s->describe_item(c->version(), res.item); + std::string name = s->data->describe_item(c->version(), res.item); c->log.info_f("Entity {:04X} (area {:02X}) created item {}", cmd.entity_index, cmd.effective_area, name); res.item.id = c->proxy_session->next_item_id++; c->log.info_f("Creating item {:08X} at {:02X}:{:g},{:g} for all clients", @@ -1782,7 +1782,7 @@ static asio::awaitable S_64(std::shared_ptr c, Channel::M auto s = c->require_server_state(); c->proxy_session->set_drop_mode(s, c->version(), c->override_random_seed, c->proxy_session->drop_mode); if (!is_ep3(c->version()) && (c->proxy_session->lobby_mode != GameMode::CHALLENGE)) { - auto supermaps = s->supermaps_for_variations( + auto supermaps = s->data->supermaps_for_variations( c->proxy_session->lobby_episode, c->proxy_session->lobby_mode, c->proxy_session->lobby_difficulty, @@ -1976,8 +1976,8 @@ static asio::awaitable C_06(std::shared_ptr c, Channel::M } auto s = c->require_server_state(); - char command_sentinel = s->chat_command_sentinel - ? s->chat_command_sentinel + char command_sentinel = s->data->chat_command_sentinel + ? s->data->chat_command_sentinel : ((c->version() == Version::DC_11_2000) ? '@' : '$'); bool is_command = (text[0] == command_sentinel) || (text[0] == '\t' && text[1] != 'C' && text[2] == command_sentinel); diff --git a/src/ProxySession.cc b/src/ProxySession.cc index 4f179fa1..f4623c35 100644 --- a/src/ProxySession.cc +++ b/src/ProxySession.cc @@ -25,14 +25,14 @@ void ProxySession::set_drop_mode( if (this->drop_mode == ProxyDropMode::INTERCEPT) { auto rand_crypt = std::make_shared((override_random_seed >= 0) ? override_random_seed : this->lobby_random_seed); this->item_creator = std::make_shared( - s->common_item_set(version, nullptr), - s->rare_item_set(version, nullptr), - s->armor_random_set, - s->tool_random_set, - s->weapon_random_set(this->lobby_difficulty), - s->tekker_adjustment_set, - s->item_parameter_table(version), - s->item_stack_limits(version), + s->data->common_item_set(version, nullptr), + s->data->rare_item_set(version, nullptr), + s->data->armor_random_set, + s->data->tool_random_set, + s->data->weapon_random_set(this->lobby_difficulty), + s->data->tekker_adjustment_set, + s->data->item_parameter_table(version), + s->data->item_stack_limits(version), (this->lobby_mode == GameMode::SOLO) ? GameMode::NORMAL : this->lobby_mode, this->lobby_difficulty, this->lobby_section_id, diff --git a/src/ProxySession.hh b/src/ProxySession.hh index 0c65defd..9280e2dc 100644 --- a/src/ProxySession.hh +++ b/src/ProxySession.hh @@ -13,7 +13,7 @@ #include "Map.hh" #include "SaveFileFormats.hh" -struct ServerState; +class ServerState; struct ProxySession { static size_t num_proxy_sessions; diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 78603e2c..f903b9fb 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -31,15 +31,15 @@ const char* ADD_NEXT_CLIENT_DISCONNECT_HOOK_NAME = "add_next_game_client"; asio::awaitable on_connect(std::shared_ptr c) { auto s = c->require_server_state(); - if (s->default_switch_assist_enabled) { + if (s->data->default_switch_assist_enabled) { c->set_flag(Client::Flag::SWITCH_ASSIST_ENABLED); } switch (c->server_behavior) { case ServerBehavior::PC_CONSOLE_DETECT: { - uint16_t pc_port = s->name_to_port_config.at("pc")->port; - uint16_t console_port = s->name_to_port_config.at("gc-us3")->port; - send_pc_console_split_reconnect(c, s->connect_address_for_client(c), pc_port, console_port); + uint16_t pc_port = s->data->name_to_port_config.at("pc").port; + uint16_t console_port = s->data->name_to_port_config.at("gc-us3").port; + send_pc_console_split_reconnect(c, s->data->connect_address_for_client(c), pc_port, console_port); // TODO: There appears to be a bug that occurs rarely when a client connects to this port; sometimes it // disconnects before receiving the data it needs. My hypothesis is that there's either a bug in Channel where // the data isn't being sent before the RST, or there's a bug in AVE-TCP where it doesn't forward the last data @@ -83,7 +83,7 @@ asio::awaitable on_disconnect(std::shared_ptr c) { static void send_main_menu(std::shared_ptr c) { auto s = c->require_server_state(); - auto main_menu = std::make_shared(MenuID::MAIN, s->name); + auto main_menu = std::make_shared(MenuID::MAIN, s->data->name); main_menu->items.emplace_back( MainMenuItemID::GO_TO_LOBBY, "Go to lobby", [wc = std::weak_ptr(c)]() -> std::string { @@ -119,21 +119,21 @@ static void send_main_menu(std::shared_ptr c) { "View server\ninformation", MenuItem::Flag::INVISIBLE_ON_DC_PROTOS | MenuItem::Flag::REQUIRES_MESSAGE_BOXES); uint32_t proxy_destinations_menu_item_flags = - (s->proxy_destinations_dc.empty() ? MenuItem::Flag::INVISIBLE_ON_DC : 0) | - (s->proxy_destinations_pc.empty() ? MenuItem::Flag::INVISIBLE_ON_PC : 0) | - (s->proxy_destinations_gc.empty() ? MenuItem::Flag::INVISIBLE_ON_GC : 0) | - (s->proxy_destinations_xb.empty() ? MenuItem::Flag::INVISIBLE_ON_XB : 0) | + (s->data->proxy_destinations_dc.empty() ? MenuItem::Flag::INVISIBLE_ON_DC : 0) | + (s->data->proxy_destinations_pc.empty() ? MenuItem::Flag::INVISIBLE_ON_PC : 0) | + (s->data->proxy_destinations_gc.empty() ? MenuItem::Flag::INVISIBLE_ON_GC : 0) | + (s->data->proxy_destinations_xb.empty() ? MenuItem::Flag::INVISIBLE_ON_XB : 0) | MenuItem::Flag::INVISIBLE_ON_BB; main_menu->items.emplace_back(MainMenuItemID::PROXY_DESTINATIONS, "Proxy server", "Connect to another\nserver through the\nproxy", proxy_destinations_menu_item_flags); main_menu->items.emplace_back(MainMenuItemID::DOWNLOAD_QUESTS, "Download quests", "Download quests", MenuItem::Flag::INVISIBLE_ON_DC_PROTOS | MenuItem::Flag::INVISIBLE_ON_PC_NTE | MenuItem::Flag::INVISIBLE_ON_BB); - if (!s->client_functions->patch_menu_empty(c->specific_version)) { + if (!s->data->client_functions->patch_menu_empty(c->specific_version)) { main_menu->items.emplace_back(MainMenuItemID::PATCH_SWITCHES, "Patches", "Change game\nbehaviors", MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL_RUNS_CODE); } - if (!s->dol_file_index->empty()) { + if (!s->data->dol_file_index->empty()) { main_menu->items.emplace_back(MainMenuItemID::PROGRAMS, "Programs", "Run GameCube\nprograms", MenuItem::Flag::GC_ONLY | MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL_RUNS_CODE | MenuItem::Flag::REQUIRES_SAVE_DISABLED); } @@ -147,7 +147,7 @@ static void send_main_menu(std::shared_ptr c) { static void send_proxy_destinations_menu(std::shared_ptr c) { auto s = c->require_server_state(); - send_menu(c, s->proxy_destinations_menu(c->version())); + send_menu(c, s->data->proxy_destinations_menu(c->version())); } static std::shared_ptr proxy_options_menu_for_client(std::shared_ptr c) { @@ -199,7 +199,7 @@ static std::shared_ptr proxy_options_menu_for_client(std::shared_ptr add_flag_option(ProxyOptionsMenuItemID::SWITCH_ASSIST, Client::Flag::SWITCH_ASSIST_ENABLED, "Switch assist", "Automatically unlock\nmulti-player doors\nwhen you step on\nany of the door\'s\nswitches"); } - if ((s->cheat_mode_behavior != ServerState::BehaviorSwitch::OFF) || + if ((s->data->cheat_mode_behavior != DataIndex::BehaviorSwitch::OFF) || c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE)) { if (!is_ep3(c->version())) { add_flag_option(ProxyOptionsMenuItemID::INFINITE_HP, Client::Flag::INFINITE_HP_ENABLED, @@ -217,7 +217,7 @@ static std::shared_ptr proxy_options_menu_for_client(std::shared_ptr "Show whispers", "Show contents of\nwhisper messages\neven if they are not\nfor you"); } } - if (s->proxy_allow_save_files) { + if (s->data->proxy_allow_save_files) { add_flag_option(ProxyOptionsMenuItemID::SAVE_FILES, Client::Flag::PROXY_SAVE_FILES, "Save files", "Save local copies of\nfiles from the\nremote server\n(quests, etc.)"); } @@ -228,8 +228,8 @@ static asio::awaitable send_auto_patches_if_needed(std::shared_ptr auto s = c->require_server_state(); if (c->login->account->auto_patches_enabled.empty() && - ((c->version() != Version::BB_V4) || s->bb_required_patches.empty()) && - s->auto_patches.empty()) { + ((c->version() != Version::BB_V4) || s->data->bb_required_patches.empty()) && + s->data->auto_patches.empty()) { c->set_flag(Client::Flag::HAS_AUTO_PATCHES); } @@ -239,9 +239,9 @@ static asio::awaitable send_auto_patches_if_needed(std::shared_ptr std::unordered_set> functions_to_send; if (c->version() == Version::BB_V4) { - for (const auto& patch_name : s->bb_required_patches) { + for (const auto& patch_name : s->data->bb_required_patches) { try { - functions_to_send.emplace(s->client_functions->get(patch_name, c->specific_version)); + functions_to_send.emplace(s->data->client_functions->get(patch_name, c->specific_version)); } catch (const std::out_of_range&) { std::string message = std::format( "Your client is not compatible with a\nrequired patch on this server.\n\nClient version: {}\nPatch name: {}", str_for_specific_version(c->specific_version), patch_name); @@ -251,9 +251,9 @@ static asio::awaitable send_auto_patches_if_needed(std::shared_ptr } } } - for (const auto& patch_name : s->auto_patches) { + for (const auto& patch_name : s->data->auto_patches) { try { - functions_to_send.emplace(s->client_functions->get(patch_name, c->specific_version)); + functions_to_send.emplace(s->data->client_functions->get(patch_name, c->specific_version)); } catch (const std::out_of_range&) { c->log.warning_f("Server has auto patch {} enabled, but it is not available for specific_version {}", patch_name, str_for_specific_version(c->specific_version)); @@ -261,7 +261,7 @@ static asio::awaitable send_auto_patches_if_needed(std::shared_ptr } for (const auto& patch_name : c->login->account->auto_patches_enabled) { try { - functions_to_send.emplace(s->client_functions->get(patch_name, c->specific_version)); + functions_to_send.emplace(s->data->client_functions->get(patch_name, c->specific_version)); } catch (const std::out_of_range&) { c->log.warning_f("Client has auto patch {} enabled, but it is not available for specific_version {}", patch_name, str_for_specific_version(c->specific_version)); @@ -280,8 +280,8 @@ asio::awaitable start_login_server_procedure(std::shared_ptr c) { s->remove_client_from_lobby(c); } - if (s->pre_lobby_event && (!is_ep3(c->version()) || s->ep3_menu_song < 0)) { - send_change_event(c, s->pre_lobby_event); + if (s->data->pre_lobby_event && (!is_ep3(c->version()) || s->data->ep3_menu_song < 0)) { + send_change_event(c, s->data->pre_lobby_event); } send_server_time(c); @@ -303,7 +303,7 @@ asio::awaitable start_login_server_procedure(std::shared_ptr c) { c->set_flag(Client::Flag::HAS_EP3_CARD_DEFS); } if ((c->version() != Version::GC_EP3_NTE) && !c->check_flag(Client::Flag::HAS_EP3_MEDIA_UPDATES)) { - for (const auto& banner : s->ep3_lobby_banners) { + for (const auto& banner : s->data->ep3_lobby_banners) { send_ep3_media_update(c, banner.type, banner.which, banner.data); c->set_flag(Client::Flag::HAS_EP3_MEDIA_UPDATES); } @@ -317,13 +317,13 @@ asio::awaitable start_login_server_procedure(std::shared_ptr c) { c->set_flag(Client::Flag::LOADING); c->log.info_f("LOADING flag set"); } - } else if (s->welcome_message.empty() || + } else if (s->data->welcome_message.empty() || c->check_flag(Client::Flag::NO_D6) || !c->check_flag(Client::Flag::AT_WELCOME_MESSAGE)) { c->clear_flag(Client::Flag::AT_WELCOME_MESSAGE); send_main_menu(c); } else { - send_message_box(c, s->welcome_message); + send_message_box(c, s->data->welcome_message); } co_return; } @@ -342,7 +342,7 @@ static asio::awaitable on_login_complete(std::shared_ptr c) { !c->check_flag(Client::Flag::SEND_FUNCTION_CALL_ACTUALLY_RUNS_CODE))) { std::shared_ptr q; try { - q = s->quest_index->get(s->enable_send_function_call_quest_numbers.at(c->specific_version)); + q = s->data->quest_index->get(s->data->enable_send_function_call_quest_numbers.at(c->specific_version)); } catch (const std::out_of_range&) { } if (!q) { @@ -430,7 +430,7 @@ asio::awaitable start_proxy_session(std::shared_ptr c, const std:: if (!c->can_use_chat_commands()) { c->clear_flag(Client::Flag::PROXY_CHAT_COMMANDS_ENABLED); } - if (!s->proxy_allow_save_files) { + if (!s->data->proxy_allow_save_files) { c->clear_flag(Client::Flag::PROXY_SAVE_FILES); } @@ -479,7 +479,7 @@ asio::awaitable start_proxy_session(std::shared_ptr c, const std:: phosg::TerminalFormat::FG_YELLOW, phosg::TerminalFormat::FG_RED, false, - s->censor_credentials); + s->data->censor_credentials); c->proxy_session = std::make_shared(channel, pc); if (c->version() == Version::GC_EP3) { @@ -538,11 +538,11 @@ asio::awaitable end_proxy_session(std::shared_ptr c, const std::st } if (is_in_game) { - std::string msg = std::format("You cannot return\nto $C6{}$C7\nwhile in a game.\n\n{}", s->name, error_message); + std::string msg = std::format("You cannot return\nto $C6{}$C7\nwhile in a game.\n\n{}", s->data->name, error_message); send_ship_info(c, msg); c->channel->disconnect(); } else { - std::string msg = std::format("You\'ve returned to\n$C6{}$C7\n\n{}", s->name, error_message); + std::string msg = std::format("You\'ve returned to\n$C6{}$C7\n\n{}", s->data->name, error_message); send_ship_info(c, msg); co_await start_login_server_procedure(c); } @@ -575,11 +575,11 @@ static asio::awaitable on_04_U(std::shared_ptr c, Channel::Message } catch (const AccountIndex::incorrect_password& e) { result_code = 0x03; } catch (const AccountIndex::missing_account& e) { - if (!s->allow_unregistered_users) { + if (!s->data->allow_unregistered_users) { result_code = 0x08; } } - } else if (!c->username.empty() && !s->allow_unregistered_users) { + } else if (!c->username.empty() && !s->data->allow_unregistered_users) { try { s->account_index->from_bb_credentials(c->username, nullptr, false); } catch (const AccountIndex::missing_account& e) { @@ -593,19 +593,19 @@ static asio::awaitable on_04_U(std::shared_ptr c, Channel::Message } // Switch to proxy session if there's a destination configured - if (is_patch(c->version()) && s->proxy_destination_patch.has_value()) { - const auto& [host, port] = *s->proxy_destination_patch; + if (is_patch(c->version()) && s->data->proxy_destination_patch.has_value()) { + const auto& [host, port] = *s->data->proxy_destination_patch; co_await start_proxy_session(c, host, port, false); } else { // No proxy destination; continue with normal patch logic bool is_bb = (c->version() == Version::BB_PATCH); - const std::string& message = is_bb ? s->bb_patch_server_message : s->pc_patch_server_message; + const std::string& message = is_bb ? s->data->bb_patch_server_message : s->data->pc_patch_server_message; if (!message.empty()) { send_message_box(c, message); } - auto index = is_bb ? s->bb_patch_file_index : s->pc_patch_file_index; + auto index = is_bb ? s->data->bb_patch_file_index : s->data->pc_patch_file_index; if (index.get()) { c->channel->send(0x0B, 0x00); // Start patch session; go to root directory @@ -812,7 +812,7 @@ static asio::awaitable on_DB_GC(std::shared_ptr c, Channel::Messag try { auto s = c->require_server_state(); c->set_login(s->account_index->from_gc_credentials( - serial_number, c->access_key, &c->password, "", s->allow_unregistered_users)); + serial_number, c->access_key, &c->password, "", s->data->allow_unregistered_users)); send_command(c, 0x9A, 0x02); } catch (const AccountIndex::no_username& e) { @@ -860,7 +860,7 @@ static asio::awaitable on_88_DCNTE(std::shared_ptr c, Channel::Mes try { auto s = c->require_server_state(); - c->set_login(s->account_index->from_dc_nte_credentials(c->serial_number, c->access_key, s->allow_unregistered_users)); + c->set_login(s->account_index->from_dc_nte_credentials(c->serial_number, c->access_key, s->data->allow_unregistered_users)); send_command(c, 0x88, 0x00); } catch (const AccountIndex::no_username& e) { @@ -900,7 +900,7 @@ static asio::awaitable on_8B_DCNTE(std::shared_ptr c, Channel::Mes try { auto s = c->require_server_state(); - c->set_login(s->account_index->from_dc_nte_credentials(c->serial_number, c->access_key, s->allow_unregistered_users)); + c->set_login(s->account_index->from_dc_nte_credentials(c->serial_number, c->access_key, s->data->allow_unregistered_users)); } catch (const AccountIndex::no_username& e) { c->log.info_f("Login failed (no username)"); send_message_box(c, "Incorrect serial number"); @@ -944,10 +944,10 @@ static asio::awaitable on_90_DC(std::shared_ptr c, Channel::Messag try { auto s = c->require_server_state(); if (c->serial_number.size() > 8 || c->access_key.size() > 8) { - c->set_login(s->account_index->from_dc_nte_credentials(c->serial_number, c->access_key, s->allow_unregistered_users)); + c->set_login(s->account_index->from_dc_nte_credentials(c->serial_number, c->access_key, s->data->allow_unregistered_users)); } else { serial_number = stoull(c->serial_number, nullptr, 16); - c->set_login(s->account_index->from_dc_credentials(serial_number, c->access_key, "", s->allow_unregistered_users)); + c->set_login(s->account_index->from_dc_credentials(serial_number, c->access_key, "", s->data->allow_unregistered_users)); } if (c->log.should_log(phosg::LogLevel::L_INFO)) { c->log.info_f("Received login: {}", c->login->str()); @@ -1011,11 +1011,11 @@ static asio::awaitable on_93_DC(std::shared_ptr c, Channel::Messag uint32_t serial_number = 0; try { if (c->serial_number.size() > 8 || c->access_key.size() > 8) { - c->set_login(s->account_index->from_dc_nte_credentials(c->serial_number, c->access_key, s->allow_unregistered_users)); + c->set_login(s->account_index->from_dc_nte_credentials(c->serial_number, c->access_key, s->data->allow_unregistered_users)); } else { serial_number = stoull(c->serial_number, nullptr, 16); c->set_login(s->account_index->from_dc_credentials( - serial_number, c->access_key, c->login_character_name, s->allow_unregistered_users)); + serial_number, c->access_key, c->login_character_name, s->data->allow_unregistered_users)); } if (c->log.should_log(phosg::LogLevel::L_INFO)) { c->log.info_f("Login: {}", c->login->str()); @@ -1079,7 +1079,7 @@ static asio::awaitable on_9A(std::shared_ptr c, Channel::Message& switch (c->version()) { case Version::DC_V2: { uint32_t serial_number = stoul(c->serial_number, nullptr, 16); - c->set_login(s->account_index->from_dc_credentials(serial_number, c->access_key, "", s->allow_unregistered_users)); + c->set_login(s->account_index->from_dc_credentials(serial_number, c->access_key, "", s->data->allow_unregistered_users)); if (c->log.should_log(phosg::LogLevel::L_INFO)) { c->log.info_f("Login: {}", c->login->str()); } @@ -1098,10 +1098,10 @@ static asio::awaitable on_9A(std::shared_ptr c, Channel::Message& c->channel->version = Version::PC_NTE; c->log.info_f("Changed client version to PC_NTE"); c->set_login(s->account_index->from_pc_nte_credentials( - cmd.guild_card_number, s->allow_unregistered_users && s->allow_pc_nte)); + cmd.guild_card_number, s->data->allow_unregistered_users && s->data->allow_pc_nte)); } else { uint32_t serial_number = stoul(cmd.serial_number.decode(), nullptr, 16); - c->set_login(s->account_index->from_pc_credentials(serial_number, c->access_key, "", s->allow_unregistered_users)); + c->set_login(s->account_index->from_pc_credentials(serial_number, c->access_key, "", s->data->allow_unregistered_users)); } break; } @@ -1253,7 +1253,7 @@ static asio::awaitable on_9D_9E(std::shared_ptr c, Channel::Messag case Version::DC_V2: { uint32_t serial_number = stoul(c->serial_number, nullptr, 16); c->set_login(s->account_index->from_dc_credentials( - serial_number, c->access_key, c->login_character_name, s->allow_unregistered_users)); + serial_number, c->access_key, c->login_character_name, s->data->allow_unregistered_users)); break; } case Version::PC_NTE: @@ -1268,11 +1268,11 @@ static asio::awaitable on_9D_9E(std::shared_ptr c, Channel::Messag c->channel->version = Version::PC_NTE; c->log.info_f("Changed client version to PC_NTE"); c->set_login(s->account_index->from_pc_nte_credentials( - base_cmd->guild_card_number, s->allow_unregistered_users && s->allow_pc_nte)); + base_cmd->guild_card_number, s->data->allow_unregistered_users && s->data->allow_pc_nte)); } else { uint32_t serial_number = stoul(base_cmd->serial_number.decode(), nullptr, 16); c->set_login(s->account_index->from_pc_credentials( - serial_number, c->access_key, c->login_character_name, s->allow_unregistered_users)); + serial_number, c->access_key, c->login_character_name, s->data->allow_unregistered_users)); } break; case Version::GC_NTE: @@ -1314,7 +1314,7 @@ static asio::awaitable on_9D_9E(std::shared_ptr c, Channel::Messag // not; we'll call on_login_complete once we receive the B3 response if (c->version() == Version::PC_V2) { try { - auto code = s->client_functions->get("ReturnToken", SPECIFIC_VERSION_X86_INDETERMINATE); + auto code = s->data->client_functions->get("ReturnToken", SPECIFIC_VERSION_X86_INDETERMINATE); std::unordered_map label_writes{{"token", c->login->account->account_id}}; auto resp = co_await send_function_call(c, code, label_writes, nullptr, 0, 0x00400000, 0x0000E000, 0, true); @@ -1374,7 +1374,7 @@ static asio::awaitable on_9E_XB(std::shared_ptr c, Channel::Messag uint64_t xb_user_id = stoull(c->access_key, nullptr, 16); uint64_t xb_account_id = cmd.xb_netloc.account_id; try { - c->set_login(s->account_index->from_xb_credentials(xb_gamertag, xb_user_id, xb_account_id, s->allow_unregistered_users)); + c->set_login(s->account_index->from_xb_credentials(xb_gamertag, xb_user_id, xb_account_id, s->data->allow_unregistered_users)); } catch (const AccountIndex::no_username& e) { c->log.info_f("Login failed (no username)"); send_command(c, 0x04, 0x03); @@ -1420,7 +1420,7 @@ static asio::awaitable on_93_BB(std::shared_ptr c, Channel::Messag auto s = c->require_server_state(); try { - c->set_login(s->account_index->from_bb_credentials(c->username, &c->password, s->allow_unregistered_users)); + c->set_login(s->account_index->from_bb_credentials(c->username, &c->password, s->data->allow_unregistered_users)); } catch (const AccountIndex::no_username& e) { c->log.info_f("Login failed (no username)"); send_client_init_bb(c, 0x08); @@ -1499,14 +1499,14 @@ static asio::awaitable on_93_BB(std::shared_ptr c, Channel::Messag // server phase, or else it won't know where to connect to during character selection. It's not clear why they // didn't just make it use the initial connection address by default... send_client_init_bb(c, 0); - send_reconnect(c, s->connect_address_for_client(c), s->name_to_port_config.at("bb-data1")->port); + send_reconnect(c, s->data->connect_address_for_client(c), s->data->name_to_port_config.at("bb-data1").port); co_return; - } else if (s->proxy_destination_bb.has_value()) { + } else if (s->data->proxy_destination_bb.has_value()) { // Start a proxy session immediately if there's a destination set. We don't send 00E6 (send_client_init_bb) in this // case. This is because the login command is resent to the remote server, and we forward its response back to the // client directly. - const auto& [host, port] = *s->proxy_destination_bb; + const auto& [host, port] = *s->data->proxy_destination_bb; co_await start_proxy_session(c, host, port, c->bb_connection_phase != 0); c->proxy_session->remote_client_config_data = c->bb_client_config; co_return; @@ -1520,7 +1520,7 @@ static asio::awaitable on_93_BB(std::shared_ptr c, Channel::Messag // ship select menu or a lobby join command. co_await on_login_complete(c); - } else if (s->hide_download_commands) { + } else if (s->data->hide_download_commands) { // The BB data server protocol is fairly well-understood and has some large commands, so we omit data logging for // clients on the data server. c->log.info_f("Client is in the BB data server phase; disabling command data logging for the rest of this client\'s session"); @@ -1547,8 +1547,8 @@ static asio::awaitable on_B7_Ep3(std::shared_ptr c, Channel::Messa // If the client is not in any lobby, assume they're at the main menu and send the menu song (if any). auto s = c->require_server_state(); auto l = c->lobby.lock(); - if (!l && (s->ep3_menu_song >= 0)) { - send_ep3_change_music(c->channel, s->ep3_menu_song); + if (!l && (s->data->ep3_menu_song >= 0)) { + send_ep3_change_music(c->channel, s->data->ep3_menu_song); } co_return; } @@ -1560,10 +1560,10 @@ static asio::awaitable on_BA_Ep3(std::shared_ptr c, Channel::Messa bool is_lobby = l && !l->is_game(); uint32_t current_meseta, total_meseta_earned; - if (s->ep3_infinite_meseta) { + if (s->data->ep3_infinite_meseta) { current_meseta = 1000000; total_meseta_earned = 1000000; - } else if (is_lobby && s->ep3_jukebox_is_free) { + } else if (is_lobby && s->data->ep3_jukebox_is_free) { current_meseta = c->login->account->ep3_current_meseta; total_meseta_earned = c->login->account->ep3_total_meseta_earned; } else { @@ -1622,7 +1622,7 @@ static bool add_next_game_client(std::shared_ptr l) { state_cmd.state.first_team_turn = 0xFF; state_cmd.state.tournament_flag = 0x01; state_cmd.state.client_sc_card_types.clear(Episode3::CardType::INVALID_FF); - if ((c->version() != Version::GC_EP3_NTE) && !(s->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) { + if ((c->version() != Version::GC_EP3_NTE) && !(s->data->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) { uint8_t mask_key = (phosg::random_object() % 0xFF) + 1; set_mask_for_ep3_game_command(&state_cmd, sizeof(state_cmd), mask_key); } @@ -1783,8 +1783,8 @@ static bool start_ep3_battle_table_game_if_ready(std::shared_ptr l, int16 } game->tournament_match = tourn_match; game->ep3_ex_result_values = (tourn_match && tourn && tourn->get_final_match() == tourn_match) - ? s->ep3_tournament_final_round_ex_values - : s->ep3_tournament_ex_values; + ? s->data->ep3_tournament_final_round_ex_values + : s->data->ep3_tournament_ex_values; game->clients_to_add.clear(); for (const auto& it : game_clients) { game->clients_to_add.emplace(it.first, it.second); @@ -1956,8 +1956,8 @@ static asio::awaitable on_CA_Ep3(std::shared_ptr c, Channel::Messa if (!l->ep3_server || l->ep3_server->battle_finished) { auto s = c->require_server_state(); - if (s->ep3_behavior_flags & Episode3::BehaviorFlag::ENABLE_RECORDING) { - l->battle_record = std::make_shared(s->ep3_behavior_flags); + if (s->data->ep3_behavior_flags & Episode3::BehaviorFlag::ENABLE_RECORDING) { + l->battle_record = std::make_shared(s->data->ep3_behavior_flags); for (auto existing_c : l->clients) { if (existing_c) { auto existing_p = existing_c->character_file(); @@ -2003,7 +2003,7 @@ static asio::awaitable on_CA_Ep3(std::shared_ptr c, Channel::Messa for (const auto& rc : rl->clients) { if (rc) { rc->ep3_prev_battle_record = l->battle_record; - if ((s->ep3_behavior_flags & Episode3::BehaviorFlag::ENABLE_STATUS_MESSAGES)) { + if ((s->data->ep3_behavior_flags & Episode3::BehaviorFlag::ENABLE_STATUS_MESSAGES)) { send_text_message(rc, "$C7Recording complete"); } } @@ -2037,13 +2037,13 @@ static asio::awaitable on_CA_Ep3(std::shared_ptr c, Channel::Messa uint32_t meseta_reward = 0; auto& round_rewards = loser_team->has_any_human_players() - ? s->ep3_defeat_player_meseta_rewards - : s->ep3_defeat_com_meseta_rewards; + ? s->data->ep3_defeat_player_meseta_rewards + : s->data->ep3_defeat_com_meseta_rewards; meseta_reward = (l->tournament_match->round_num - 1 < round_rewards.size()) ? round_rewards[l->tournament_match->round_num - 1] : round_rewards.back(); if (tourn && (l->tournament_match == tourn->get_final_match())) { - meseta_reward += s->ep3_final_round_meseta_bonus; + meseta_reward += s->data->ep3_final_round_meseta_bonus; } for (const auto& player : winner_team->players) { if (player.is_human()) { @@ -2116,7 +2116,7 @@ static asio::awaitable on_D6_V3(std::shared_ptr c, Channel::Messag check_size_v(msg.data.size(), 0); if (c->check_flag(Client::Flag::IN_INFORMATION_MENU)) { auto s = c->require_server_state(); - send_menu(c, s->information_menu(c->version())); + send_menu(c, s->data->information_menu(c->version())); } else if (c->check_flag(Client::Flag::AT_WELCOME_MESSAGE)) { c->clear_flag(Client::Flag::AT_WELCOME_MESSAGE); send_main_menu(c); @@ -2137,10 +2137,10 @@ static asio::awaitable on_09(std::shared_ptr c, Channel::Message& case MenuID::QUEST_EP1: case MenuID::QUEST_EP2: { bool is_download_quest = !c->lobby.lock(); - if (!s->quest_index) { + if (!s->data->quest_index) { send_quest_info(c, "$C7Quests are not available.", 0x00, is_download_quest); } else { - auto q = s->quest_index->get(cmd.item_id); + auto q = s->data->quest_index->get(cmd.item_id); if (!q) { send_quest_info(c, "$C4Quest does not\nexist.", 0x00, is_download_quest); } else { @@ -2159,7 +2159,7 @@ static asio::awaitable on_09(std::shared_ptr c, Channel::Message& ? Episode3::MapIndex::VisibilityFlag::ONLINE_TRIAL : Episode3::MapIndex::VisibilityFlag::ONLINE_FINAL; - auto map = s->ep3_map_index->map_for_id(cmd.item_id); + auto map = s->data->ep3_map_index->map_for_id(cmd.item_id); if (!map || !map->check_visibility_flag(vis_flag)) { send_quest_info(c, "$C4Map does not exist.", 0x00, true); } else { @@ -2408,7 +2408,8 @@ static void on_quest_loaded(std::shared_ptr l) { if (is_v4(lc->version())) { lc->change_bank(lc->bb_character_index); } - lc->create_challenge_overlay(lc->version(), l->quest->meta.challenge_template_index, s->level_table(lc->version())); + lc->create_challenge_overlay( + lc->version(), l->quest->meta.challenge_template_index, s->data->level_table(lc->version())); lc->log.info_f("Created challenge overlay"); l->assign_inventory_and_bank_item_ids(lc, true); @@ -2416,7 +2417,7 @@ static void on_quest_loaded(std::shared_ptr l) { if (is_v4(lc->version())) { lc->change_bank(lc->bb_character_index); } - lc->create_battle_overlay(l->quest->meta.battle_rules, s->level_table(lc->version())); + lc->create_battle_overlay(l->quest->meta.battle_rules, s->data->level_table(lc->version())); lc->log.info_f("Created battle overlay"); } } @@ -2529,7 +2530,7 @@ static asio::awaitable on_10_main_menu(std::shared_ptr c, uint32_t } case MainMenuItemID::INFORMATION: { - send_menu(c, s->information_menu(c->version())); + send_menu(c, s->data->information_menu(c->version())); c->set_flag(Client::Flag::IN_INFORMATION_MENU); break; } @@ -2550,7 +2551,7 @@ static asio::awaitable on_10_main_menu(std::shared_ptr c, uint32_t // We have to prepare the client for patches here, even though we don't send them from this mennu, because we // need to know the client's specific_version before sending the menu. co_await prepare_client_for_patches(c); - send_menu(c, c->require_server_state()->client_functions->patch_switches_menu(c->specific_version, s->auto_patches, c->login->account->auto_patches_enabled)); + send_menu(c, c->require_server_state()->data->client_functions->patch_switches_menu(c->specific_version, s->data->auto_patches, c->login->account->auto_patches_enabled)); break; } @@ -2559,7 +2560,7 @@ static asio::awaitable on_10_main_menu(std::shared_ptr c, uint32_t throw std::runtime_error("client does not support send_function_call"); } co_await prepare_client_for_patches(c); - send_menu(c, c->require_server_state()->dol_file_index->menu); + send_menu(c, c->require_server_state()->data->dol_file_index->menu); break; } @@ -2572,7 +2573,7 @@ static asio::awaitable on_10_main_menu(std::shared_ptr c, uint32_t break; case MainMenuItemID::CLEAR_LICENSE: { - auto conf_menu = std::make_shared(MenuID::CLEAR_LICENSE_CONFIRMATION, s->name); + auto conf_menu = std::make_shared(MenuID::CLEAR_LICENSE_CONFIRMATION, s->data->name); conf_menu->items.emplace_back(ClearLicenseConfirmationMenuItemID::CANCEL, "Go back", "Go back to the\nmain menu", 0); conf_menu->items.emplace_back(ClearLicenseConfirmationMenuItemID::CLEAR_LICENSE, "Clear license", @@ -2607,7 +2608,7 @@ static void on_10_information(std::shared_ptr c, uint32_t item_id) { send_main_menu(c); } else { try { - auto contents = c->require_server_state()->information_contents_for_client(c); + auto contents = c->require_server_state()->data->information_contents_for_client(c); send_message_box(c, contents->at(item_id)); } catch (const std::out_of_range&) { send_message_box(c, "$C6No such information exists."); @@ -2694,7 +2695,7 @@ static asio::awaitable on_10_proxy_destinations(std::shared_ptr c, auto s = c->require_server_state(); const std::pair* dest = nullptr; try { - dest = &s->proxy_destinations(c->version()).at(item_id); + dest = &s->data->proxy_destinations(c->version()).at(item_id); } catch (const std::out_of_range&) { } @@ -2784,7 +2785,7 @@ static void on_10_game_menu(std::shared_ptr c, uint32_t item_id, const s static void on_10_quest_categories(std::shared_ptr c, uint32_t item_id) { if (is_ep3(c->version())) { auto s = c->require_server_state(); - if (!s->ep3_map_index) { + if (!s->data->ep3_map_index) { send_lobby_message_box(c, "$C7Quests are not available."); return; } @@ -2792,7 +2793,7 @@ static void on_10_quest_categories(std::shared_ptr c, uint32_t item_id) } else { auto s = c->require_server_state(); - if (!s->quest_index) { + if (!s->data->quest_index) { send_lobby_message_box(c, "$C7Quests are not available."); return; } @@ -2805,7 +2806,7 @@ static void on_10_quest_categories(std::shared_ptr c, uint32_t item_id) include_condition = l->quest_include_condition(); } - const auto& quests = s->quest_index->filter(episode, version_flags, item_id, include_condition); + const auto& quests = s->data->quest_index->filter(episode, version_flags, item_id, include_condition); send_quest_menu(c, quests, !l); } } @@ -2816,11 +2817,11 @@ static void on_10_quest_menu(std::shared_ptr c, uint32_t item_id) { } auto s = c->require_server_state(); - if (!s->quest_index) { + if (!s->data->quest_index) { send_lobby_message_box(c, "$C7Quests are not\navailable."); return; } - auto q = s->quest_index->get(item_id); + auto q = s->data->quest_index->get(item_id); if (!q) { send_lobby_message_box(c, "$C7Quest does not exist."); return; @@ -2874,7 +2875,7 @@ static void on_10_ep3_download_quest_menu(std::shared_ptr c, uint32_t it throw std::runtime_error("Episode 3 quests can only be downloaded when client is not in a lobby"); } - auto map = s->ep3_map_index->map_for_id(item_id); + auto map = s->data->ep3_map_index->map_for_id(item_id); auto vis_flag = (c->version() == Version::GC_EP3_NTE) ? Episode3::MapIndex::VisibilityFlag::ONLINE_TRIAL @@ -2900,12 +2901,12 @@ static void on_10_patch_switches(std::shared_ptr c, uint32_t item_id) { } auto s = c->require_server_state(); - auto fn = s->client_functions->get_by_menu_item_id(item_id); + auto fn = s->data->client_functions->get_by_menu_item_id(item_id); if (!c->login->account->auto_patches_enabled.emplace(fn->short_name).second) { c->login->account->auto_patches_enabled.erase(fn->short_name); } c->login->account->save(); - send_menu(c, s->client_functions->patch_switches_menu(c->specific_version, s->auto_patches, c->login->account->auto_patches_enabled)); + send_menu(c, s->data->client_functions->patch_switches_menu(c->specific_version, s->data->auto_patches, c->login->account->auto_patches_enabled)); } } @@ -2919,7 +2920,7 @@ static asio::awaitable on_10_programs(std::shared_ptr c, uint32_t } auto s = c->require_server_state(); - auto dol = s->dol_file_index->item_id_to_file.at(item_id); + auto dol = s->data->dol_file_index->item_id_to_file.at(item_id); co_await send_dol_file(c, dol); // Disconnects the client } } @@ -3119,7 +3120,7 @@ static asio::awaitable on_08_E6(std::shared_ptr c, Channel::Messag static asio::awaitable on_1F(std::shared_ptr c, Channel::Message& msg) { check_size_v(msg.data.size(), 0); auto s = c->require_server_state(); - send_menu(c, s->information_menu(c->version()), true); + send_menu(c, s->data->information_menu(c->version()), true); co_return; } @@ -3652,8 +3653,8 @@ static asio::awaitable on_06(std::shared_ptr c, Channel::Message& } auto s = c->require_server_state(); - char command_sentinel = s->chat_command_sentinel - ? s->chat_command_sentinel + char command_sentinel = s->data->chat_command_sentinel + ? s->data->chat_command_sentinel : ((c->version() == Version::DC_11_2000) ? '@' : '$'); if ((text[0] == command_sentinel) && c->can_use_chat_commands()) { if (text[1] == command_sentinel) { @@ -3970,7 +3971,7 @@ static asio::awaitable on_E5_BB(std::shared_ptr c, Channel::Messag try { auto s = c->require_server_state(); c->create_character_file( - c->login->account->account_id, c->language(), cmd.preview.visual, s->level_table(c->version())); + c->login->account->account_id, c->language(), cmd.preview.visual, s->data->level_table(c->version())); } catch (const std::exception& e) { send_message_box(c, std::format("$C6New character could not be created:\n{}", e.what())); should_send_approve = false; @@ -4103,7 +4104,7 @@ static asio::awaitable on_DF_BB(std::shared_ptr c, Channel::Messag if (is_v4(lc->version())) { lc->change_bank(lc->bb_character_index); } - lc->create_challenge_overlay(lc->version(), l->quest->meta.challenge_template_index, s->level_table(lc->version())); + lc->create_challenge_overlay(lc->version(), l->quest->meta.challenge_template_index, s->data->level_table(lc->version())); lc->log.info_f("Created challenge overlay"); l->assign_inventory_and_bank_item_ids(lc, true); } @@ -4169,10 +4170,10 @@ static asio::awaitable on_DF_BB(std::shared_ptr c, Channel::Messag ? p->challenge_records.ep2_online_award_state : p->challenge_records.ep1_online_award_state; award_state.rank_award_flags |= cmd.rank_bitmask; - p->add_item(cmd.item, *s->item_stack_limits(c->version())); + p->add_item(cmd.item, *s->data->item_stack_limits(c->version())); l->on_item_id_generated_externally(cmd.item.id); l->log.info_f("(Challenge mode) Item awarded to player {}: {}", - c->lobby_client_id, s->describe_item(Version::BB_V4, cmd.item)); + c->lobby_client_id, s->data->describe_item(Version::BB_V4, cmd.item)); break; } } @@ -4280,18 +4281,18 @@ static void on_choice_search_t(std::shared_ptr c, const ChoiceSearchConf result.info_string.encode(info_string, c->language()); std::string location_string; if (l->is_game()) { - location_string = std::format("{},,BLOCK01,{}", l->name, s->name); + location_string = std::format("{},,BLOCK01,{}", l->name, s->data->name); } else if (l->is_ep3()) { - location_string = std::format("BLOCK01-C{:02},,BLOCK01,{}", l->lobby_id - 15, s->name); + location_string = std::format("BLOCK01-C{:02},,BLOCK01,{}", l->lobby_id - 15, s->data->name); } else { - location_string = std::format("BLOCK01-{:02},,BLOCK01,{}", l->lobby_id, s->name); + location_string = std::format("BLOCK01-{:02},,BLOCK01,{}", l->lobby_id, s->data->name); } 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->game_server_port_for_version(c->version()); + result.reconnect_command.address = s->data->connect_address_for_client(c); + result.reconnect_command.port = s->data->game_server_port_for_version(c->version()); 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.visual.name.decode(lc->language()), c->language()); @@ -4508,7 +4509,7 @@ std::shared_ptr create_game_generic( auto current_lobby = creator_c->require_lobby(); - size_t min_level = s->default_min_level_for_game(creator_c->version(), episode, difficulty); + size_t min_level = s->data->default_min_level_for_game(creator_c->version(), episode, difficulty); auto p = creator_c->character_file(); if (!creator_c->login->account->check_flag(Account::Flag::FREE_JOIN_GAMES) && (min_level > p->disp.stats.level)) { @@ -4524,7 +4525,7 @@ std::shared_ptr create_game_generic( game->episode = episode; game->mode = mode; game->difficulty = difficulty; - game->allowed_versions = s->compatibility_groups.at(static_cast(creator_c->version())); + game->allowed_versions = s->data->compatibility_groups.at(static_cast(creator_c->version())); static_assert(NUM_VERSIONS == 14, "Don't forget to update the group compatibility restrictions"); if (!allow_v1 || (difficulty == Difficulty::ULTIMATE) || (mode == GameMode::CHALLENGE) || (mode == GameMode::SOLO)) { game->forbid_version(Version::DC_NTE); @@ -4579,13 +4580,13 @@ std::shared_ptr create_game_generic( game->floor_item_managers.emplace_back(game->lobby_id, game->floor_item_managers.size()); } - if (s->behavior_enabled(s->cheat_mode_behavior)) { + if (s->data->behavior_enabled(s->data->cheat_mode_behavior)) { game->set_flag(Lobby::Flag::CHEATS_ENABLED); } - if (!s->behavior_can_be_overridden(s->cheat_mode_behavior)) { + if (!s->data->behavior_can_be_overridden(s->data->cheat_mode_behavior)) { game->set_flag(Lobby::Flag::CANNOT_CHANGE_CHEAT_MODE); } - if (s->use_game_creator_section_id) { + if (s->data->use_game_creator_section_id) { game->set_flag(Lobby::Flag::USE_CREATOR_SECTION_ID); } if (watched_lobby || battle_player) { @@ -4616,8 +4617,8 @@ std::shared_ptr create_game_generic( game->battle_player = battle_player; battle_player->set_lobby(game); } - game->base_exp_multiplier = s->bb_global_exp_multiplier; - game->exp_share_multiplier = s->exp_share_multiplier; + game->base_exp_multiplier = s->data->bb_global_exp_multiplier; + game->exp_share_multiplier = s->data->exp_share_multiplier; const std::unordered_map* quest_flag_rewrites; switch (creator_c->version()) { @@ -4627,31 +4628,31 @@ std::shared_ptr create_game_generic( case Version::DC_V2: case Version::PC_NTE: case Version::PC_V2: - quest_flag_rewrites = &s->quest_flag_rewrites_v1_v2; + quest_flag_rewrites = &s->data->quest_flag_rewrites_v1_v2; if (game->mode == GameMode::BATTLE) { - game->drop_mode = s->default_drop_mode_v1_v2_battle; - game->allowed_drop_modes = s->allowed_drop_modes_v1_v2_battle; + game->drop_mode = s->data->default_drop_mode_v1_v2_battle; + game->allowed_drop_modes = s->data->allowed_drop_modes_v1_v2_battle; } else if (game->mode == GameMode::CHALLENGE) { - game->drop_mode = s->default_drop_mode_v1_v2_challenge; - game->allowed_drop_modes = s->allowed_drop_modes_v1_v2_challenge; + game->drop_mode = s->data->default_drop_mode_v1_v2_challenge; + game->allowed_drop_modes = s->data->allowed_drop_modes_v1_v2_challenge; } else { - game->drop_mode = s->default_drop_mode_v1_v2_normal; - game->allowed_drop_modes = s->allowed_drop_modes_v1_v2_normal; + game->drop_mode = s->data->default_drop_mode_v1_v2_normal; + game->allowed_drop_modes = s->data->allowed_drop_modes_v1_v2_normal; } break; case Version::GC_NTE: case Version::GC_V3: case Version::XB_V3: - quest_flag_rewrites = &s->quest_flag_rewrites_v3; + quest_flag_rewrites = &s->data->quest_flag_rewrites_v3; if (game->mode == GameMode::BATTLE) { - game->drop_mode = s->default_drop_mode_v3_battle; - game->allowed_drop_modes = s->allowed_drop_modes_v3_battle; + game->drop_mode = s->data->default_drop_mode_v3_battle; + game->allowed_drop_modes = s->data->allowed_drop_modes_v3_battle; } else if (game->mode == GameMode::CHALLENGE) { - game->drop_mode = s->default_drop_mode_v3_challenge; - game->allowed_drop_modes = s->allowed_drop_modes_v3_challenge; + game->drop_mode = s->data->default_drop_mode_v3_challenge; + game->allowed_drop_modes = s->data->allowed_drop_modes_v3_challenge; } else { - game->drop_mode = s->default_drop_mode_v3_normal; - game->allowed_drop_modes = s->allowed_drop_modes_v3_normal; + game->drop_mode = s->data->default_drop_mode_v3_normal; + game->allowed_drop_modes = s->data->allowed_drop_modes_v3_normal; } break; case Version::GC_EP3_NTE: @@ -4661,16 +4662,16 @@ std::shared_ptr create_game_generic( game->allowed_drop_modes = (1 << static_cast(game->drop_mode)); break; case Version::BB_V4: - quest_flag_rewrites = &s->quest_flag_rewrites_v4; + quest_flag_rewrites = &s->data->quest_flag_rewrites_v4; if (game->mode == GameMode::BATTLE) { - game->drop_mode = s->default_drop_mode_v4_battle; - game->allowed_drop_modes = s->allowed_drop_modes_v4_battle; + game->drop_mode = s->data->default_drop_mode_v4_battle; + game->allowed_drop_modes = s->data->allowed_drop_modes_v4_battle; } else if (game->mode == GameMode::CHALLENGE) { - game->drop_mode = s->default_drop_mode_v4_challenge; - game->allowed_drop_modes = s->allowed_drop_modes_v4_challenge; + game->drop_mode = s->data->default_drop_mode_v4_challenge; + game->allowed_drop_modes = s->data->allowed_drop_modes_v4_challenge; } else { - game->drop_mode = s->default_drop_mode_v4_normal; - game->allowed_drop_modes = s->allowed_drop_modes_v4_normal; + game->drop_mode = s->data->default_drop_mode_v4_normal; + game->allowed_drop_modes = s->data->allowed_drop_modes_v4_normal; } // Disallow CLIENT mode on BB if (game->drop_mode == ServerDropMode::CLIENT) { @@ -4698,9 +4699,9 @@ std::shared_ptr create_game_generic( bool is_solo = (game->mode == GameMode::SOLO); if (game->mode == GameMode::CHALLENGE) { - game->rare_enemy_rates = s->rare_enemy_rates_challenge; + game->rare_enemy_rates = s->data->rare_enemy_rates_challenge; } else { - game->rare_enemy_rates = s->rare_enemy_rates(game->difficulty); + game->rare_enemy_rates = s->data->rare_enemy_rates(game->difficulty); } if (game->episode != Episode::EP3) { @@ -4714,7 +4715,7 @@ std::shared_ptr create_game_generic( auto vars_str = game->variations.str(); game->log.info_f("Using variations from client override: {}", vars_str); } else { - auto sdt = s->set_data_table(creator_c->version(), game->episode, game->mode, game->difficulty); + auto sdt = s->data->set_data_table(creator_c->version(), game->episode, game->mode, game->difficulty); game->variations = sdt->generate_variations(game->episode, is_solo, game->rand_crypt); auto vars_str = game->variations.str(); game->log.info_f("Using random variations: {}", vars_str); @@ -4850,7 +4851,7 @@ static asio::awaitable on_0C_C1_E7_EC(std::shared_ptr c, Channel:: game = create_game_generic(s, c, cmd.name.decode(c->language()), cmd.password.decode(c->language()), episode, mode, cmd.difficulty, allow_v1, watched_lobby); if (game && (game->episode == Episode::EP3)) { - game->ep3_ex_result_values = s->ep3_default_ex_values; + game->ep3_ex_result_values = s->data->ep3_default_ex_values; if (spectators_forbidden) { game->set_flag(Lobby::Flag::SPECTATORS_FORBIDDEN); } @@ -4954,8 +4955,8 @@ static asio::awaitable on_6F(std::shared_ptr c, Channel::Message& auto s = c->require_server_state(); std::shared_ptr q; try { - int64_t quest_num = s->enable_send_function_call_quest_numbers.at(c->specific_version); - q = s->quest_index->get(quest_num); + int64_t quest_num = s->data->enable_send_function_call_quest_numbers.at(c->specific_version); + q = s->data->quest_index->get(quest_num); } catch (const std::out_of_range&) { throw std::logic_error("cannot find patch enable quest after it was previously found during login"); } @@ -5186,8 +5187,8 @@ static asio::awaitable on_D2_V3_BB(std::shared_ptr c, Channel::Mes // Delete items that are being given away for (const auto& item : c->pending_item_trade->items) { - size_t amount = item.stack_size(*s->item_stack_limits(c->version())); - p->remove_item(item.id, amount, *s->item_stack_limits(c->version())); + size_t amount = item.stack_size(*s->data->item_stack_limits(c->version())); + p->remove_item(item.id, amount, *s->data->item_stack_limits(c->version())); // This is a special case: when the trade is executed, the client deletes the traded items from its own // inventory automatically, so we should NOT send the 6x29 to that client; we should only send it to the other @@ -5203,7 +5204,7 @@ static asio::awaitable on_D2_V3_BB(std::shared_ptr c, Channel::Mes for (const auto& trade_item : other_c->pending_item_trade->items) { ItemData added_item = trade_item; added_item.id = l->generate_item_id(c->lobby_client_id); - p->add_item(added_item, *s->item_stack_limits(c->version())); + p->add_item(added_item, *s->data->item_stack_limits(c->version())); send_create_inventory_item_to_lobby(c, c->lobby_client_id, added_item); } send_command(c, 0xD3, 0x00); @@ -5576,7 +5577,7 @@ static asio::awaitable on_EA_BB(std::shared_ptr c, Channel::Messag send_team_metadata_change_notifications(s, team, 0, TeamMetadataChange::REWARD_FLAGS); } if (!reward.reward_item.empty()) { - c->bank_file()->add_item(reward.reward_item, *s->item_stack_limits(c->version())); + c->bank_file()->add_item(reward.reward_item, *s->data->item_stack_limits(c->version())); c->print_bank(); } } diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 0665e148..045d4e34 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -333,7 +333,7 @@ void forward_subcommand_with_item_transcode_t(std::shared_ptr c, uint8_t out_cmd.header.subcommand = translate_subcommand_number(lc->version(), c->version(), out_cmd.header.subcommand); if (out_cmd.header.subcommand) { out_cmd.item_data.decode_for_version(c->version()); - out_cmd.item_data.encode_for_version(lc->version(), s->item_parameter_table_for_encode(lc->version())); + out_cmd.item_data.encode_for_version(lc->version(), s->data->item_parameter_table_for_encode(lc->version())); send_command_t(lc, command, flag, out_cmd); } else { lc->log.info_f("Subcommand cannot be translated to client\'s version"); @@ -937,7 +937,7 @@ G_SyncPlayerDispAndInventory_DCNTE_6x70 Parsed6x70Data::as_dc_nte(std::shared_pt ret.visual.name.encode(this->name, this->language); ret.visual.sh = this->visual_sh; ret.visual.enforce_lobby_join_limits_for_version(Version::DC_NTE); - uint32_t name_color = s->name_color_for_client(this->from_version, this->from_client_customization); + uint32_t name_color = s->data->name_color_for_client(this->from_version, this->from_client_customization); if (name_color) { ret.visual.sh.name_color = name_color; ret.visual.sh.compute_name_color_checksum(); @@ -950,7 +950,7 @@ G_SyncPlayerDispAndInventory_DCNTE_6x70 Parsed6x70Data::as_dc_nte(std::shared_pt ret.num_items, this->item_version, Version::DC_NTE, - s->item_parameter_table_for_encode(Version::DC_NTE)); + s->data->item_parameter_table_for_encode(Version::DC_NTE)); return ret; } @@ -967,7 +967,7 @@ G_SyncPlayerDispAndInventory_DC112000_6x70 Parsed6x70Data::as_dc_112000(std::sha ret.player_flags = this->get_player_flags(false); ret.visual.name.encode(this->name, this->language); ret.visual.sh = this->visual_sh; - uint32_t name_color = s->name_color_for_client(this->from_version, this->from_client_customization); + uint32_t name_color = s->data->name_color_for_client(this->from_version, this->from_client_customization); if (name_color) { ret.visual.sh.name_color = name_color; ret.visual.sh.compute_name_color_checksum(); @@ -980,7 +980,7 @@ G_SyncPlayerDispAndInventory_DC112000_6x70 Parsed6x70Data::as_dc_112000(std::sha ret.num_items, this->item_version, Version::DC_11_2000, - s->item_parameter_table_for_encode(Version::DC_11_2000)); + s->data->item_parameter_table_for_encode(Version::DC_11_2000)); ret.visual.enforce_lobby_join_limits_for_version(Version::DC_11_2000); return ret; } @@ -990,7 +990,7 @@ G_SyncPlayerDispAndInventory_DC_PC_6x70 Parsed6x70Data::as_dc_pc(std::shared_ptr ret.base = this->base_v1(false); ret.visual.name.encode(this->name, this->language); ret.visual.sh = this->visual_sh; - uint32_t name_color = s->name_color_for_client(this->from_version, this->from_client_customization); + uint32_t name_color = s->data->name_color_for_client(this->from_version, this->from_client_customization); if (name_color) { ret.visual.sh.name_color = name_color; ret.visual.sh.compute_name_color_checksum(); @@ -999,7 +999,7 @@ G_SyncPlayerDispAndInventory_DC_PC_6x70 Parsed6x70Data::as_dc_pc(std::shared_ptr ret.num_items = this->num_items; ret.items = this->items; transcode_inventory_items( - ret.items, ret.num_items, this->item_version, to_version, s->item_parameter_table_for_encode(to_version)); + ret.items, ret.num_items, this->item_version, to_version, s->data->item_parameter_table_for_encode(to_version)); ret.visual.sh.enforce_lobby_join_limits_for_version(to_version); return ret; } @@ -1010,7 +1010,7 @@ G_SyncPlayerDispAndInventory_GC_6x70 Parsed6x70Data::as_gc_gcnte(std::shared_ptr ret.visual.name.encode(this->name, this->language); ret.visual.sh = this->visual_sh; ret.visual.enforce_lobby_join_limits_for_version(to_version); - uint32_t name_color = s->name_color_for_client(this->from_version, this->from_client_customization); + uint32_t name_color = s->data->name_color_for_client(this->from_version, this->from_client_customization); if (name_color) { ret.visual.sh.name_color = name_color; if (is_v1_or_v2(to_version)) { @@ -1023,7 +1023,7 @@ G_SyncPlayerDispAndInventory_GC_6x70 Parsed6x70Data::as_gc_gcnte(std::shared_ptr ret.num_items = this->num_items; ret.items = this->items; transcode_inventory_items( - ret.items, ret.num_items, this->item_version, to_version, s->item_parameter_table_for_encode(to_version)); + ret.items, ret.num_items, this->item_version, to_version, s->data->item_parameter_table_for_encode(to_version)); ret.floor = this->floor; return ret; } @@ -1033,7 +1033,7 @@ G_SyncPlayerDispAndInventory_XB_6x70 Parsed6x70Data::as_xb(std::shared_ptrbase_v1(true); ret.visual.name.encode(this->name, this->language); ret.visual.sh = this->visual_sh; - uint32_t name_color = s->name_color_for_client(this->from_version, this->from_client_customization); + uint32_t name_color = s->data->name_color_for_client(this->from_version, this->from_client_customization); if (name_color) { ret.visual.sh.name_color = name_color; ret.visual.sh.name_color_checksum = 0; @@ -1042,7 +1042,7 @@ G_SyncPlayerDispAndInventory_XB_6x70 Parsed6x70Data::as_xb(std::shared_ptrnum_items; ret.items = this->items; transcode_inventory_items( - ret.items, ret.num_items, this->item_version, Version::XB_V3, s->item_parameter_table_for_encode(Version::XB_V3)); + ret.items, ret.num_items, this->item_version, Version::XB_V3, s->data->item_parameter_table_for_encode(Version::XB_V3)); ret.visual.sh.enforce_lobby_join_limits_for_version(Version::XB_V3); ret.floor = this->floor; ret.xb_user_id_high = this->xb_user_id >> 32; @@ -1058,7 +1058,7 @@ G_SyncPlayerDispAndInventory_BB_6x70 Parsed6x70Data::as_bb(std::shared_ptrvisual_sh; ret.visual.name.encode(this->name, this->language); ret.visual.enforce_lobby_join_limits_for_version(Version::BB_V4); - uint32_t name_color = s->name_color_for_client(this->from_version, this->from_client_customization); + uint32_t name_color = s->data->name_color_for_client(this->from_version, this->from_client_customization); if (name_color) { ret.visual.sh.name_color = name_color; ret.visual.sh.name_color_checksum = 0; @@ -1067,7 +1067,7 @@ G_SyncPlayerDispAndInventory_BB_6x70 Parsed6x70Data::as_bb(std::shared_ptrnum_items; ret.items = this->items; transcode_inventory_items( - ret.items, ret.num_items, this->item_version, Version::BB_V4, s->item_parameter_table_for_encode(Version::BB_V4)); + ret.items, ret.num_items, this->item_version, Version::BB_V4, s->data->item_parameter_table_for_encode(Version::BB_V4)); ret.floor = this->floor; ret.xb_user_id_high = this->xb_user_id >> 32; ret.xb_user_id_low = this->xb_user_id; @@ -1389,7 +1389,8 @@ static void on_ep3_battle_subs(std::shared_ptr c, SubcommandMessage& msg if (!lc || (lc == c)) { continue; } - if (!(s->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING) && (lc->version() != Version::GC_EP3_NTE)) { + if (!(s->data->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING) && + (lc->version() != Version::GC_EP3_NTE)) { set_mask_for_ep3_game_command(msg.data, msg.size, (phosg::random_object() % 0xFF) + 1); } send_command(lc, 0xC9, 0x00, msg.data, msg.size); @@ -1531,12 +1532,12 @@ static void on_word_select_t(std::shared_ptr c, SubcommandMessage& msg) if (is_big_endian(lc->version())) { G_WordSelectBE_6x74 out_cmd = { subcommand, cmd.size, cmd.client_id.load(), - s->word_select_table->translate(cmd.message, from_version, lc_version)}; + s->data->word_select_table->translate(cmd.message, from_version, lc_version)}; send_command_t(lc, 0x60, 0x00, out_cmd); } else { G_WordSelect_6x74 out_cmd = { subcommand, cmd.size, cmd.client_id.load(), - s->word_select_table->translate(cmd.message, from_version, lc_version)}; + s->data->word_select_table->translate(cmd.message, from_version, lc_version)}; send_command_t(lc, 0x60, 0x00, out_cmd); } @@ -1933,12 +1934,12 @@ static void on_player_drop_item(std::shared_ptr c, SubcommandMessage& ms auto s = c->require_server_state(); auto l = c->require_lobby(); auto p = c->character_file(); - auto item = p->remove_item(cmd.item_id, 0, *s->item_stack_limits(c->version())); + auto item = p->remove_item(cmd.item_id, 0, *s->data->item_stack_limits(c->version())); l->add_item(cmd.floor, item, cmd.pos, nullptr, nullptr, 0x00F); if (l->log.should_log(phosg::LogLevel::L_INFO)) { auto s = c->require_server_state(); - auto name = s->describe_item(c->version(), item); + auto name = s->data->describe_item(c->version(), item); l->log.info_f("Player {} dropped item {:08X} ({}) at {}:({:g}, {:g})", cmd.header.client_id, cmd.item_id, name, cmd.floor, cmd.pos.x, cmd.pos.z); c->print_inventory(); @@ -1973,15 +1974,15 @@ static void on_create_inventory_item_t(std::shared_ptr c, SubcommandMess } if (l->log.should_log(phosg::LogLevel::L_INFO)) { - auto name = s->describe_item(c->version(), item); + auto name = s->data->describe_item(c->version(), item); l->log.info_f("Player {} created inventory item {:08X} ({}) in inventory of NPC {:02X}; ignoring", c->lobby_client_id, item.id, name, cmd.header.client_id); } } else { - c->character_file()->add_item(item, *s->item_stack_limits(c->version())); + c->character_file()->add_item(item, *s->data->item_stack_limits(c->version())); if (l->log.should_log(phosg::LogLevel::L_INFO)) { - auto name = s->describe_item(c->version(), item); + auto name = s->data->describe_item(c->version(), item); l->log.info_f("Player {} created inventory item {:08X} ({})", c->lobby_client_id, item.id, name); c->print_inventory(); } @@ -2022,7 +2023,7 @@ static void on_drop_partial_stack_t(std::shared_ptr c, SubcommandMessage if (l->log.should_log(phosg::LogLevel::L_INFO)) { auto s = c->require_server_state(); - auto name = s->describe_item(c->version(), item); + auto name = s->data->describe_item(c->version(), item); l->log.info_f("Player {} split stack to create floor item {:08X} ({}) at {}:({:g},{:g})", cmd.header.client_id, item.id, name, cmd.floor, cmd.pos.x, cmd.pos.z); c->print_inventory(); @@ -2056,7 +2057,7 @@ static void on_drop_partial_stack_bb(std::shared_ptr c, SubcommandMessag auto s = c->require_server_state(); auto p = c->character_file(); - const auto& limits = *s->item_stack_limits(c->version()); + const auto& limits = *s->data->item_stack_limits(c->version()); auto item = p->remove_item(cmd.item_id, cmd.amount, limits); // If a stack was split, the original item still exists, so the dropped item needs a new ID. remove_item signals this @@ -2074,7 +2075,7 @@ static void on_drop_partial_stack_bb(std::shared_ptr c, SubcommandMessag if (l->log.should_log(phosg::LogLevel::L_INFO)) { auto s = c->require_server_state(); - auto name = s->describe_item(c->version(), item); + auto name = s->data->describe_item(c->version(), item); l->log.info_f("Player {} split stack {:08X} (removed: {}) at {}:({:g}, {:g})", cmd.header.client_id, cmd.item_id, name, cmd.floor, cmd.pos.x, cmd.pos.z); c->print_inventory(); @@ -2100,13 +2101,13 @@ static void on_buy_shop_item(std::shared_ptr c, SubcommandMessage& msg) item.data2d = 0; // Clear the price field item.decode_for_version(c->version()); l->on_item_id_generated_externally(item.id); - p->add_item(item, *s->item_stack_limits(c->version())); + p->add_item(item, *s->data->item_stack_limits(c->version())); - size_t price = s->item_parameter_table(c->version())->price_for_item(item); + size_t price = s->data->item_parameter_table(c->version())->price_for_item(item); p->remove_meseta(price, c->version() != Version::BB_V4); if (l->log.should_log(phosg::LogLevel::L_INFO)) { - auto name = s->describe_item(c->version(), item); + auto name = s->data->describe_item(c->version(), item); l->log.info_f("Player {} bought item {:08X} ({}) from shop ({} Meseta)", cmd.header.client_id, item.id, name, price); c->print_inventory(); @@ -2125,7 +2126,7 @@ void send_item_notification_if_needed(std::shared_ptr c, const ItemData& break; case Client::ItemDropNotificationMode::RARES_ONLY: should_notify = (is_from_rare_table || (item.data1[0] == 0x03)) && - s->item_parameter_table(c->version())->is_item_rare(item); + s->data->item_parameter_table(c->version())->is_item_rare(item); should_include_rare_header = true; break; case Client::ItemDropNotificationMode::ALL_ITEMS: @@ -2137,7 +2138,7 @@ void send_item_notification_if_needed(std::shared_ptr c, const ItemData& } if (should_notify) { - std::string name = s->describe_item(c->version(), item, ItemNameIndex::Flag::INCLUDE_PSO_COLOR_ESCAPES); + std::string name = s->data->describe_item(c->version(), item, ItemNameIndex::Flag::INCLUDE_PSO_COLOR_ESCAPES); const char* rare_header = (should_include_rare_header ? "$C6Rare item dropped:\n" : ""); send_text_message_fmt(c, "{}{}", rare_header, name); } @@ -2161,7 +2162,7 @@ static void on_box_or_enemy_item_drop_t(std::shared_ptr c, SubcommandMes throw std::runtime_error("BB client sent 6x5F command"); } - bool should_notify = s->rare_notifs_enabled_for_client_drops && (l->drop_mode == ServerDropMode::CLIENT); + bool should_notify = s->data->rare_notifs_enabled_for_client_drops && (l->drop_mode == ServerDropMode::CLIENT); std::shared_ptr ene_st; std::shared_ptr obj_st; @@ -2179,15 +2180,9 @@ static void on_box_or_enemy_item_drop_t(std::shared_ptr c, SubcommandMes l->on_item_id_generated_externally(item.id); l->add_item(cmd.item.floor, item, cmd.item.pos, obj_st, ene_st, should_notify ? 0x100F : 0x000F); - auto name = s->describe_item(c->version(), item); + auto name = s->data->describe_item(c->version(), item); l->log.info_f("Player {} (leader) created floor item {:08X} ({}){} at {}:({:g}, {:g})", - l->leader_id, - item.id, - name, - from_entity_str, - cmd.item.floor, - cmd.item.pos.x, - cmd.item.pos.z); + l->leader_id, item.id, name, from_entity_str, cmd.item.floor, cmd.item.pos.x, cmd.item.pos.z); for (auto& lc : l->clients) { if (!lc) { @@ -2253,7 +2248,7 @@ static asio::awaitable on_pick_up_item_generic( } try { - p->add_item(fi->data, *s->item_stack_limits(c->version())); + p->add_item(fi->data, *s->data->item_stack_limits(c->version())); } catch (const std::out_of_range&) { // Inventory is full; put the item back where it was l->log.warning_f("Player {} requests to pick up {:08X}, but their inventory is full; dropping command", @@ -2264,7 +2259,7 @@ static asio::awaitable on_pick_up_item_generic( if (l->log.should_log(phosg::LogLevel::L_INFO)) { auto s = c->require_server_state(); - auto name = s->describe_item(c->version(), fi->data); + auto name = s->data->describe_item(c->version(), fi->data); l->log.info_f("Player {} picked up {:08X} ({})", client_id, item_id, name); c->print_inventory(); } @@ -2285,20 +2280,20 @@ static asio::awaitable on_pick_up_item_generic( uint32_t pi = fi->data.primary_identifier(); bool should_send_game_notif, should_send_global_notif; if (is_v1_or_v2(c->version()) && (c->version() != Version::GC_NTE)) { - should_send_game_notif = s->notify_game_for_item_primary_identifiers_v1_v2.count(pi); - should_send_global_notif = s->notify_server_for_item_primary_identifiers_v1_v2.count(pi); + should_send_game_notif = s->data->notify_game_for_item_primary_identifiers_v1_v2.count(pi); + should_send_global_notif = s->data->notify_server_for_item_primary_identifiers_v1_v2.count(pi); } else if (!is_v4(c->version())) { - should_send_game_notif = s->notify_game_for_item_primary_identifiers_v3.count(pi); - should_send_global_notif = s->notify_server_for_item_primary_identifiers_v3.count(pi); + should_send_game_notif = s->data->notify_game_for_item_primary_identifiers_v3.count(pi); + should_send_global_notif = s->data->notify_server_for_item_primary_identifiers_v3.count(pi); } else { - should_send_game_notif = s->notify_game_for_item_primary_identifiers_v4.count(pi); - should_send_global_notif = s->notify_server_for_item_primary_identifiers_v4.count(pi); + should_send_game_notif = s->data->notify_game_for_item_primary_identifiers_v4.count(pi); + should_send_global_notif = s->data->notify_server_for_item_primary_identifiers_v4.count(pi); } if (should_send_game_notif || should_send_global_notif) { std::string p_name = p->disp.visual.name.decode(); - std::string desc_ingame = s->describe_item(c->version(), fi->data, ItemNameIndex::Flag::INCLUDE_PSO_COLOR_ESCAPES); - std::string desc_http = s->describe_item(c->version(), fi->data); + std::string desc_ingame = s->data->describe_item(c->version(), fi->data, ItemNameIndex::Flag::INCLUDE_PSO_COLOR_ESCAPES); + std::string desc_http = s->data->describe_item(c->version(), fi->data); if (s->http_server) { auto message = std::make_shared(phosg::JSON::dict({ @@ -2391,7 +2386,7 @@ static void on_use_item(std::shared_ptr c, SubcommandMessage& msg) { // Note: We manually downscope item here because player_use_item will likely move or delete the item, which will // break the reference, so we don't want to accidentally use it again after that. const auto& item = p->inventory.items[index].data; - name = s->describe_item(c->version(), item); + name = s->data->describe_item(c->version(), item); } player_use_item(c, index, l->rand_crypt); @@ -2420,9 +2415,9 @@ static void on_feed_mag(std::shared_ptr c, SubcommandMessage& msg) { { // Note: We downscope these because player_feed_mag will likely delete the items, which will break these references const auto& fed_item = p->inventory.items[fed_index].data; - fed_name = s->describe_item(c->version(), fed_item); + fed_name = s->data->describe_item(c->version(), fed_item); const auto& mag_item = p->inventory.items[mag_index].data; - mag_name = s->describe_item(c->version(), mag_item); + mag_name = s->data->describe_item(c->version(), mag_item); } player_feed_mag(c, mag_index, fed_index); @@ -2430,7 +2425,7 @@ static void on_feed_mag(std::shared_ptr c, SubcommandMessage& msg) { // fed item. So on BB, we should remove the fed item here, but on other versions, we allow the following 6x29 command // to do that. if (c->version() == Version::BB_V4) { - p->remove_item(cmd.fed_item_id, 1, *s->item_stack_limits(c->version())); + p->remove_item(cmd.fed_item_id, 1, *s->data->item_stack_limits(c->version())); } if (l->log.should_log(phosg::LogLevel::L_INFO)) { @@ -2530,7 +2525,7 @@ static void on_open_shop_bb_or_ep3_battle_subs(std::shared_ptr c, Subcom } for (auto& item : c->bb_shop_contents[cmd.shop_type]) { item.id = 0xFFFFFFFF; - item.data2d = s->item_parameter_table(c->version())->price_for_item(item); + item.data2d = s->data->item_parameter_table(c->version())->price_for_item(item); } send_shop(c, cmd.shop_type); @@ -2593,7 +2588,7 @@ static void on_ep3_private_word_select_bb_bank_action(std::shared_ptr c, auto s = c->require_server_state(); if (is_ep3(c->version())) { const auto& cmd = msg.check_size_t(); - s->word_select_table->validate(cmd.message, c->version()); + s->data->word_select_table->validate(cmd.message, c->version()); std::string from_name = c->character_file()->disp.visual.name.decode(c->language()); static const std::string whisper_text = "(whisper)"; @@ -2665,7 +2660,7 @@ static void on_ep3_private_word_select_bb_bank_action(std::shared_ptr c, } } else { // Deposit item - const auto& limits = *s->item_stack_limits(c->version()); + const auto& limits = *s->data->item_stack_limits(c->version()); auto item = p->remove_item(cmd.item_id, cmd.item_amount, limits); // If a stack was split, the bank item retains the same item ID as the inventory item. This is annoying but // doesn't cause any problems because we always generate a new item ID when withdrawing from the bank, so @@ -2677,7 +2672,7 @@ static void on_ep3_private_word_select_bb_bank_action(std::shared_ptr c, send_destroy_item_to_lobby(c, cmd.item_id, cmd.item_amount, true); if (l->log.should_log(phosg::LogLevel::L_INFO)) { - std::string name = s->describe_item(Version::BB_V4, item); + std::string name = s->data->describe_item(Version::BB_V4, item); l->log.info_f("Player {} deposited item {:08X} (x{}) ({}) in the bank", c->lobby_client_id, cmd.item_id, cmd.item_amount, name); c->print_inventory(); @@ -2700,14 +2695,14 @@ static void on_ep3_private_word_select_bb_bank_action(std::shared_ptr c, } } else { // Take item - const auto& limits = *s->item_stack_limits(c->version()); + const auto& limits = *s->data->item_stack_limits(c->version()); auto item = bank->remove_item(cmd.item_id, cmd.item_amount, limits); item.id = l->generate_item_id(c->lobby_client_id); p->add_item(item, limits); send_create_inventory_item_to_lobby(c, c->lobby_client_id, item); if (l->log.should_log(phosg::LogLevel::L_INFO)) { - std::string name = s->describe_item(Version::BB_V4, item); + std::string name = s->data->describe_item(Version::BB_V4, item); l->log.info_f("Player {} withdrew item {:08X} (x{}) ({}) from the bank", c->lobby_client_id, item.id, cmd.item_amount, name); c->print_inventory(); @@ -2991,7 +2986,7 @@ static void on_entity_drop_item_request(std::shared_ptr c, SubcommandMes if (res.item.empty()) { l->log.info_f("No item was created"); } else { - std::string name = s->describe_item(c->version(), res.item); + std::string name = s->data->describe_item(c->version(), res.item); l->log.info_f("Entity {:04X} (area {:02X}) created item {}", cmd.entity_index, cmd.effective_area, name); if (drop_mode == ServerDropMode::SERVER_DUPLICATE) { for (const auto& lc : l->clients) { @@ -3029,7 +3024,7 @@ static void on_entity_drop_item_request(std::shared_ptr c, SubcommandMes if (res.item.empty()) { l->log.info_f("No item was created for {}", lc->channel->name); } else { - std::string name = s->describe_item(lc->version(), res.item); + std::string name = s->data->describe_item(lc->version(), res.item); l->log.info_f("Entity {:04X} (area {:02X}) created item {}", cmd.entity_index, cmd.effective_area, name); res.item.id = l->generate_item_id(0xFF); l->log.info_f("Creating item {:08X} at {:02X}:{:g},{:g} for {}", @@ -3846,7 +3841,7 @@ static void on_charge_attack_bb(std::shared_ptr c, SubcommandMessage& ms static void send_max_level_notification_if_needed(std::shared_ptr c) { auto s = c->require_server_state(); - if (!s->notify_server_for_max_level_achieved) { + if (!s->data->notify_server_for_max_level_achieved) { return; } @@ -3886,7 +3881,7 @@ static void on_level_up(std::shared_ptr c, SubcommandMessage& msg) { if (is_pre_v1(c->version())) { msg.check_size_t(); auto s = c->require_server_state(); - auto level_table = s->level_table(c->version()); + auto level_table = s->data->level_table(c->version()); const auto& incrs = level_table->stats_delta_for_level(p->disp.visual.sh.char_class, p->disp.stats.level + 1); p->disp.stats.char_stats.atp += incrs.atp; p->disp.stats.char_stats.mst += incrs.mst; @@ -3922,7 +3917,7 @@ static void add_player_exp(std::shared_ptr c, uint32_t exp, uint16_t fro bool leveled_up = false; do { - const auto& level = s->level_table(c->version())->stats_delta_for_level(p->disp.visual.sh.char_class, p->disp.stats.level + 1); + const auto& level = s->data->level_table(c->version())->stats_delta_for_level(p->disp.visual.sh.char_class, p->disp.stats.level + 1); if (p->disp.stats.exp >= level.exp) { leveled_up = true; level.apply(p->disp.stats.char_stats); @@ -4027,7 +4022,7 @@ static void on_steal_exp_bb(std::shared_ptr c, SubcommandMessage& msg) { const auto& inventory = p->inventory; const auto& weapon = inventory.items[inventory.find_equipped_item(EquipSlot::WEAPON)]; - auto item_parameter_table = s->item_parameter_table(c->version()); + auto item_parameter_table = s->data->item_parameter_table(c->version()); uint8_t special_id = 0; if (((weapon.data.data1[1] < 0x0A) && (weapon.data.data1[2] < 0x05)) || @@ -4046,7 +4041,7 @@ static void on_steal_exp_bb(std::shared_ptr c, SubcommandMessage& msg) { Episode episode = episode_for_area(area); auto type = ene_st->type(c->version(), area, l->difficulty, l->event); uint32_t enemy_exp = base_exp_for_enemy_type( - s->battle_params, l->quest, type, episode, l->difficulty, ene_st->super_ene->floor, l->mode == GameMode::SOLO); + s->data->battle_params, l->quest, type, episode, l->difficulty, ene_st->super_ene->floor, l->mode == GameMode::SOLO); // Note: The original code checks if special.type is 9, 10, or 11, and skips applying the android bonus if so. We // don't do anything for those special types, so we don't check for that here. @@ -4098,7 +4093,7 @@ static void on_enemy_exp_request_bb(std::shared_ptr c, SubcommandMessage auto& inventory = c->character_file()->inventory; for (size_t z = 0; z < inventory.num_items; z++) { auto& item = inventory.items[z]; - if ((item.flags & 0x08) && s->item_parameter_table(c->version())->is_unsealable_item(item.data)) { + if ((item.flags & 0x08) && s->data->item_parameter_table(c->version())->is_unsealable_item(item.data)) { size_t new_kill_count = item.data.get_kill_count() + 1; item.data.set_kill_count(new_kill_count); c->log.info_f("Item {:08X} kill count updated to {}", item.data.id, new_kill_count); @@ -4110,7 +4105,7 @@ static void on_enemy_exp_request_bb(std::shared_ptr c, SubcommandMessage Episode episode = episode_for_area(area); auto type = ene_st->type(c->version(), area, l->difficulty, l->event); double base_exp = base_exp_for_enemy_type( - s->battle_params, l->quest, type, episode, l->difficulty, ene_st->super_ene->floor, l->mode == GameMode::SOLO); + s->data->battle_params, l->quest, type, episode, l->difficulty, ene_st->super_ene->floor, l->mode == GameMode::SOLO); // If this player killed the enemy, they get full EXP; if they tagged the enemy, they get 80% EXP; if auto EXP share // is enabled and they are close enough to the monster, they get a smaller share; if none of these situations apply, @@ -4204,7 +4199,7 @@ static void on_adjust_player_meseta_bb(std::shared_ptr c, SubcommandMess item.data1[0] = 0x04; item.data2d = cmd.amount; item.id = l->generate_item_id(c->lobby_client_id); - p->add_item(item, *s->item_stack_limits(c->version())); + p->add_item(item, *s->data->item_stack_limits(c->version())); send_create_inventory_item_to_lobby(c, c->lobby_client_id, item); } } @@ -4240,7 +4235,7 @@ static void on_quest_create_item_bb(std::shared_ptr c, SubcommandMessage const auto& cmd = msg.check_size_t(); auto s = c->require_server_state(); auto l = c->require_lobby(); - const auto& limits = *s->item_stack_limits(c->version()); + const auto& limits = *s->data->item_stack_limits(c->version()); ItemData item; item = cmd.item_data; @@ -4261,7 +4256,7 @@ static void on_quest_create_item_bb(std::shared_ptr c, SubcommandMessage c->character_file()->add_item(item, limits); send_create_inventory_item_to_lobby(c, c->lobby_client_id, item); if (l->log.should_log(phosg::LogLevel::L_INFO)) { - auto name = s->describe_item(c->version(), item); + auto name = s->data->describe_item(c->version(), item); l->log.info_f("Player {} created inventory item {:08X} ({}) via quest command", c->lobby_client_id, item.id, name); c->print_inventory(); @@ -4269,7 +4264,7 @@ static void on_quest_create_item_bb(std::shared_ptr c, SubcommandMessage } catch (const std::out_of_range&) { if (l->log.should_log(phosg::LogLevel::L_INFO)) { - auto name = s->describe_item(c->version(), item); + auto name = s->data->describe_item(c->version(), item); l->log.info_f("Player {} attempted to create inventory item {:08X} ({}) via quest command, but it cannot be placed in their inventory", c->lobby_client_id, item.id, name); } @@ -4292,11 +4287,11 @@ static void on_transfer_item_via_mail_message_bb(std::shared_ptr c, Subc auto s = c->require_server_state(); auto p = c->character_file(); - const auto& limits = *s->item_stack_limits(c->version()); + const auto& limits = *s->data->item_stack_limits(c->version()); auto item = p->remove_item(cmd.item_id, cmd.amount, limits); if (l->log.should_log(phosg::LogLevel::L_INFO)) { - auto name = s->describe_item(c->version(), item); + auto name = s->data->describe_item(c->version(), item); l->log.info_f("Player {} sent inventory item {}:{:08X} ({}) x{} to player {:08X}", c->lobby_client_id, cmd.header.client_id, cmd.item_id, name, cmd.amount, cmd.target_guild_card_number); c->print_inventory(); @@ -4353,15 +4348,15 @@ static void on_exchange_item_for_team_points_bb(std::shared_ptr c, Subco auto s = c->require_server_state(); auto p = c->character_file(); - const auto& limits = *s->item_stack_limits(c->version()); + const auto& limits = *s->data->item_stack_limits(c->version()); auto item = p->remove_item(cmd.item_id, cmd.amount, limits); size_t amount = item.stack_size(limits); - size_t points = s->item_parameter_table(Version::BB_V4)->get_item_team_points(item); + size_t points = s->data->item_parameter_table(Version::BB_V4)->get_item_team_points(item); s->team_index->add_member_points(c->login->account->account_id, points * amount); if (l->log.should_log(phosg::LogLevel::L_INFO)) { - auto name = s->describe_item(c->version(), item); + auto name = s->data->describe_item(c->version(), item); l->log.info_f("Player {} exchanged inventory item {}:{:08X} ({}) x{} for {} * {} = {} team points", c->lobby_client_id, cmd.header.client_id, cmd.item_id, name, amount, points, amount, points * amount); c->print_inventory(); @@ -4389,10 +4384,10 @@ static void on_destroy_inventory_item(std::shared_ptr c, SubcommandMessa auto s = c->require_server_state(); auto p = c->character_file(); - auto item = p->remove_item(cmd.item_id, cmd.amount, *s->item_stack_limits(c->version())); + auto item = p->remove_item(cmd.item_id, cmd.amount, *s->data->item_stack_limits(c->version())); if (l->log.should_log(phosg::LogLevel::L_INFO)) { - auto name = s->describe_item(c->version(), item); + auto name = s->data->describe_item(c->version(), item); l->log.info_f("Player {} destroyed inventory item {}:{:08X} ({})", c->lobby_client_id, cmd.header.client_id, cmd.item_id, name); c->print_inventory(); @@ -4441,7 +4436,7 @@ static void on_destroy_floor_item(std::shared_ptr c, SubcommandMessage& c->lobby_client_id, cmd.item_id); } else { - auto name = s->describe_item(c->version(), fi->data); + auto name = s->data->describe_item(c->version(), fi->data); l->log.info_f("Player {} destroyed floor item {:08X} ({})", c->lobby_client_id, cmd.item_id, name); // Only forward to players for whom the item was visible @@ -4525,7 +4520,7 @@ static void on_accept_identify_item_bb(std::shared_ptr c, SubcommandMess throw std::runtime_error("accepted item ID does not match previous identify request"); } auto s = c->require_server_state(); - c->character_file()->add_item(c->bb_identify_result, *s->item_stack_limits(c->version())); + c->character_file()->add_item(c->bb_identify_result, *s->data->item_stack_limits(c->version())); send_create_inventory_item_to_lobby(c, c->lobby_client_id, c->bb_identify_result); c->bb_identify_result.clear(); } @@ -4544,12 +4539,12 @@ static void on_sell_item_at_shop_bb(std::shared_ptr c, SubcommandMessage auto s = c->require_server_state(); auto p = c->character_file(); - auto item = p->remove_item(cmd.item_id, cmd.amount, *s->item_stack_limits(c->version())); - size_t price = (s->item_parameter_table(c->version())->price_for_item(item) >> 3) * cmd.amount; + auto item = p->remove_item(cmd.item_id, cmd.amount, *s->data->item_stack_limits(c->version())); + size_t price = (s->data->item_parameter_table(c->version())->price_for_item(item) >> 3) * cmd.amount; p->add_meseta(price); if (l->log.should_log(phosg::LogLevel::L_INFO)) { - auto name = s->describe_item(c->version(), item); + auto name = s->data->describe_item(c->version(), item); l->log.info_f("Player {} sold inventory item {:08X} ({}) for {} Meseta", c->lobby_client_id, cmd.item_id, name, price); c->print_inventory(); @@ -4569,7 +4564,7 @@ static void on_buy_shop_item_bb(std::shared_ptr c, SubcommandMessage& ms const auto& cmd = msg.check_size_t(); auto s = c->require_server_state(); - const auto& limits = *s->item_stack_limits(c->version()); + const auto& limits = *s->data->item_stack_limits(c->version()); ItemData item; item = c->bb_shop_contents.at(cmd.shop_type).at(cmd.item_index); @@ -4591,7 +4586,7 @@ static void on_buy_shop_item_bb(std::shared_ptr c, SubcommandMessage& ms if (l->log.should_log(phosg::LogLevel::L_INFO)) { auto s = c->require_server_state(); - auto name = s->describe_item(c->version(), item); + auto name = s->data->describe_item(c->version(), item); l->log.info_f("Player {} purchased item {:08X} ({}) for {} meseta", c->lobby_client_id, item.id, name, price); c->print_inventory(); } @@ -4645,7 +4640,7 @@ static void on_battle_restart_bb(std::shared_ptr c, SubcommandMessage& m if (is_v4(lc->version())) { lc->change_bank(lc->bb_character_index); } - lc->create_battle_overlay(new_rules, s->level_table(c->version())); + lc->create_battle_overlay(new_rules, s->data->level_table(c->version())); } } l->map_state->reset(); @@ -4683,7 +4678,7 @@ static void on_battle_level_up_bb(std::shared_ptr c, SubcommandMessage& auto lp = lc->character_file(); uint32_t target_level = std::min(lp->disp.stats.level + cmd.num_levels, 199); uint32_t before_exp = lp->disp.stats.exp; - s->level_table(lc->version())->advance_to_level(lp->disp.stats, target_level, lp->disp.visual.sh.char_class); + s->data->level_table(lc->version())->advance_to_level(lp->disp.stats, target_level, lp->disp.visual.sh.char_class); if ((lp->disp.stats.exp > before_exp) && (lc->version() == Version::BB_V4)) { send_give_experience(lc, lp->disp.stats.exp - before_exp, 0xFFFF); send_level_up(lc); @@ -4718,7 +4713,7 @@ static void on_battle_tech_level_up(std::shared_ptr c, SubcommandMessage if (lc) { auto s = c->require_server_state(); auto lp = lc->character_file(); - auto pmt = s->item_parameter_table(lc->version()); + auto pmt = s->data->item_parameter_table(lc->version()); for (uint8_t tech_num = 0; tech_num < 0x13; tech_num++) { size_t level = lp->get_technique_level(tech_num); if (level != 0xFF) { @@ -4789,7 +4784,7 @@ static void on_challenge_mode_retry_or_quit(std::shared_ptr c, Subcomman if (is_v4(lc->version())) { lc->change_bank(lc->bb_character_index); } - lc->create_challenge_overlay(lc->version(), l->quest->meta.challenge_template_index, s->level_table(c->version())); + lc->create_challenge_overlay(lc->version(), l->quest->meta.challenge_template_index, s->data->level_table(c->version())); lc->log.info_f("Created challenge overlay"); l->assign_inventory_and_bank_item_ids(lc, true); } @@ -4958,7 +4953,7 @@ static void on_quest_exchange_item_bb(std::shared_ptr c, SubcommandMessa try { auto p = c->character_file(); - const auto& limits = *s->item_stack_limits(c->version()); + const auto& limits = *s->data->item_stack_limits(c->version()); ItemData new_item = cmd.replace_item; assert_quest_item_create_allowed(l, new_item); @@ -4993,10 +4988,10 @@ static void on_wrap_item_bb(std::shared_ptr c, SubcommandMessage& msg) { auto s = c->require_server_state(); auto p = c->character_file(); - auto item = p->remove_item(cmd.item.id, 1, *s->item_stack_limits(c->version())); + auto item = p->remove_item(cmd.item.id, 1, *s->data->item_stack_limits(c->version())); send_destroy_item_to_lobby(c, item.id, 1); - item.wrap(*s->item_stack_limits(c->version()), cmd.present_color); - p->add_item(item, *s->item_stack_limits(c->version())); + item.wrap(*s->data->item_stack_limits(c->version()), cmd.present_color); + p->add_item(item, *s->data->item_stack_limits(c->version())); send_create_inventory_item_to_lobby(c, c->lobby_client_id, item); } @@ -5014,7 +5009,7 @@ static void on_photon_drop_exchange_for_item_bb(std::shared_ptr c, Subco try { auto p = c->character_file(); - const auto& limits = *s->item_stack_limits(c->version()); + const auto& limits = *s->data->item_stack_limits(c->version()); ItemData new_item = cmd.new_item; assert_quest_item_create_allowed(l, new_item); @@ -5047,7 +5042,7 @@ static void on_photon_drop_exchange_for_s_rank_special_bb(std::shared_ptr(); auto s = c->require_server_state(); - const auto& limits = *s->item_stack_limits(c->version()); + const auto& limits = *s->data->item_stack_limits(c->version()); try { auto p = c->character_file(); @@ -5142,7 +5137,7 @@ static void on_secret_lottery_ticket_exchange_bb(std::shared_ptr c, Subc item.data1[z] = r.min; } auto s = c->require_server_state(); - const auto& limits = *s->item_stack_limits(c->version()); + const auto& limits = *s->data->item_stack_limits(c->version()); item.enforce_stack_size_limits(limits); uint32_t slt_item_id = p->inventory.items[currency_index].data.id; @@ -5175,7 +5170,7 @@ static void on_photon_crystal_exchange_bb(std::shared_ptr c, SubcommandM auto s = c->require_server_state(); auto p = c->character_file(); size_t index = p->inventory.find_item_by_primary_identifier(0x03100200); - auto item = p->remove_item(p->inventory.items[index].data.id, 1, *s->item_stack_limits(c->version())); + auto item = p->remove_item(p->inventory.items[index].data.id, 1, *s->data->item_stack_limits(c->version())); send_destroy_item_to_lobby(c, item.id, 1); l->drop_mode = ServerDropMode::DISABLED; l->allowed_drop_modes = (1 << static_cast(l->drop_mode)); // DISABLED only @@ -5199,7 +5194,7 @@ static void on_quest_F95E_result_bb(std::shared_ptr c, SubcommandMessage size_t count = (cmd.type > 0x03) ? 1 : (static_cast(l->difficulty) + 1); c->log.info_f("Creating {} F95E result items", count); for (size_t z = 0; z < count; z++) { - const auto& results = s->quest_F95E_results.at(cmd.type).at(static_cast(l->difficulty)); + const auto& results = s->data->quest_F95E_results.at(cmd.type).at(static_cast(l->difficulty)); if (results.empty()) { throw std::runtime_error("invalid result type"); } @@ -5214,7 +5209,7 @@ static void on_quest_F95E_result_bb(std::shared_ptr c, SubcommandMessage } else if (item.data1[0] == 0x00) { item.data1[4] |= 0x80; // Unidentified } else { - item.enforce_stack_size_limits(*s->item_stack_limits(c->version())); + item.enforce_stack_size_limits(*s->data->item_stack_limits(c->version())); } item.id = l->generate_item_id(0xFF); @@ -5244,12 +5239,12 @@ static void on_quest_F95F_result_bb(std::shared_ptr c, SubcommandMessage auto s = c->require_server_state(); auto p = c->character_file(); - const auto& result = s->quest_F95F_results.at(cmd.result_index); + const auto& result = s->data->quest_F95F_results.at(cmd.result_index); if (result.second.empty()) { throw std::runtime_error("invalid result index"); } - const auto& limits = *s->item_stack_limits(c->version()); + const auto& limits = *s->data->item_stack_limits(c->version()); bool failed = false; ItemData ticket_item; @@ -5309,7 +5304,7 @@ static void on_quest_F960_result_bb(std::shared_ptr c, SubcommandMessage ItemData item; for (size_t num_failures = 0; num_failures <= cmd.result_tier; num_failures++) { size_t tier = cmd.result_tier - num_failures; - const auto& results = s->quest_F960_success_results.at(tier); + const auto& results = s->data->quest_F960_success_results.at(tier); uint64_t probability = results.base_probability + num_failures * results.probability_upgrade; if (l->rand_crypt->next() <= probability) { c->log.info_f("Tier {} yielded a prize", tier); @@ -5322,7 +5317,7 @@ static void on_quest_F960_result_bb(std::shared_ptr c, SubcommandMessage } if (item.empty()) { c->log.info_f("Choosing result from failure tier"); - const auto& result_items = s->quest_F960_failure_results.results.at(weekday); + const auto& result_items = s->data->quest_F960_failure_results.results.at(weekday); item = result_items[l->rand_crypt->next() % result_items.size()]; } if (item.empty()) { @@ -5333,7 +5328,7 @@ static void on_quest_F960_result_bb(std::shared_ptr c, SubcommandMessage item.id = l->generate_item_id(c->lobby_client_id); // If it's a weapon, make it unidentified - auto item_parameter_table = s->item_parameter_table(c->version()); + auto item_parameter_table = s->data->item_parameter_table(c->version()); if ((item.data1[0] == 0x00) && (item_parameter_table->is_item_rare(item) || (item.data1[4] != 0))) { item.data1[4] |= 0x80; } @@ -5347,7 +5342,7 @@ static void on_quest_F960_result_bb(std::shared_ptr c, SubcommandMessage // Add the item to the player's inventory if possible; if not, drop it on the floor where the player is standing bool added_to_inventory; try { - p->add_item(item, *s->item_stack_limits(c->version())); + p->add_item(item, *s->data->item_stack_limits(c->version())); added_to_inventory = true; } catch (const std::out_of_range&) { // If the game's drop mode is private or duplicate, make the item visible only to this player; in other modes, make @@ -5360,7 +5355,7 @@ static void on_quest_F960_result_bb(std::shared_ptr c, SubcommandMessage } if (c->log.should_log(phosg::LogLevel::L_INFO)) { - std::string name = s->describe_item(c->version(), item); + std::string name = s->data->describe_item(c->version(), item); c->log.info_f("Awarded item {} {}", name, added_to_inventory ? "in inventory" : "on ground (inventory is full)"); } if (added_to_inventory) { @@ -5387,7 +5382,7 @@ static void on_momoka_item_exchange_bb(std::shared_ptr c, SubcommandMess auto s = c->require_server_state(); auto p = c->character_file(); - const auto& limits = *s->item_stack_limits(c->version()); + const auto& limits = *s->data->item_stack_limits(c->version()); ItemData new_item = cmd.replace_item; assert_quest_item_create_allowed(l, new_item); @@ -5449,7 +5444,7 @@ static void on_upgrade_weapon_attribute_bb(std::shared_ptr c, Subcommand uint32_t payment_primary_identifier = cmd.payment_type ? 0x03100100 : 0x03100000; size_t payment_index = p->inventory.find_item_by_primary_identifier(payment_primary_identifier); auto& payment_item = p->inventory.items[payment_index].data; - if (payment_item.stack_size(*s->item_stack_limits(c->version())) < cmd.payment_count) { + if (payment_item.stack_size(*s->data->item_stack_limits(c->version())) < cmd.payment_count) { throw std::runtime_error("not enough payment items present"); } @@ -5480,7 +5475,7 @@ static void on_upgrade_weapon_attribute_bb(std::shared_ptr c, Subcommand } auto removed_payment_item = p->remove_item( - payment_item.id, cmd.payment_count, *s->item_stack_limits(c->version())); + payment_item.id, cmd.payment_count, *s->data->item_stack_limits(c->version())); send_destroy_item_to_lobby(c, removed_payment_item.id, cmd.payment_count); item.data1[attribute_index] = cmd.attribute; diff --git a/src/ReplaySession.cc b/src/ReplaySession.cc index 70445e52..e3e7d518 100644 --- a/src/ReplaySession.cc +++ b/src/ReplaySession.cc @@ -319,19 +319,17 @@ void ReplaySession::apply_default_mask(std::shared_ptr ev) { break; } default: - throw std::logic_error("invalid game version"); + throw std::logic_error("Invalid game version"); } } ReplaySession::ReplaySession(std::shared_ptr state, FILE* input_log) : state(state), - prev_psov2_crypt_enabled(this->state->use_psov2_rand_crypt), commands_sent(0), bytes_sent(0), commands_received(0), bytes_received(0), - idle_timeout_timer(*this->state->io_context), - run_failed(false) { + idle_timeout_timer(*this->state->io_context) { std::shared_ptr parsing_command = nullptr; size_t line_num = 0; @@ -364,16 +362,16 @@ ReplaySession::ReplaySession(std::shared_ptr state, FILE* input_log } if (line == "### use psov2 crypt") { - this->state->use_psov2_rand_crypt = true; + this->use_psov2_rand_crypt = true; } if (line == "### use legacy item random behavior") { - this->state->use_legacy_item_random_behavior = true; + this->use_legacy_item_random_behavior = true; } if (line.starts_with("### cc ")) { // ### cc $ if (this->clients.size() != 1) { throw std::runtime_error(std::format( - "(ev-line {}) cc shortcut cannot be used with multiple clients connected; use on C-X cc instead", + "(ev-line {}) Bare `cc` shortcut cannot be used with multiple clients connected; use `on C-X cc` instead", line_num)); } std::shared_ptr event; @@ -383,7 +381,7 @@ ReplaySession::ReplaySession(std::shared_ptr state, FILE* input_log event->data = encode_chat_message(c->version, line.substr(7)); num_events++; } catch (const std::exception& e) { - throw std::runtime_error(std::format("(ev-line {}) failed to generate chat message ({})", line_num, e.what())); + throw std::runtime_error(std::format("(ev-line {}) Failed to generate chat message ({})", line_num, e.what())); } continue; @@ -400,7 +398,7 @@ ReplaySession::ReplaySession(std::shared_ptr state, FILE* input_log event->data = encode_chat_message(c->version, line.substr(end_offset + 13)); num_events++; } catch (const std::exception& e) { - throw std::runtime_error(std::format("(ev-line {}) failed to generate chat message ({})", line_num, e.what())); + throw std::runtime_error(std::format("(ev-line {}) Failed to generate chat message ({})", line_num, e.what())); } continue; @@ -412,21 +410,21 @@ ReplaySession::ReplaySession(std::shared_ptr state, FILE* input_log auto tokens = phosg::split(line, ' '); if (!tokens[8].starts_with("C-")) { - throw std::runtime_error(std::format("(ev-line {}) client connection message missing client ID token", line_num)); + throw std::runtime_error(std::format("(ev-line {}) Client connection message missing client ID token", line_num)); } uint64_t client_id = stoull(tokens[8].substr(2), nullptr, 16); auto listen_tokens = phosg::split(tokens[10], '-'); if (listen_tokens.size() < 4) { throw std::runtime_error(std::format( - "(ev-line {}) client connection message listening socket token format is incorrect", line_num)); + "(ev-line {}) Client connection message listening socket token format is incorrect", line_num)); } uint16_t port = stoul(listen_tokens[1], nullptr, 10); Version version = phosg::enum_for_name(listen_tokens[2]); auto c = std::make_shared(state->io_context, client_id, port, version); if (!this->clients.emplace(c->id, c).second) { - throw std::runtime_error(std::format("(ev-line {}) duplicate client ID in input log", line_num)); + throw std::runtime_error(std::format("(ev-line {}) Duplicate client ID in input log", line_num)); } this->create_event(Event::Type::CONNECT, c, line_num); num_events++; @@ -438,21 +436,21 @@ ReplaySession::ReplaySession(std::shared_ptr state, FILE* input_log if (offset != std::string::npos) { auto tokens = phosg::split(line, ' '); if (tokens.size() < 11) { - throw std::runtime_error(std::format("(ev-line {}) client disconnection message has incorrect token count", line_num)); + throw std::runtime_error(std::format("(ev-line {}) Client disconnection message has incorrect token count", line_num)); } if (!tokens[10].starts_with("C-")) { - throw std::runtime_error(std::format("(ev-line {}) client disconnection message missing client ID token", line_num)); + throw std::runtime_error(std::format("(ev-line {}) Client disconnection message missing client ID token", line_num)); } uint64_t client_id = stoul(tokens[10].substr(2), nullptr, 16); try { auto& c = this->clients.at(client_id); if (c->disconnect_event.get()) { - throw std::runtime_error(std::format("(ev-line {}) client has multiple disconnect events", line_num)); + throw std::runtime_error(std::format("(ev-line {}) Client has multiple disconnect events", line_num)); } c->disconnect_event = this->create_event(Event::Type::DISCONNECT, c, line_num); num_events++; } catch (const std::out_of_range&) { - throw std::runtime_error(std::format("(ev-line {}) unknown disconnecting client ID in input log", line_num)); + throw std::runtime_error(std::format("(ev-line {}) Unknown disconnecting client ID in input log", line_num)); } continue; } @@ -466,7 +464,7 @@ ReplaySession::ReplaySession(std::shared_ptr state, FILE* input_log if (offset != std::string::npos) { auto tokens = phosg::split(line, ' '); if (tokens.size() < 10) { - throw std::runtime_error(std::format("(ev-line {}) command header line too short", line_num)); + throw std::runtime_error(std::format("(ev-line {}) Command header line too short", line_num)); } bool from_client = (tokens[6] == "Received"); uint64_t client_id = stoull(tokens[8].substr(2), nullptr, 16); @@ -475,7 +473,7 @@ ReplaySession::ReplaySession(std::shared_ptr state, FILE* input_log from_client ? Event::Type::SEND : Event::Type::RECEIVE, this->clients.at(client_id), line_num); num_events++; } catch (const std::out_of_range&) { - throw std::runtime_error(std::format("(ev-line {}) input log contains command for missing client", line_num)); + throw std::runtime_error(std::format("(ev-line {}) Input log contains command for missing client", line_num)); } continue; } @@ -494,8 +492,12 @@ ReplaySession::ReplaySession(std::shared_ptr state, FILE* input_log } asio::awaitable ReplaySession::run() { + bool prev_use_psov2_rand_crypt = this->state->use_psov2_rand_crypt; + bool prev_use_legacy_item_random_behavior = this->state->use_legacy_item_random_behavior; + this->state->use_psov2_rand_crypt = this->use_psov2_rand_crypt; + this->state->use_legacy_item_random_behavior = this->use_legacy_item_random_behavior; + try { - replay_log.info_f("Starting replay"); while (this->first_event) { if (!this->first_event->complete) { auto& c = this->clients.at(this->first_event->client_id); @@ -505,15 +507,15 @@ asio::awaitable ReplaySession::run() { case Event::Type::CONNECT: { if (c->channel->connected()) { throw std::runtime_error(std::format( - "(ev-line {}) connect event on already-connected client", this->first_event->line_num)); + "(ev-line {}) Connect event on already-connected client", this->first_event->line_num)); } - std::shared_ptr port_config; + const DataIndex::PortConfiguration* port_config; try { - port_config = this->state->number_to_port_config.at(c->port); + port_config = &this->state->data->number_to_port_config.at(c->port); } catch (const std::out_of_range&) { throw std::runtime_error(std::format( - "(ev-line {}) client connected to port missing from configuration", this->first_event->line_num)); + "(ev-line {}) Client connected to port missing from configuration", this->first_event->line_num)); } auto server_channel = std::make_shared(this->state->io_context, port_config->version, c->channel->language, "", phosg::TerminalFormat::END, phosg::TerminalFormat::END, false, false); @@ -523,7 +525,7 @@ asio::awaitable ReplaySession::run() { this->state->game_server->connect_channel(server_channel, c->port, port_config->behavior); } else { throw std::runtime_error(std::format( - "(ev-line {}) no server available for connection", this->first_event->line_num)); + "(ev-line {}) No server available for connection", this->first_event->line_num)); } break; } @@ -535,7 +537,7 @@ asio::awaitable ReplaySession::run() { case Event::Type::SEND: if (!c->channel->connected()) { throw std::runtime_error(std::format( - "(ev-line {}) send event attempted on unconnected client", this->first_event->line_num)); + "(ev-line {}) Send event attempted on unconnected client", this->first_event->line_num)); } c->channel->send(this->first_event->data); this->commands_sent++; @@ -544,8 +546,8 @@ asio::awaitable ReplaySession::run() { case Event::Type::RECEIVE: { if (!c->channel->connected()) { - throw std::runtime_error(std::format("(ev-line {}) receive event on non-connected client", - this->first_event->line_num)); + throw std::runtime_error(std::format( + "(ev-line {}) Receive event on non-connected client", this->first_event->line_num)); } if (c->receive_events.front() != this->first_event) { throw std::logic_error("Client receive events are out of order"); @@ -561,25 +563,29 @@ asio::awaitable ReplaySession::run() { this->bytes_received += full_command.size(); if (c->receive_events.empty()) { - phosg::print_data(stderr, full_command, 0, phosg::FormatDataFlags::PRINT_ASCII | phosg::FormatDataFlags::OFFSET_16_BITS); - throw std::runtime_error("received unexpected command for client"); + std::string data_str = phosg::format_data(full_command, 0, phosg::FormatDataFlags::PRINT_ASCII | phosg::FormatDataFlags::OFFSET_16_BITS); + throw std::runtime_error(std::format("Received unexpected command for client:\n{}", data_str)); } auto& ev = c->receive_events.front(); if ((full_command.size() != ev->data.size()) && !ev->allow_size_disparity) { - replay_log.error_f("Expected command:"); - phosg::print_data(stderr, ev->data, 0, phosg::FormatDataFlags::PRINT_ASCII | phosg::FormatDataFlags::OFFSET_16_BITS); - replay_log.error_f("Received command:"); - phosg::print_data(stderr, full_command, 0, phosg::FormatDataFlags::PRINT_ASCII | phosg::FormatDataFlags::OFFSET_16_BITS); - throw std::runtime_error(std::format("(ev-line {}) received command sizes do not match", ev->line_num)); + std::string expected_data = phosg::format_data( + ev->data, 0, phosg::FormatDataFlags::PRINT_ASCII | phosg::FormatDataFlags::OFFSET_16_BITS); + std::string received_data = phosg::format_data( + full_command, 0, phosg::FormatDataFlags::PRINT_ASCII | phosg::FormatDataFlags::OFFSET_16_BITS); + throw std::runtime_error(std::format( + "(ev-line {}) Received command sizes do not match:\nExpected command:\n{}\nReceived command:\n{}", + ev->line_num, expected_data, received_data)); } for (size_t x = 0; x < std::min(full_command.size(), ev->data.size()); x++) { if ((full_command[x] & ev->mask[x]) != (ev->data[x] & ev->mask[x])) { - replay_log.error_f("Expected command:"); - phosg::print_data(stderr, ev->data, 0, phosg::FormatDataFlags::PRINT_ASCII | phosg::FormatDataFlags::OFFSET_16_BITS); - replay_log.error_f("Received command:"); - phosg::print_data(stderr, full_command, 0, ev->data, phosg::FormatDataFlags::USE_COLOR | phosg::FormatDataFlags::PRINT_ASCII | phosg::FormatDataFlags::OFFSET_16_BITS); - throw std::runtime_error(std::format("(ev-line {}) received command data does not match expected data", ev->line_num)); + std::string expected_data = phosg::format_data( + ev->data, 0, phosg::FormatDataFlags::PRINT_ASCII | phosg::FormatDataFlags::OFFSET_16_BITS); + std::string received_data = phosg::format_data( + full_command, 0, ev->data, phosg::FormatDataFlags::USE_COLOR | phosg::FormatDataFlags::PRINT_ASCII | phosg::FormatDataFlags::OFFSET_16_BITS); + throw std::runtime_error(std::format( + "(ev-line {}) Received command does not match expected\nExpected command:\n{}\nReceived command:\n{}", + ev->line_num, expected_data, received_data)); } } @@ -624,18 +630,18 @@ asio::awaitable ReplaySession::run() { // TODO: At some point it may matter which BB private key file we use. Don't just blindly use the // first one here. c->channel->crypt_in = std::make_shared( - *this->state->bb_private_keys[0], cmd.server_key.data(), cmd.server_key.size()); + *this->state->data->bb_private_keys[0], cmd.server_key.data(), cmd.server_key.size()); c->channel->crypt_out = std::make_shared( - *this->state->bb_private_keys[0], cmd.client_key.data(), cmd.client_key.size()); + *this->state->data->bb_private_keys[0], cmd.client_key.data(), cmd.client_key.size()); } break; default: - throw std::logic_error("unsupported encryption version"); + throw std::logic_error("Unsupported encryption version"); } break; } default: - throw std::logic_error("unhandled event type"); + throw std::logic_error("Unhandled event type"); } this->first_event->complete = true; } @@ -646,13 +652,12 @@ asio::awaitable ReplaySession::run() { } } } catch (const std::exception& e) { - replay_log.error_f("Replay failed: {}", e.what()); + this->failure = std::format("Replay failed: {}", e.what()); if (this->first_event) { - replay_log.error_f("Next pending event: {}", this->first_event->str()); + this->failure += std::format("\nNext pending event: {}", this->first_event->str()); } else { - replay_log.error_f("No events are pending at failure time"); + this->failure += std::format("\nNo events are pending at failure time"); } - this->run_failed = true; } for (auto& [_, c] : this->clients) { @@ -660,14 +665,18 @@ asio::awaitable ReplaySession::run() { c->channel->disconnect(); } } - this->state->use_psov2_rand_crypt = this->prev_psov2_crypt_enabled; - if (!this->run_failed) { + this->state->use_psov2_rand_crypt = prev_use_psov2_rand_crypt; + this->state->use_legacy_item_random_behavior = prev_use_legacy_item_random_behavior; + + if (this->failure.empty()) { // Wait a bit longer to ensure that any command sent at the end of the replay session don't crash the server co_await async_sleep(std::chrono::seconds(2)); replay_log.info_f("Replay complete: {} commands sent ({} bytes), {} commands received ({} bytes)", this->commands_sent, this->bytes_sent, this->commands_received, this->bytes_received); } + + this->idle_timeout_timer.cancel(); } void ReplaySession::reschedule_idle_timeout() { diff --git a/src/ReplaySession.hh b/src/ReplaySession.hh index 917fe2bc..515981de 100644 --- a/src/ReplaySession.hh +++ b/src/ReplaySession.hh @@ -21,8 +21,9 @@ public: ~ReplaySession() = default; asio::awaitable run(); - inline bool failed() const { - return this->run_failed; + + inline std::string failure_str() const { + return this->failure; } private: @@ -62,7 +63,8 @@ private: }; std::shared_ptr state; - bool prev_psov2_crypt_enabled; + bool use_psov2_rand_crypt = false; + bool use_legacy_item_random_behavior = false; std::unordered_map> clients; @@ -75,7 +77,7 @@ private: size_t bytes_received; asio::steady_timer idle_timeout_timer; - bool run_failed; + std::string failure; std::shared_ptr create_event(Event::Type type, std::shared_ptr c, size_t line_num); diff --git a/src/SendCommands.cc b/src/SendCommands.cc index a2742289..5c379600 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -232,7 +232,7 @@ void send_server_init_bb(std::shared_ptr c, uint8_t flags) { send_command_t(c, use_secondary_message ? 0x9B : 0x03, 0x00, cmd); c->bb_detector_crypt = std::make_shared( - c->require_server_state()->bb_private_keys, + c->require_server_state()->data->bb_private_keys, bb_crypt_initial_client_commands, cmd.basic_cmd.client_key.data(), sizeof(cmd.basic_cmd.client_key)); @@ -336,7 +336,7 @@ asio::awaitable prepare_client_for_patches(std::shared_ptr c) { auto s = c->require_server_state(); if (!c->check_flag(Client::Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH)) { - auto fn = s->client_functions->get("CacheClearFix-Phase1", ClientFunctionIndex::Function::Architecture::POWERPC); + auto fn = s->data->client_functions->get("CacheClearFix-Phase1", ClientFunctionIndex::Function::Architecture::POWERPC); std::unordered_map label_writes; auto call1_res = co_await send_function_call(c, fn, label_writes, nullptr, 0, 0x80000000, 8, 0x7F2734EC); try { @@ -345,7 +345,7 @@ asio::awaitable prepare_client_for_patches(std::shared_ptr c) { } catch (const std::out_of_range&) { c->log.info_f("Could not detect specific version from header checksum {:08X}", call1_res.checksum); } - co_await send_function_call(c, s->client_functions->get("CacheClearFix-Phase2", ClientFunctionIndex::Function::Architecture::POWERPC)); + co_await send_function_call(c, s->data->client_functions->get("CacheClearFix-Phase2", ClientFunctionIndex::Function::Architecture::POWERPC)); c->log.info_f("Client cache behavior patched"); c->set_flag(Client::Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH); } @@ -360,7 +360,7 @@ asio::awaitable prepare_client_for_patches(std::shared_ptr c) { } if ((arch != ClientFunctionIndex::Function::Architecture::UNKNOWN) && specific_version_is_indeterminate(c->specific_version)) { - auto vers_detect_res = co_await send_function_call(c, s->client_functions->get("VersionDetect", arch)); + auto vers_detect_res = co_await send_function_call(c, s->data->client_functions->get("VersionDetect", arch)); c->specific_version = vers_detect_res.return_value; c->log.info_f("Version detected as {:08X}", c->specific_version); } @@ -517,7 +517,7 @@ asio::awaitable send_protected_command(std::shared_ptr c, const vo case Version::GC_EP3: case Version::BB_V4: { auto s = c->require_server_state(); - if (!s->enable_v3_v4_protected_subcommands || + if (!s->data->enable_v3_v4_protected_subcommands || !c->check_flag(Client::Flag::HAS_SEND_FUNCTION_CALL) || !c->check_flag(Client::Flag::SEND_FUNCTION_CALL_ACTUALLY_RUNS_CODE)) { co_return false; @@ -526,7 +526,7 @@ asio::awaitable send_protected_command(std::shared_ptr c, const vo co_await prepare_client_for_patches(c); try { - auto fn = s->client_functions->get("CallProtectedHandler", c->specific_version); + auto fn = s->data->client_functions->get("CallProtectedHandler", c->specific_version); std::unordered_map label_writes{{"size", size}}; co_await send_function_call(c, fn, label_writes, data, size); auto l = echo_to_lobby ? c->lobby.lock() : nullptr; @@ -550,7 +550,7 @@ asio::awaitable send_dol_file(std::shared_ptr c, std::shared_ptr label_writes{{"address", 0x80000034}}; // ArenaHigh from GC globals auto addr_ret = co_await send_function_call( - c, s->client_functions->get("ReadMemoryWord", c->specific_version), label_writes); + c, s->data->client_functions->get("ReadMemoryWord", c->specific_version), label_writes); uint32_t dol_base_addr = (addr_ret.return_value - dol->data.size()) & (~3); // Write the file in multiple chunks @@ -562,7 +562,7 @@ asio::awaitable send_dol_file(std::shared_ptr c, std::shared_ptrdata.substr(offset, bytes_to_send); auto s = c->require_server_state(); - auto fn = s->client_functions->get("WriteMemory", c->specific_version); + auto fn = s->data->client_functions->get("WriteMemory", c->specific_version); label_writes = {{"dest_addr", (dol_base_addr + offset)}, {"size", bytes_to_send}}; co_await send_function_call(c, fn, label_writes, data_to_send.data(), data_to_send.size()); @@ -573,7 +573,7 @@ asio::awaitable send_dol_file(std::shared_ptr c, std::shared_ptrclient_functions->get("RunDOL", c->specific_version); + auto fn = s->data->client_functions->get("RunDOL", c->specific_version); label_writes = {{"dol_base_ptr", dol_base_addr}}; co_await send_function_call(c, fn, label_writes); // The client will stop running PSO after this, so disconnect them @@ -700,7 +700,7 @@ void send_stream_file_index_bb(std::shared_ptr c) { auto s = c->require_server_state(); std::vector entries; - for (const auto& sf_entry : s->bb_stream_file->entries) { + for (const auto& sf_entry : s->data->bb_stream_file->entries) { auto& e = entries.emplace_back(); e.size = sf_entry.size; e.checksum = sf_entry.checksum; @@ -716,11 +716,11 @@ void send_stream_file_chunk_bb(std::shared_ptr c, uint32_t chunk_index) S_StreamFileChunk_BB_02EB chunk_cmd; chunk_cmd.chunk_index = chunk_index; size_t offset = sizeof(chunk_cmd.data) * chunk_index; - if (offset > s->bb_stream_file->data.size()) { + if (offset > s->data->bb_stream_file->data.size()) { throw std::runtime_error("client requested chunk beyond end of stream file"); } - size_t bytes = std::min(s->bb_stream_file->data.size() - offset, sizeof(chunk_cmd.data)); - chunk_cmd.data.assign_range(reinterpret_cast(s->bb_stream_file->data.data() + offset), bytes, 0); + size_t bytes = std::min(s->data->bb_stream_file->data.size() - offset, sizeof(chunk_cmd.data)); + chunk_cmd.data.assign_range(reinterpret_cast(s->data->bb_stream_file->data.data() + offset), bytes, 0); size_t cmd_size = offsetof(S_StreamFileChunk_BB_02EB, data) + bytes; cmd_size = (cmd_size + 3) & ~3; @@ -1183,17 +1183,17 @@ void send_card_search_result_t(std::shared_ptr c, std::shared_ptrconnect_address_for_client(c); - cmd.reconnect_command.port = s->game_server_port_for_version(c->version()); + cmd.reconnect_command.address = s->data->connect_address_for_client(c); + cmd.reconnect_command.port = s->data->game_server_port_for_version(c->version()); cmd.reconnect_command.unused = 0; std::string location_string; if (result_lobby->is_game()) { - location_string = std::format("{},,BLOCK01,{}", result_lobby->name, s->name); + location_string = std::format("{},,BLOCK01,{}", result_lobby->name, s->data->name); } else if (result_lobby->is_ep3()) { - location_string = std::format("BLOCK01-C{:02},,BLOCK01,{}", result_lobby->lobby_id - 15, s->name); + location_string = std::format("BLOCK01-C{:02},,BLOCK01,{}", result_lobby->lobby_id - 15, s->data->name); } else { - location_string = std::format("BLOCK01-{:02},,BLOCK01,{}", result_lobby->lobby_id, s->name); + location_string = std::format("BLOCK01-{:02},,BLOCK01,{}", result_lobby->lobby_id, s->data->name); } cmd.location_string.encode(location_string, c->language()); cmd.extension.lobby_refs[0].menu_id = MenuID::LOBBY; @@ -1467,7 +1467,7 @@ void send_game_menu_t(std::shared_ptr c, bool is_spectator_team_list, bo e.item_id = 0x00000000; e.difficulty_tag = 0x00; e.num_players = 0x00; - e.name.encode(s->name, c->language()); + e.name.encode(s->data->name, c->language()); e.episode = 0x00; e.flags = 0x04; } @@ -1623,7 +1623,7 @@ void send_quest_categories_menu_t(std::shared_ptr c, QuestMenuType menu_ std::vector entries; auto s = c->require_server_state(); - for (const auto& cat : s->quest_index->categories(menu_type, episode, version_flags, include_condition)) { + for (const auto& cat : s->data->quest_index->categories(menu_type, episode, version_flags, include_condition)) { auto& e = entries.emplace_back(); e.menu_id = cat->use_ep2_icon() ? MenuID::QUEST_CATEGORIES_EP2 : MenuID::QUEST_CATEGORIES_EP1_EP3_EP4; e.item_id = cat->category_id; @@ -1646,7 +1646,7 @@ void send_ep3_download_quest_categories_menu(std::shared_ptr c) { std::vector entries; auto s = c->require_server_state(); - for (const auto& [_, cat] : s->ep3_map_index->all_categories()) { + for (const auto& [_, cat] : s->data->ep3_map_index->all_categories()) { if (cat->check_visibility_flag(vis_flag)) { auto& e = entries.emplace_back(); e.menu_id = MenuID::QUEST_CATEGORIES_EP1_EP3_EP4; @@ -1669,7 +1669,7 @@ void send_ep3_download_quest_menu(std::shared_ptr c, uint32_t category_i : Episode3::MapIndex::VisibilityFlag::ONLINE_FINAL; auto s = c->require_server_state(); - auto category = s->ep3_map_index->category_for_id(category_id); + auto category = s->data->ep3_map_index->category_for_id(category_id); if (!category->check_visibility_flag(vis_flag)) { throw std::runtime_error("category is not visible to this client"); } @@ -1873,7 +1873,7 @@ static void send_join_spectator_team(std::shared_ptr c, std::shared_ptr< auto& p = cmd.players[z]; populate_lobby_data_for_client(p.lobby_data, wc, c); p.inventory = wc_p->inventory; - p.inventory.encode_for_client(c->version(), s->item_parameter_table_for_encode(c->version())); + p.inventory.encode_for_client(c->version(), s->data->item_parameter_table_for_encode(c->version())); p.disp = wc_p->disp.to_v123(c->language(), p.inventory.language); p.disp.enforce_lobby_join_limits_for_version(c->version()); @@ -1887,7 +1887,7 @@ static void send_join_spectator_team(std::shared_ptr c, std::shared_ptr< : wc_p->disp.stats.level.load(); e.name_color = wc_p->disp.visual.sh.name_color; - uint32_t name_color = s->name_color_for_client(wc); + uint32_t name_color = s->data->name_color_for_client(wc); if (name_color) { p.disp.visual.sh.name_color = name_color; e.name_color = name_color; @@ -1917,7 +1917,7 @@ static void send_join_spectator_team(std::shared_ptr c, std::shared_ptr< auto& p = cmd.players[client_id]; p.lobby_data = entry.lobby_data; p.inventory = entry.inventory; - p.inventory.encode_for_client(c->version(), s->item_parameter_table_for_encode(c->version())); + p.inventory.encode_for_client(c->version(), s->data->item_parameter_table_for_encode(c->version())); p.disp = entry.disp; p.disp.enforce_lobby_join_limits_for_version(c->version()); @@ -1956,7 +1956,7 @@ static void send_join_spectator_team(std::shared_ptr c, std::shared_ptr< : other_p->disp.stats.level.load(); cmd_e.name_color = other_p->disp.visual.sh.name_color; - uint32_t name_color = s->name_color_for_client(other_c); + uint32_t name_color = s->data->name_color_for_client(other_c); if (name_color) { cmd_p.disp.visual.sh.name_color = name_color; cmd_e.name_color = name_color; @@ -2066,11 +2066,11 @@ void send_join_game(std::shared_ptr c, std::shared_ptr l) { auto other_p = lc->character_file(); auto& cmd_p = cmd.players_ep3[x]; cmd_p.inventory = other_p->inventory; - cmd_p.inventory.encode_for_client(c->version(), s->item_parameter_table_for_encode(c->version())); + cmd_p.inventory.encode_for_client(c->version(), s->data->item_parameter_table_for_encode(c->version())); cmd_p.disp = convert_player_disp_data( other_p->disp, c->language(), other_p->inventory.language); cmd_p.disp.enforce_lobby_join_limits_for_version(c->version()); - uint32_t name_color = s->name_color_for_client(lc); + uint32_t name_color = s->data->name_color_for_client(lc); if (name_color) { cmd_p.disp.visual.sh.name_color = name_color; } @@ -2180,13 +2180,13 @@ void send_join_lobby_t(std::shared_ptr c, std::shared_ptr l, std: auto& e = cmd.entries[used_entries++]; populate_lobby_data_for_client(e.lobby_data, lc, c); e.inventory = lp->inventory; - e.inventory.encode_for_client(c->version(), s->item_parameter_table_for_encode(c->version())); + e.inventory.encode_for_client(c->version(), s->data->item_parameter_table_for_encode(c->version())); if ((lc == c) && is_v1_or_v2(c->version()) && lc->v1_v2_last_reported_disp) { e.disp = convert_player_disp_data(*lc->v1_v2_last_reported_disp, c->language(), lp->inventory.language); } else { e.disp = convert_player_disp_data(lp->disp, c->language(), lp->inventory.language); e.disp.enforce_lobby_join_limits_for_version(c->version()); - uint32_t name_color = s->name_color_for_client(lc); + uint32_t name_color = s->data->name_color_for_client(lc); if (name_color) { e.disp.visual.sh.name_color = name_color; if (is_v1_or_v2(c->version())) { @@ -2253,10 +2253,10 @@ void send_join_lobby_xb(std::shared_ptr c, std::shared_ptr l, std auto& e = cmd.entries[used_entries++]; populate_lobby_data_for_client(e.lobby_data, lc, c); e.inventory = lp->inventory; - e.inventory.encode_for_client(c->version(), s->item_parameter_table_for_encode(c->version())); + e.inventory.encode_for_client(c->version(), s->data->item_parameter_table_for_encode(c->version())); e.disp = convert_player_disp_data(lp->disp, c->language(), lp->inventory.language); e.disp.enforce_lobby_join_limits_for_version(c->version()); - uint32_t name_color = s->name_color_for_client(lc); + uint32_t name_color = s->data->name_color_for_client(lc); if (name_color) { e.disp.visual.sh.name_color = name_color; } @@ -2301,13 +2301,13 @@ void send_join_lobby_dc_nte(std::shared_ptr c, std::shared_ptr l, auto& e = cmd.entries[used_entries++]; populate_lobby_data_for_client(e.lobby_data, lc, c); e.inventory = lp->inventory; - e.inventory.encode_for_client(c->version(), s->item_parameter_table_for_encode(c->version())); + e.inventory.encode_for_client(c->version(), s->data->item_parameter_table_for_encode(c->version())); if ((lc == c) && is_v1_or_v2(c->version()) && lc->v1_v2_last_reported_disp) { e.disp = convert_player_disp_data(*lc->v1_v2_last_reported_disp, c->language(), lp->inventory.language); } else { e.disp = convert_player_disp_data(lp->disp, c->language(), lp->inventory.language); e.disp.enforce_lobby_join_limits_for_version(c->version()); - uint32_t name_color = s->name_color_for_client(lc); + uint32_t name_color = s->data->name_color_for_client(lc); if (name_color) { e.disp.visual.sh.name_color = name_color; e.disp.visual.sh.compute_name_color_checksum(); @@ -2459,7 +2459,7 @@ asio::awaitable send_get_player_info(std::shared_ptrrequire_server_state(); - auto fn = s->client_functions->get("GetExtendedPlayerInfo", c->specific_version); + auto fn = s->data->client_functions->get("GetExtendedPlayerInfo", c->specific_version); send_function_call(c->channel, c->enabled_flags, fn); c->function_call_response_queue.emplace_back(std::make_shared>()); full_req_sent = true; @@ -2486,7 +2486,7 @@ void send_execute_item_trade(std::shared_ptr c, const std::vectorlobby_client_id; cmd.item_count = items.size(); - auto item_parameter_table = s->item_parameter_table_for_encode(c->version()); + auto item_parameter_table = s->data->item_parameter_table_for_encode(c->version()); for (size_t x = 0; x < items.size(); x++) { cmd.item_datas[x] = items[x]; cmd.item_datas[x].encode_for_version(c->version(), item_parameter_table); @@ -2767,7 +2767,7 @@ void send_game_item_state(std::shared_ptr c) { fi.room_id = 0; fi.drop_number = (floor == 0) ? 0xFFFF : (decompressed_header.next_drop_number_per_floor.at(floor - 1)++); fi.item = item->data; - fi.item.encode_for_version(c->version(), s->item_parameter_table_for_encode(c->version())); + fi.item.encode_for_version(c->version(), s->data->item_parameter_table_for_encode(c->version())); floor_items_w.put(fi); decompressed_header.floor_item_count_per_floor.at(floor)++; @@ -2792,7 +2792,7 @@ void send_game_item_state(std::shared_ptr c) { } uint8_t subcommand = get_pre_v1_subcommand(c->version(), 0x4F, 0x56, 0x5D); G_DropStackedItem_PC_V3_BB_6x5D cmd = {{{subcommand, 0x0A, 0x0000}, floor, 0, item->pos, item->data}, 0}; - cmd.item_data.encode_for_version(c->version(), s->item_parameter_table_for_encode(c->version())); + cmd.item_data.encode_for_version(c->version(), s->data->item_parameter_table_for_encode(c->version())); w.put(cmd); } } @@ -3080,7 +3080,7 @@ void send_drop_item_to_channel( uint8_t subcommand = get_pre_v1_subcommand(ch->version, 0x51, 0x58, 0x5F); G_DropItem_PC_V3_BB_6x5F cmd = { {{subcommand, 0x0B, 0x0000}, {floor, source_type, entity_index, pos, 0, 0, item}}, 0}; - cmd.item.item.encode_for_version(ch->version, s->item_parameter_table_for_encode(ch->version)); + cmd.item.item.encode_for_version(ch->version, s->data->item_parameter_table_for_encode(ch->version)); ch->send(0x60, 0x00, &cmd, sizeof(cmd)); } } @@ -3093,7 +3093,7 @@ void send_drop_stacked_item_to_channel( const VectorXZF& pos) { uint8_t subcommand = get_pre_v1_subcommand(ch->version, 0x4F, 0x56, 0x5D); G_DropStackedItem_PC_V3_BB_6x5D cmd = {{{subcommand, 0x0A, 0x0000}, floor, 0, pos, item}, 0}; - cmd.item_data.encode_for_version(ch->version, s->item_parameter_table_for_encode(ch->version)); + cmd.item_data.encode_for_version(ch->version, s->data->item_parameter_table_for_encode(ch->version)); ch->send(0x60, 0x00, &cmd, sizeof(cmd)); } @@ -3303,8 +3303,8 @@ void send_ep3_card_list_update(std::shared_ptr c) { if (!c->check_flag(Client::Flag::HAS_EP3_CARD_DEFS)) { auto s = c->require_server_state(); const auto& data = (c->version() == Version::GC_EP3_NTE) - ? s->ep3_card_index_trial->get_compressed_definitions() - : s->ep3_card_index->get_compressed_definitions(); + ? s->data->ep3_card_index_trial->get_compressed_definitions() + : s->data->ep3_card_index->get_compressed_definitions(); phosg::StringWriter w; w.put_u32l(data.size()); @@ -3326,8 +3326,8 @@ void send_ep3_media_update(std::shared_ptr c, uint32_t type, uint32_t wh void send_ep3_rank_update(std::shared_ptr c) { auto s = c->require_server_state(); - uint32_t current_meseta = s->ep3_infinite_meseta ? 1000000 : c->login->account->ep3_current_meseta; - uint32_t total_meseta_earned = s->ep3_infinite_meseta ? 1000000 : c->login->account->ep3_total_meseta_earned; + uint32_t current_meseta = s->data->ep3_infinite_meseta ? 1000000 : c->login->account->ep3_current_meseta; + uint32_t total_meseta_earned = s->data->ep3_infinite_meseta ? 1000000 : c->login->account->ep3_total_meseta_earned; S_RankUpdate_Ep3_B7 cmd = {0, {}, current_meseta, total_meseta_earned, 0xFFFFFFFF}; send_command_t(c, 0xB7, 0x00, cmd); } @@ -3383,7 +3383,7 @@ void send_ep3_confirm_tournament_entry( if (tourn) { auto s = c->require_server_state(); cmd.tournament_name.encode(tourn->get_name(), c->language()); - cmd.server_name.encode(s->name, c->language()); + cmd.server_name.encode(s->data->name, c->language()); // TODO: Fill this in appropriately when we support scheduled start times cmd.start_time.encode("Unknown", c->language()); auto& teams = tourn->all_teams(); @@ -3690,7 +3690,7 @@ void send_ep3_set_tournament_player_decks_t(std::shared_ptr c) { add_entries_for_team(match->preceding_b->winner_team, 2); if ((c->version() != Version::GC_EP3_NTE) && - !(s->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) { + !(s->data->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) { uint8_t mask_key = (phosg::random_object() % 0xFF) + 1; set_mask_for_ep3_game_command(&cmd, sizeof(cmd), mask_key); } @@ -3751,14 +3751,14 @@ void send_ep3_tournament_match_result(std::shared_ptr l, uint32_t meseta_ cmd.winner_team_id = (match->preceding_b->winner_team == match->winner_team); cmd.meseta_amount = meseta_reward; cmd.meseta_reward_text.encode("You got %s meseta!", Language::ENGLISH); - if ((lc->version() != Version::GC_EP3_NTE) && !(s->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) { + if ((lc->version() != Version::GC_EP3_NTE) && !(s->data->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) { uint8_t mask_key = (phosg::random_object() % 0xFF) + 1; set_mask_for_ep3_game_command(&cmd, sizeof(cmd), mask_key); } send_command_t(lc, 0xC9, 0x00, cmd); } - if (s->ep3_behavior_flags & Episode3::BehaviorFlag::ENABLE_STATUS_MESSAGES) { + if (s->data->ep3_behavior_flags & Episode3::BehaviorFlag::ENABLE_STATUS_MESSAGES) { send_text_message_fmt(l, "$C5TOURN/{:X}/{} WIN {}", tourn->get_menu_item_id(), match->round_num, match->winner_team == match->preceding_a->winner_team ? 'A' : 'B'); @@ -3788,7 +3788,7 @@ void send_ep3_update_game_metadata(std::shared_ptr l) { cmd.total_spectators = total_spectators; for (auto c : l->clients) { if (c) { - if ((c->version() == Version::GC_EP3) && !(s->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) { + if ((c->version() == Version::GC_EP3) && !(s->data->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) { G_SetGameMetadata_Ep3_6xB4x52 masked_cmd = cmd; uint8_t mask_key = (phosg::random_object() % 0xFF) + 1; set_mask_for_ep3_game_command(&masked_cmd, sizeof(masked_cmd), mask_key); @@ -3823,7 +3823,7 @@ void send_ep3_update_game_metadata(std::shared_ptr l) { cmd.text.encode(text, Language::ENGLISH); for (auto c : watcher_l->clients) { if (c) { - if ((c->version() == Version::GC_EP3) && !(s->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) { + if ((c->version() == Version::GC_EP3) && !(s->data->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) { G_SetGameMetadata_Ep3_6xB4x52 masked_cmd = cmd; uint8_t mask_key = (phosg::random_object() % 0xFF) + 1; set_mask_for_ep3_game_command(&masked_cmd, sizeof(masked_cmd), mask_key); @@ -3890,7 +3890,7 @@ void send_quest_file_chunk( c->log.info_f("Sending quest file chunk {}:{}", filename, chunk_index); const auto& s = c->require_server_state(); - c->channel->send(is_download_quest ? 0xA7 : 0x13, chunk_index, &cmd, sizeof(cmd), s->hide_download_commands); + c->channel->send(is_download_quest ? 0xA7 : 0x13, chunk_index, &cmd, sizeof(cmd), s->data->hide_download_commands); } template @@ -4080,33 +4080,33 @@ bool send_ep3_start_tournament_deck_select_if_all_clients_ready(std::shared_ptr< void send_ep3_card_auction(std::shared_ptr l) { auto s = l->require_server_state(); - if ((s->ep3_card_auction_points == 0) || - (s->ep3_card_auction_min_size == 0) || - (s->ep3_card_auction_max_size == 0)) { + if ((s->data->ep3_card_auction_points == 0) || + (s->data->ep3_card_auction_min_size == 0) || + (s->data->ep3_card_auction_max_size == 0)) { throw std::runtime_error("card auctions are not configured on this server"); } uint16_t num_cards; - if (s->ep3_card_auction_min_size == s->ep3_card_auction_max_size) { - num_cards = s->ep3_card_auction_min_size; + if (s->data->ep3_card_auction_min_size == s->data->ep3_card_auction_max_size) { + num_cards = s->data->ep3_card_auction_min_size; } else { - num_cards = s->ep3_card_auction_min_size + - (phosg::random_object() % (s->ep3_card_auction_max_size - s->ep3_card_auction_min_size + 1)); + num_cards = s->data->ep3_card_auction_min_size + + (phosg::random_object() % (s->data->ep3_card_auction_max_size - s->data->ep3_card_auction_min_size + 1)); } num_cards = std::min(num_cards, 0x14); - auto card_index = l->is_ep3_nte() ? s->ep3_card_index_trial : s->ep3_card_index; + auto card_index = l->is_ep3_nte() ? s->data->ep3_card_index_trial : s->data->ep3_card_index; uint64_t distribution_size = 0; - for (const auto& e : s->ep3_card_auction_pool) { + for (const auto& e : s->data->ep3_card_auction_pool) { distribution_size += e.probability; } S_StartCardAuction_Ep3_EF cmd; - cmd.points_available = s->ep3_card_auction_points; + cmd.points_available = s->data->ep3_card_auction_points; for (size_t z = 0; z < num_cards; z++) { uint64_t v = phosg::random_object() % distribution_size; - for (const auto& e : s->ep3_card_auction_pool) { + for (const auto& e : s->data->ep3_card_auction_pool) { if (v >= e.probability) { v -= e.probability; } else { diff --git a/src/ServerState.cc b/src/ServerState.cc index 44f00d67..ebbdfac4 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -19,64 +19,38 @@ #include "Text.hh" #include "TextIndex.hh" -#ifdef PHOSG_WINDOWS -static constexpr bool IS_WINDOWS = true; -#else -static constexpr bool IS_WINDOWS = false; -#endif - -CheatFlags::CheatFlags(const phosg::JSON& json) : CheatFlags() { - std::unordered_set enabled_keys; - for (const auto& it : json.as_list()) { - enabled_keys.emplace(it->as_string()); +std::shared_ptr ServerState::create_shared(std::shared_ptr data, bool is_replay) { + std::shared_ptr s(new ServerState()); + s->data = data; + s->io_context = std::make_shared(1); + if (s->data->num_worker_threads > 0) { + config_log.info_f("Starting thread pool with {} threads", s->data->num_worker_threads); + s->thread_pool = std::make_shared(s->data->num_worker_threads); + } else { + config_log.warning_f("WorkerThreads is zero or not set; using default thread count"); + s->thread_pool = std::make_shared(); } - - this->create_items = enabled_keys.count("CreateItems"); - this->edit_section_id = enabled_keys.count("EditSectionID"); - this->edit_stats = enabled_keys.count("EditStats"); - this->ep3_replace_assist = enabled_keys.count("Ep3ReplaceAssist"); - this->ep3_unset_field_character = enabled_keys.count("Ep3UnsetFieldCharacter"); - this->infinite_hp_tp = enabled_keys.count("InfiniteHPTP"); - this->fast_kills = enabled_keys.count("FastKills"); - this->insufficient_minimum_level = enabled_keys.count("InsufficientMinimumLevel"); - this->override_random_seed = enabled_keys.count("OverrideRandomSeed"); - this->override_section_id = enabled_keys.count("OverrideSectionID"); - this->override_variations = enabled_keys.count("OverrideVariations"); - this->proxy_override_drops = enabled_keys.count("ProxyOverrideDrops"); - this->reset_materials = enabled_keys.count("ResetMaterials"); - this->warp = enabled_keys.count("Warp"); + s->is_replay = is_replay; + s->load_accounts(); + s->load_ep3_tournament_state(); + s->create_default_lobbies(); + s->load_teams(); + return s; } -ServerState::QuestF960Result::QuestF960Result( - const phosg::JSON& json, std::shared_ptr name_index, const ItemData::StackLimits& limits) { - static const std::array day_names = { - "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; - this->meseta_cost = json.get_int("MesetaCost", 0); - this->base_probability = json.get_int("BaseProbability", 0); - this->probability_upgrade = json.get_int("ProbabilityUpgrade", 0); - for (size_t day = 0; day < 7; day++) { - for (const auto& item_it : json.get_list(day_names[day])) { - if (item_it->is_int()) { - this->results[day].emplace_back(ItemData::from_primary_identifier(limits, item_it->as_int())); - } else { - try { - this->results[day].emplace_back(name_index->parse_item_description(item_it->as_string())); - } catch (const std::exception& e) { - config_log.warning_f( - "Cannot parse item description \"{}\": {} (skipping entry)", item_it->as_string(), e.what()); - } - } - } - } +std::shared_ptr ServerState::clone_shared() { + std::shared_ptr ret(new ServerState()); + ret->data = this->data; + ret->io_context = this->io_context; + ret->thread_pool = this->thread_pool; + ret->is_replay = this->is_replay; + ret->load_accounts(); + ret->load_ep3_tournament_state(); + ret->create_default_lobbies(); + ret->load_teams(); + return ret; } -ServerState::ServerState(const std::string& config_filename, bool is_replay) - : creation_time(phosg::now()), - io_context(std::make_shared(1)), - config_filename(config_filename), - is_replay(is_replay), - thread_pool(std::make_unique()) {} - void ServerState::add_client_to_available_lobby(std::shared_ptr c, bool allow_games) { std::shared_ptr added_to_lobby; @@ -112,7 +86,7 @@ void ServerState::add_client_to_available_lobby(std::shared_ptr c, bool } if (!added_to_lobby) { - for (const auto& lobby_id : this->public_lobby_search_order(c)) { + for (const auto& lobby_id : this->data->public_lobby_search_order(c)) { added_to_lobby = try_join_lobby(lobby_id); if (added_to_lobby) { break; @@ -127,7 +101,7 @@ void ServerState::add_client_to_available_lobby(std::shared_ptr c, bool added_to_lobby->block = 100; added_to_lobby->name = "Overflow"; added_to_lobby->max_clients = 12; - added_to_lobby->event = this->pre_lobby_event; + added_to_lobby->event = this->data->pre_lobby_event; added_to_lobby->allow_version(c->version()); added_to_lobby->add_client(c); } @@ -231,7 +205,7 @@ std::shared_ptr ServerState::create_lobby(bool is_game) { } auto l = std::make_shared(this->shared_from_this(), this->next_lobby_id++, is_game); this->id_to_lobby.emplace(l->lobby_id, l); - l->idle_timeout_usecs = this->persistent_game_idle_timeout_usecs; + l->idle_timeout_usecs = this->data->persistent_game_idle_timeout_usecs; return l; } @@ -300,1311 +274,6 @@ std::shared_ptr ServerState::find_client(const std::string* identifier, throw std::out_of_range("client not found"); } -uint32_t ServerState::connect_address_for_client(std::shared_ptr c) const { - { - auto ipss_channel = dynamic_pointer_cast(c->channel); - if (ipss_channel) { - auto ipss_c = ipss_channel->ipss_client.lock(); - if (!ipss_c) { - throw std::runtime_error("IPSS client is expired"); - } - return IPStackSimulator::connect_address_for_remote_address(ipss_c->ipv4_addr); - } - } - - { - auto socket_channel = dynamic_pointer_cast(c->channel); - if (socket_channel) { - uint32_t addr = ipv4_addr_for_asio_addr(socket_channel->remote_addr.address()); - uint32_t ret = is_local_address(addr) ? this->local_address : this->external_address; - return ret ? ret : addr; - } - } - - { - auto peer_channel = dynamic_pointer_cast(c->channel); - if (peer_channel) { - // This is used during replays; the "client" will ignore this and reconnect via another PeerChannel - return 0xEEEEEEEE; - } - } - - throw std::runtime_error("no connect address available"); -} - -uint16_t ServerState::game_server_port_for_version(Version v) const { - switch (v) { - case Version::DC_NTE: - case Version::DC_11_2000: - case Version::DC_V1: - case Version::DC_V2: - case Version::GC_NTE: - case Version::GC_V3: - case Version::GC_EP3_NTE: - case Version::GC_EP3: - return this->name_to_port_config.at("gc-us3")->port; - case Version::PC_NTE: - case Version::PC_V2: - return this->name_to_port_config.at("pc")->port; - case Version::XB_V3: - return this->name_to_port_config.at("xb")->port; - case Version::BB_V4: - return this->name_to_port_config.at("bb-data1")->port; - default: - throw std::runtime_error("unknown version"); - } -} - -std::shared_ptr ServerState::information_menu(Version version) const { - if (is_v1_or_v2(version)) { - return this->information_menu_v2; - } else if (is_v3(version)) { - return this->information_menu_v3; - } - throw std::out_of_range("no information menu exists for this version"); -} - -std::shared_ptr ServerState::proxy_destinations_menu(Version version) const { - switch (version) { - case Version::DC_NTE: - case Version::DC_11_2000: - case Version::DC_V1: - case Version::DC_V2: - return this->proxy_destinations_menu_dc; - case Version::PC_NTE: - case Version::PC_V2: - return this->proxy_destinations_menu_pc; - case Version::GC_NTE: - case Version::GC_V3: - case Version::GC_EP3_NTE: - case Version::GC_EP3: - return this->proxy_destinations_menu_gc; - case Version::XB_V3: - return this->proxy_destinations_menu_xb; - default: - throw std::out_of_range("no proxy destinations menu exists for this version"); - } -} - -const std::vector>& ServerState::proxy_destinations(Version version) const { - switch (version) { - case Version::DC_NTE: - case Version::DC_11_2000: - case Version::DC_V1: - case Version::DC_V2: - case Version::GC_NTE: - return this->proxy_destinations_dc; - case Version::PC_NTE: - case Version::PC_V2: - return this->proxy_destinations_pc; - case Version::GC_V3: - case Version::GC_EP3_NTE: - case Version::GC_EP3: - return this->proxy_destinations_gc; - case Version::XB_V3: - return this->proxy_destinations_xb; - default: - throw std::out_of_range("no proxy destinations menu exists for this version"); - } -} - -const std::vector& ServerState::public_lobby_search_order(Version version, bool is_client_customization) const { - static_assert(NUM_VERSIONS == 14, "Don\'t forget to update the public lobby search orders in config.json"); - if (is_client_customization && !this->client_customization_public_lobby_search_order.empty()) { - return this->client_customization_public_lobby_search_order; - } - return this->public_lobby_search_orders.at(static_cast(version)); -} - -std::shared_ptr> ServerState::information_contents_for_client(std::shared_ptr c) const { - return is_v1_or_v2(c->version()) ? this->information_contents_v2 : this->information_contents_v3; -} - -size_t ServerState::default_min_level_for_game(Version version, Episode episode, Difficulty difficulty) const { - const auto& min_levels = is_v4(version) - ? this->min_levels_v4 - : is_v3(version) - ? this->min_levels_v3 - : this->min_levels_v1_v2; - switch (episode) { - case Episode::EP1: - return min_levels[0].at(static_cast(difficulty)); - case Episode::EP2: - return min_levels[1].at(static_cast(difficulty)); - case Episode::EP3: - return 0; - case Episode::EP4: - return min_levels[2].at(static_cast(difficulty)); - default: - throw std::runtime_error("invalid episode"); - } -} - -std::shared_ptr ServerState::set_data_table( - Version version, Episode episode, GameMode mode, Difficulty difficulty) const { - bool use_ult_tables = ((episode == Episode::EP1) && (difficulty == Difficulty::ULTIMATE) && !is_v1(version) && (version != Version::PC_NTE)); - if (mode == GameMode::SOLO && is_v4(version)) { - return use_ult_tables ? this->bb_solo_set_data_table_ep1_ult : this->bb_solo_set_data_table; - } - - const auto& tables = use_ult_tables ? this->set_data_tables_ep1_ult : this->set_data_tables; - auto ret = tables.at(static_cast(version)); - if (ret == nullptr) { - throw std::runtime_error("no set data table exists for this version"); - } - return ret; -} - -std::shared_ptr ServerState::level_table(Version version) const { - switch (version) { - case Version::DC_NTE: - case Version::DC_11_2000: - case Version::DC_V1: - case Version::DC_V2: - case Version::PC_NTE: - case Version::PC_V2: - case Version::GC_NTE: // TODO: Does NTE use the v2 table, the v3 table, or neither? - return this->level_table_v1_v2; - case Version::GC_V3: - case Version::GC_EP3_NTE: - case Version::GC_EP3: - case Version::XB_V3: - return this->level_table_v3; - case Version::BB_V4: - return this->level_table_v4; - default: - throw std::logic_error("level table not available for version"); - } -} - -std::shared_ptr ServerState::item_parameter_table(Version version) const { - auto ret = this->item_parameter_tables.at(static_cast(version)); - if (ret == nullptr) { - throw std::runtime_error("no item parameter table exists for this version"); - } - return ret; -} - -std::shared_ptr ServerState::item_parameter_table_for_encode(Version version) const { - return this->item_parameter_table(is_v1(version) ? Version::PC_V2 : version); -} - -std::shared_ptr ServerState::mag_metadata_table(Version version) const { - if (version == Version::DC_NTE) { - return this->mag_metadata_table_dc_nte; - } else if (version == Version::DC_11_2000) { - return this->mag_metadata_table_dc_11_2000; - } else if (is_v1(version)) { - return this->mag_metadata_table_v1; - } else if (is_v2(version)) { - return this->mag_metadata_table_v2; - } else if (!is_v4(version)) { - return this->mag_metadata_table_v3; - } else { - return this->mag_metadata_table_v4; - } -} - -std::shared_ptr ServerState::item_stack_limits(Version version) const { - auto ret = this->item_stack_limits_tables.at(static_cast(version)); - if (ret == nullptr) { - throw std::runtime_error("no item stack limits table exists for this version"); - } - return ret; -} - -std::shared_ptr ServerState::item_name_index_opt(Version version) const { - return this->item_name_indexes.at(static_cast(version)); -} - -std::shared_ptr ServerState::item_name_index(Version version) const { - auto ret = this->item_name_index_opt(version); - if (ret == nullptr) { - throw std::runtime_error("no item name index exists for this version"); - } - return ret; -} - -std::string ServerState::describe_item(Version version, const ItemData& item, uint8_t flags) const { - if (is_v1(version)) { - ItemData encoded = item; - encoded.encode_for_version(version, this->item_parameter_table(version)); - return this->item_name_index(version)->describe_item(encoded, flags); - } else { - return this->item_name_index(version)->describe_item(item, flags); - } -} - -ItemData ServerState::parse_item_description(Version version, const std::string& description) const { - return this->item_name_index(version)->parse_item_description(description); -} - -std::shared_ptr ServerState::common_item_set(Version logic_version, std::shared_ptr q) const { - if (q && !q->meta.common_item_set_name.empty()) { - try { - return this->common_item_sets.at(q->meta.common_item_set_name); - } catch (const std::out_of_range&) { - throw std::runtime_error(std::format("common item set {} for quest {} does not exist", - q->meta.common_item_set_name, q->meta.name)); - } - } else if (is_v1_or_v2(logic_version) && (logic_version != Version::GC_NTE)) { - // TODO: We should probably have a v1 common item set at some point too - return this->common_item_sets.at("common-table-v1-v2"); - } else if ((logic_version == Version::GC_NTE) || is_v3(logic_version) || is_v4(logic_version)) { - return this->common_item_sets.at("common-table-v3-v4"); - } else { - throw std::runtime_error(std::format( - "no default common item set is available for {}", phosg::name_for_enum(logic_version))); - } -} - -std::shared_ptr ServerState::rare_item_set(Version logic_version, std::shared_ptr q) const { - if (q && !q->meta.rare_item_set_name.empty()) { - try { - return this->rare_item_sets.at(q->meta.rare_item_set_name); - } catch (const std::out_of_range&) { - throw std::runtime_error(std::format("rare item set {} for quest {} does not exist", - q->meta.rare_item_set_name, q->meta.name)); - } - } else if (is_v1(logic_version)) { - return this->rare_item_sets.at("rare-table-v1"); - } else if (is_v2(logic_version) && (logic_version != Version::GC_NTE)) { - return this->rare_item_sets.at("rare-table-v2"); - } else if (is_v3(logic_version) || (logic_version == Version::GC_NTE)) { - return this->rare_item_sets.at("rare-table-v3"); - } else if (is_v4(logic_version)) { - return this->rare_item_sets.at("rare-table-v4"); - } else { - throw std::runtime_error(std::format("no default rare item set is available for {}", phosg::name_for_enum(logic_version))); - } -} - -void ServerState::set_port_configuration(const std::vector& port_configs) { - this->name_to_port_config.clear(); - this->number_to_port_config.clear(); - - bool any_port_is_pc_console_detect = false; - for (const auto& pc : port_configs) { - auto spc = std::make_shared(pc); - if (!this->name_to_port_config.emplace(spc->name, spc).second) { - // Note: This is a logic_error instead of a runtime_error because port_configs comes from a JSON map, so the - // names should already all be unique. In contrast, the user can define port configurations with the same number - // while still writing valid JSON, so only one of these cases can reasonably occur as a result of user behavior. - throw std::logic_error("duplicate name in port configuration"); - } - if (!this->number_to_port_config.emplace(spc->port, spc).second) { - throw std::runtime_error("duplicate number in port configuration"); - } - if (spc->behavior == ServerBehavior::PC_CONSOLE_DETECT) { - any_port_is_pc_console_detect = true; - } - } - - if (any_port_is_pc_console_detect) { - if (!this->name_to_port_config.count("pc")) { - throw std::runtime_error("pc port is not defined, but some ports use the pc_console_detect behavior"); - } - if (!this->name_to_port_config.count("gc-us3")) { - throw std::runtime_error("gc-us3 port is not defined, but some ports use the pc_console_detect behavior"); - } - } -} - -std::shared_ptr ServerState::load_bb_file(const std::string& filename) const { - - if (this->bb_patch_file_index) { - // First, look in the patch tree's data directory - std::string patch_index_path = "./data/" + filename; - try { - return this->bb_patch_file_index->get(patch_index_path)->load_data(); - } catch (const std::out_of_range&) { - } - } - - if (this->bb_data_gsl) { - // Second, look in the patch tree's data.gsl file - try { - // TODO: It's kinda not great that we copy the data here; find a way to avoid doing this (also in the below case) - return std::make_shared(this->bb_data_gsl->get_copy(filename)); - } catch (const std::out_of_range&) { - } - - // Third, look in data.gsl without the filename extension - size_t dot_offset = filename.rfind('.'); - if (dot_offset != std::string::npos) { - std::string no_ext_gsl_filename = filename.substr(0, dot_offset); - try { - return std::make_shared(this->bb_data_gsl->get_copy(no_ext_gsl_filename)); - } catch (const std::out_of_range&) { - } - } - } - - // Finally, look in system/blueburst - return std::make_shared(phosg::load_file("system/blueburst/" + filename)); -} - -std::shared_ptr ServerState::load_map_file(Version version, const std::string& filename) const { - if (version == Version::BB_V4) { - try { - return this->load_bb_file(filename); - } catch (const std::exception& e) { - } - } else if (version == Version::PC_V2) { - try { - return std::make_shared(phosg::load_file("system/patch-pc/Media/PSO/" + filename)); - } catch (const std::exception& e) { - } - } - try { - std::string path = std::format("system/maps/{}/{}", file_path_token_for_version(version), filename); - return std::make_shared(phosg::load_file(path)); - } catch (const std::exception& e) { - } - return nullptr; -} - -std::pair ServerState::parse_port_spec(const phosg::JSON& json) const { - if (json.is_list()) { - std::string addr = json.at(0).as_string(); - try { - addr = string_for_address(this->all_addresses.at(addr)); - } catch (const std::out_of_range&) { - } - return std::make_pair(addr, json.at(1).as_int()); - } else { - return std::make_pair("", json.as_int()); - } -} - -std::vector ServerState::parse_port_configuration(const phosg::JSON& json) const { - std::vector ret; - for (const auto& item_json_it : json.as_dict()) { - const auto& item_list = item_json_it.second; - PortConfiguration& pc = ret.emplace_back(); - pc.name = item_json_it.first; - auto spec = this->parse_port_spec(item_list->at(0)); - pc.addr = std::move(spec.first); - pc.port = spec.second; - pc.version = phosg::enum_for_name(item_list->at(1).as_string()); - pc.behavior = phosg::enum_for_name(item_list->at(2).as_string()); - } - return ret; -} - -void ServerState::collect_network_addresses() { - config_log.info_f("Reading network addresses"); - this->all_addresses = get_local_addresses(); - for (const auto& it : this->all_addresses) { - config_log.info_f("Found interface: {} = {}", it.first, string_for_address(it.second)); - } -} - -void ServerState::load_config_early() { - if (this->config_filename.empty()) { - throw std::logic_error("configuration filename is missing"); - } - - config_log.info_f("Loading configuration"); - this->config_json = std::make_shared(phosg::JSON::parse(phosg::load_file(this->config_filename))); - - auto parse_behavior_switch = [&](const std::string& json_key, BehaviorSwitch default_value) -> ServerState::BehaviorSwitch { - try { - std::string behavior = this->config_json->get_string(json_key); - if (behavior == "Off") { - return ServerState::BehaviorSwitch::OFF; - } else if (behavior == "OffByDefault") { - return ServerState::BehaviorSwitch::OFF_BY_DEFAULT; - } else if (behavior == "OnByDefault") { - return ServerState::BehaviorSwitch::ON_BY_DEFAULT; - } else if (behavior == "On") { - return ServerState::BehaviorSwitch::ON; - } else { - throw std::runtime_error("invalid value for " + json_key); - } - } catch (const std::out_of_range&) { - return default_value; - } - }; - - this->name = this->config_json->at("ServerName").as_string(); - this->num_worker_threads = this->config_json->at("WorkerThreads").as_int(); - - if (!this->one_time_config_loaded) { - try { - this->username = this->config_json->at("User").as_string(); - if (this->username == "$SUDO_USER") { - const char* user_from_env = getenv("SUDO_USER"); - if (!user_from_env) { - throw std::runtime_error("configuration specifies $SUDO_USER, but variable is not defined"); - } - this->username = user_from_env; - } - } catch (const std::out_of_range&) { - } - - this->set_port_configuration(parse_port_configuration(this->config_json->at("PortConfiguration"))); - try { - auto spec = this->parse_port_spec(this->config_json->at("DNSServerPort")); - this->dns_server_addr = std::move(spec.first); - this->dns_server_port = spec.second; - } catch (const std::out_of_range&) { - } - try { - for (const auto& item : this->config_json->at("IPStackListen").as_list()) { - if (item->is_int()) { - this->ip_stack_addresses.emplace_back(std::format("0.0.0.0:{}", item->as_int())); - } else if (!IS_WINDOWS) { - this->ip_stack_addresses.emplace_back(item->as_string()); - } else { - config_log.warning_f("Unix sockets are not supported on Windows; skipping address {}", item->as_string()); - } - } - } catch (const std::out_of_range&) { - } - try { - for (const auto& item : this->config_json->at("PPPStackListen").as_list()) { - if (item->is_int()) { - this->ppp_stack_addresses.emplace_back(std::format("0.0.0.0:{}", item->as_int())); - } else if (!IS_WINDOWS) { - this->ppp_stack_addresses.emplace_back(item->as_string()); - } else { - config_log.warning_f("Unix sockets are not supported on Windows; skipping address {}", item->as_string()); - } - } - } catch (const std::out_of_range&) { - } - try { - for (const auto& item : this->config_json->at("PPPRawListen").as_list()) { - if (item->is_int()) { - this->ppp_raw_addresses.emplace_back(std::format("0.0.0.0:{}", item->as_int())); - } else if (!IS_WINDOWS) { - this->ppp_raw_addresses.emplace_back(item->as_string()); - } else { - config_log.warning_f("Unix sockets are not supported on Windows; skipping address {}", item->as_string()); - } - } - } catch (const std::out_of_range&) { - } - try { - for (const auto& item : this->config_json->at("HTTPListen").as_list()) { - if (item->is_int()) { - this->http_addresses.emplace_back(std::format("0.0.0.0:{}", item->as_int())); - } else if (!IS_WINDOWS) { - this->http_addresses.emplace_back(item->as_string()); - } else { - config_log.warning_f("Unix sockets are not supported on Windows; skipping address {}", item->as_string()); - } - } - } catch (const std::out_of_range&) { - } - - this->one_time_config_loaded = true; - } - - try { - auto local_address_str = this->config_json->at("LocalAddress").as_string(); - try { - this->local_address = this->all_addresses.at(local_address_str); - config_log.info_f("Added local address: {} ({})", string_for_address(this->local_address), local_address_str); - } catch (const std::out_of_range&) { - this->local_address = address_for_string(local_address_str.c_str()); - config_log.info_f("Added local address: {}", local_address_str); - } - this->all_addresses.erase(""); - this->all_addresses.emplace("", this->local_address); - } catch (const std::out_of_range&) { - for (const auto& it : this->all_addresses) { - // Choose any local interface except the loopback interface - if (!is_loopback_address(it.second) && is_local_address(it.second)) { - this->local_address = it.second; - } - } - if (this->local_address) { - config_log.warning_f("Local address not specified; using {} as default", string_for_address(this->local_address)); - } else { - config_log.warning_f("Local address not specified and no default is available"); - } - } - - try { - auto external_address_str = this->config_json->at("ExternalAddress").as_string(); - try { - this->external_address = this->all_addresses.at(external_address_str); - config_log.info_f("Added external address: {} ({})", - string_for_address(this->external_address), external_address_str); - } catch (const std::out_of_range&) { - this->external_address = address_for_string(external_address_str.c_str()); - config_log.info_f("Added external address: {}", external_address_str); - } - this->all_addresses.erase(""); - this->all_addresses.emplace("", this->external_address); - } catch (const std::out_of_range&) { - for (const auto& it : this->all_addresses) { - // Choose any non-local address, if any exist - if (!is_local_address(it.second)) { - this->external_address = it.second; - break; - } - } - if (this->external_address) { - config_log.warning_f("External address not specified; using {} as default", - string_for_address(this->external_address)); - } else { - config_log.warning_f( - "External address not specified and no default is available; only local clients will be able to connect"); - } - } - - try { - this->banned_ipv4_ranges = std::make_shared(this->config_json->at("BannedIPV4Ranges")); - this->disconnect_all_banned_clients(); - } catch (const std::out_of_range&) { - this->banned_ipv4_ranges = std::make_shared(); - } - - this->client_ping_interval_usecs = this->config_json->get_int("ClientPingInterval", 30000000); - this->client_idle_timeout_usecs = this->config_json->get_int("ClientIdleTimeout", 60000000); - this->patch_client_idle_timeout_usecs = this->config_json->get_int("PatchClientIdleTimeout", 300000000); - - this->ip_stack_debug = this->config_json->get_bool("IPStackDebug", false); - this->allow_unregistered_users = this->config_json->get_bool("AllowUnregisteredUsers", false); - this->allow_pc_nte = this->config_json->get_bool("AllowPCNTE", false); - this->allow_same_account_concurrent_logins = this->config_json->get_bool("AllowSameAccountConcurrentLogins", false); - this->use_temp_accounts_for_prototypes = this->config_json->get_bool("UseTemporaryAccountsForPrototypes", true); - this->notify_server_for_max_level_achieved = this->config_json->get_bool("NotifyServerForMaxLevelAchieved", false); - this->allowed_drop_modes_v1_v2_normal = this->config_json->get_int("AllowedDropModesV1V2Normal", 0x1F); - this->allowed_drop_modes_v1_v2_battle = this->config_json->get_int("AllowedDropModesV1V2Battle", 0x07); - this->allowed_drop_modes_v1_v2_challenge = this->config_json->get_int("AllowedDropModesV1V2Challenge", 0x07); - this->allowed_drop_modes_v3_normal = this->config_json->get_int("AllowedDropModesV3Normal", 0x1F); - this->allowed_drop_modes_v3_battle = this->config_json->get_int("AllowedDropModesV3Battle", 0x07); - this->allowed_drop_modes_v3_challenge = this->config_json->get_int("AllowedDropModesV3Challenge", 0x07); - this->allowed_drop_modes_v4_normal = this->config_json->get_int("AllowedDropModesV4Normal", 0x1D); - this->allowed_drop_modes_v4_battle = this->config_json->get_int("AllowedDropModesV4Battle", 0x05); - this->allowed_drop_modes_v4_challenge = this->config_json->get_int("AllowedDropModesV4Challenge", 0x05); - this->default_drop_mode_v1_v2_normal = this->config_json->get_enum("DefaultDropModeV1V2Normal", ServerDropMode::CLIENT); - this->default_drop_mode_v1_v2_battle = this->config_json->get_enum("DefaultDropModeV1V2Battle", ServerDropMode::CLIENT); - this->default_drop_mode_v1_v2_challenge = this->config_json->get_enum("DefaultDropModeV1V2Challenge", ServerDropMode::CLIENT); - this->default_drop_mode_v3_normal = this->config_json->get_enum("DefaultDropModeV3Normal", ServerDropMode::CLIENT); - this->default_drop_mode_v3_battle = this->config_json->get_enum("DefaultDropModeV3Battle", ServerDropMode::CLIENT); - this->default_drop_mode_v3_challenge = this->config_json->get_enum("DefaultDropModeV3Challenge", ServerDropMode::CLIENT); - this->default_drop_mode_v4_normal = this->config_json->get_enum("DefaultDropModeV4Normal", ServerDropMode::SERVER_SHARED); - this->default_drop_mode_v4_battle = this->config_json->get_enum("DefaultDropModeV4Battle", ServerDropMode::SERVER_SHARED); - this->default_drop_mode_v4_challenge = this->config_json->get_enum("DefaultDropModeV4Challenge", ServerDropMode::SERVER_SHARED); - if ((this->default_drop_mode_v4_normal == ServerDropMode::CLIENT) || - (this->default_drop_mode_v4_battle == ServerDropMode::CLIENT) || - (this->default_drop_mode_v4_challenge == ServerDropMode::CLIENT)) { - throw std::runtime_error("default V4 drop mode cannot be CLIENT"); - } - if ((this->allowed_drop_modes_v4_normal & (1 << static_cast(ServerDropMode::CLIENT))) || - (this->allowed_drop_modes_v4_battle & (1 << static_cast(ServerDropMode::CLIENT))) || (this->allowed_drop_modes_v4_challenge & (1 << static_cast(ServerDropMode::CLIENT)))) { - throw std::runtime_error("CLIENT drop mode cannot be allowed in V4"); - } - - auto parse_quest_flag_rewrites = [&json = this->config_json](const char* key) -> std::unordered_map { - std::unordered_map ret; - try { - for (const auto& it : json->get_dict(key)) { - if (!it.first.starts_with("F_")) { - throw std::runtime_error("invalid flag reference: " + it.first); - } - uint16_t flag = stoul(it.first.substr(2), nullptr, 16); - if (it.second->is_bool()) { - ret.emplace(flag, it.second->as_bool() ? "true" : "false"); - } else { - ret.emplace(flag, it.second->as_string()); - } - } - } catch (const std::out_of_range&) { - } - return ret; - }; - this->quest_flag_rewrites_v1_v2 = parse_quest_flag_rewrites("QuestFlagRewritesV1V2"); - this->quest_flag_rewrites_v3 = parse_quest_flag_rewrites("QuestFlagRewritesV3"); - this->quest_flag_rewrites_v4 = parse_quest_flag_rewrites("QuestFlagRewritesV4"); - - this->quest_counter_fields.clear(); - try { - for (const auto& it : this->config_json->get_dict("QuestCounterFields")) { - const auto& def = it.second->as_list(); - this->quest_counter_fields.emplace(it.first, std::make_pair(def.at(0)->as_int(), def.at(1)->as_int())); - } - } catch (const std::out_of_range&) { - } - - this->persistent_game_idle_timeout_usecs = this->config_json->get_int("PersistentGameIdleTimeout", 0); - this->cheat_mode_behavior = parse_behavior_switch("CheatModeBehavior", BehaviorSwitch::OFF_BY_DEFAULT); - this->default_switch_assist_enabled = this->config_json->get_bool("EnableSwitchAssistByDefault", false); - this->use_game_creator_section_id = this->config_json->get_bool("UseGameCreatorSectionID", false); - this->rare_notifs_enabled_for_client_drops = this->config_json->get_bool("RareNotificationsEnabledForClientDrops", false); - this->default_rare_notifs_enabled_v1_v2 = this->config_json->get_bool("RareNotificationsEnabledByDefault", false); - this->default_rare_notifs_enabled_v3_v4 = this->default_rare_notifs_enabled_v1_v2; - this->default_rare_notifs_enabled_v1_v2 = this->config_json->get_bool("RareNotificationsEnabledByDefaultV1V2", this->default_rare_notifs_enabled_v1_v2); - this->default_rare_notifs_enabled_v3_v4 = this->config_json->get_bool("RareNotificationsEnabledByDefaultV3V4", this->default_rare_notifs_enabled_v3_v4); - this->enable_send_function_call_quest_numbers.clear(); - try { - for (const auto& it : this->config_json->get_dict("EnableSendFunctionCallQuestNumbers")) { - if (it.first.size() != 4) { - throw std::runtime_error(std::format( - "specific_version {} in EnableSendFunctionCallQuestNumbers is not a 4-byte string", - it.first)); - } - uint32_t specific_version = phosg::StringReader(it.first).get_u32b(); - int64_t quest_num = it.second->as_int(); - this->enable_send_function_call_quest_numbers.emplace(specific_version, quest_num); - } - } catch (const std::out_of_range&) { - } - this->enable_v3_v4_protected_subcommands = this->config_json->get_bool("EnableV3V4ProtectedSubcommands", false); - - auto parse_int_list = +[](const phosg::JSON& json) -> std::vector { - std::vector ret; - for (const auto& item : json.as_list()) { - ret.emplace_back(item->as_int()); - } - return ret; - }; - - this->ep3_infinite_meseta = this->config_json->get_bool("Episode3InfiniteMeseta", false); - try { - this->ep3_defeat_player_meseta_rewards = parse_int_list(this->config_json->at("Episode3DefeatPlayerMeseta")); - } catch (const std::out_of_range&) { - this->ep3_defeat_player_meseta_rewards = {300, 400, 500, 600, 700}; - } - try { - this->ep3_defeat_com_meseta_rewards = parse_int_list(this->config_json->get("Episode3DefeatCOMMeseta", phosg::JSON::list())); - } catch (const std::out_of_range&) { - this->ep3_defeat_com_meseta_rewards = {100, 200, 300, 400, 500}; - } - this->ep3_final_round_meseta_bonus = this->config_json->get_int("Episode3FinalRoundMesetaBonus", 300); - this->ep3_jukebox_is_free = this->config_json->get_bool("Episode3JukeboxIsFree", false); - this->ep3_behavior_flags = this->config_json->get_int("Episode3BehaviorFlags", 0); - this->ep3_card_auction_points = this->config_json->get_int("CardAuctionPoints", 0); - this->hide_download_commands = this->config_json->get_bool("HideDownloadCommands", true); - this->censor_credentials = this->config_json->get_bool("CensorCredentials", true); - this->proxy_allow_save_files = this->config_json->get_bool("ProxyAllowSaveFiles", true); - - try { - const auto& i = this->config_json->at("CardAuctionSize"); - if (i.is_int()) { - this->ep3_card_auction_min_size = i.as_int(); - this->ep3_card_auction_max_size = this->ep3_card_auction_min_size; - } else { - this->ep3_card_auction_min_size = i.at(0).as_int(); - this->ep3_card_auction_max_size = i.at(1).as_int(); - } - } catch (const std::out_of_range&) { - this->ep3_card_auction_min_size = 0; - this->ep3_card_auction_max_size = 0; - } - - this->ep3_lobby_banners.clear(); - size_t banner_index = 0; - for (const auto& it : this->config_json->get("Episode3LobbyBanners", phosg::JSON::list()).as_list()) { - std::string path = "system/ep3/banners/" + it->at(2).as_string(); - - std::string compressed_gvm_data; - std::string decompressed_gvm_data; - std::string lower_path = phosg::tolower(path); - if (lower_path.ends_with(".gvm.prs")) { - compressed_gvm_data = phosg::load_file(path); - } else if (lower_path.ends_with(".gvm")) { - decompressed_gvm_data = phosg::load_file(path); - } else if (lower_path.ends_with(".bmp")) { - auto img = phosg::ImageRGBA8888N::from_file_data(phosg::load_file(path)); - decompressed_gvm_data = encode_gvm( - img, - has_any_transparent_pixels(img) ? GVRDataFormat::RGB5A3 : GVRDataFormat::RGB565, - std::format("bnr{}", banner_index), - 0x80 | banner_index); - banner_index++; - } else { - throw std::runtime_error(std::format("banner {} is in an unknown format", path)); - } - - size_t decompressed_size = decompressed_gvm_data.empty() - ? prs_decompress_size(compressed_gvm_data) - : decompressed_gvm_data.size(); - if (decompressed_size > 0x37000) { - throw std::runtime_error(std::format( - "banner {} is too large (0x{:X} bytes; maximum size is 0x37000 bytes)", path, decompressed_size)); - } - - if (compressed_gvm_data.empty()) { - compressed_gvm_data = prs_compress_optimal(decompressed_gvm_data); - } - if (compressed_gvm_data.size() > 0x3800) { - throw std::runtime_error(std::format( - "banner {} cannot be compressed small enough (0x{:X} bytes; maximum size is 0x3800 bytes compressed)", - it->at(2).as_string(), compressed_gvm_data.size())); - } - config_log.info_f( - "Loaded Episode 3 lobby banner {} (0x{:X} -> 0x{:X} bytes)", - path, decompressed_size, compressed_gvm_data.size()); - this->ep3_lobby_banners.emplace_back( - Ep3LobbyBannerEntry{.type = static_cast(it->at(0).as_int()), - .which = static_cast(it->at(1).as_int()), - .data = std::move(compressed_gvm_data)}); - } - - { - auto parse_ep3_ex_result_cmd = [&](const phosg::JSON& src) -> std::shared_ptr { - auto ret = std::make_shared(); - const auto& win_json = src.at("Win"); - for (size_t z = 0; z < std::min(win_json.size(), 10); z++) { - ret->win_entries[z].threshold = win_json.at(z).at(0).as_int(); - ret->win_entries[z].value = win_json.at(z).at(1).as_int(); - } - const auto& lose_json = src.at("Lose"); - for (size_t z = 0; z < std::min(lose_json.size(), 10); z++) { - ret->lose_entries[z].threshold = lose_json.at(z).at(0).as_int(); - ret->lose_entries[z].value = lose_json.at(z).at(1).as_int(); - } - return ret; - }; - const auto& categories_json = this->config_json->at("Episode3EXResultValues"); - this->ep3_default_ex_values = parse_ep3_ex_result_cmd(categories_json.at("Default")); - try { - this->ep3_tournament_ex_values = parse_ep3_ex_result_cmd(categories_json.at("Tournament")); - } catch (const std::out_of_range&) { - this->ep3_tournament_ex_values = this->ep3_default_ex_values; - } - try { - this->ep3_tournament_ex_values = parse_ep3_ex_result_cmd(categories_json.at("TournamentFinalMatch")); - } catch (const std::out_of_range&) { - this->ep3_tournament_final_round_ex_values = this->ep3_tournament_ex_values; - } - } - - try { - const auto& stack_limits_tables_json = this->config_json->at("ItemStackLimits"); - for (size_t v_s = NUM_PATCH_VERSIONS; v_s < NUM_VERSIONS; v_s++) { - try { - Version v = static_cast(v_s); - this->item_stack_limits_tables[v_s] = std::make_shared( - v, stack_limits_tables_json.at(v_s - NUM_PATCH_VERSIONS)); - } catch (const std::out_of_range&) { - } - } - } catch (const std::out_of_range&) { - } - - this->bb_max_bank_items = this->config_json->get_int("BBMaxBankItems", 200); - this->bb_max_bank_meseta = this->config_json->get_int("BBMaxBankMeseta", 999999); - - for (size_t v_s = NUM_PATCH_VERSIONS; v_s < NUM_VERSIONS; v_s++) { - if (!this->item_stack_limits_tables[v_s]) { - Version v = static_cast(v_s); - if ((v == Version::DC_NTE) || (v == Version::DC_11_2000)) { - this->item_stack_limits_tables[v_s] = std::make_shared( - v, ItemData::StackLimits::DEFAULT_TOOL_LIMITS_DC_NTE, 999999); - } else if (v_s < static_cast(Version::GC_NTE)) { - this->item_stack_limits_tables[v_s] = std::make_shared( - v, ItemData::StackLimits::DEFAULT_TOOL_LIMITS_V1_V2, 999999); - } else { - this->item_stack_limits_tables[v_s] = std::make_shared( - v, ItemData::StackLimits::DEFAULT_TOOL_LIMITS_V3_V4, 999999); - } - } - } - - this->bb_global_exp_multiplier = this->config_json->get_float("BBGlobalEXPMultiplier", 1.0f); - this->exp_share_multiplier = this->config_json->get_float("BBEXPShareMultiplier", 0.5f); - this->server_global_drop_rate_multiplier = this->config_json->get_float("ServerGlobalDropRateMultiplier", 1.0f); - - if (this->is_debug) { - set_all_log_levels(phosg::LogLevel::L_DEBUG); - } else { - set_log_levels_from_json(this->config_json->get("LogLevels", phosg::JSON::dict())); - } - - try { - this->run_shell_behavior = this->config_json->at("RunInteractiveShell").as_bool() - ? ServerState::RunShellBehavior::ALWAYS - : ServerState::RunShellBehavior::NEVER; - } catch (const std::out_of_range&) { - } - - try { - const auto& groups = this->config_json->get_list("CompatibilityGroups"); - this->compatibility_groups.fill(0); - for (size_t v_s = 0; v_s < groups.size(); v_s++) { - this->compatibility_groups[v_s] = groups[v_s]->as_int(); - } - } catch (const std::out_of_range&) { - static_assert(NUM_VERSIONS == 14, "Don't forget to update the default compatibility groups"); - this->compatibility_groups = { - 0x0000, // PC_PATCH - 0x0000, // BB_PATCH - 0x0004, // DC_NTE compatible only with itself - 0x0008, // DC_11_2000 compatible only with itself - 0x00B0, // DC_V1 compatible with DC_V1, DC_V2, and PC_V2 - 0x00B0, // DC_V2 compatible with DC_V1, DC_V2, and PC_V2 - 0x0040, // PC_NTE compatible only with itself - 0x00B0, // PC_V2 compatible with DC_V1, DC_V2, and PC_V2 - 0x0100, // GC_NTE compatible only with itself - 0x1200, // GC_V3 compatible with GC_V3 and XB_V3 - 0x0400, // GC_EP3_NTE compatible only with itself - 0x0800, // GC_EP3 compatible only with itself - 0x1200, // XB_V3 compatible with GC_V3 and XB_V3 - 0x2000, // BB_V4 compatible only with itself - }; - } - - this->enable_chat_commands = this->config_json->get_bool("EnableChatCommands", true); - try { - const auto& s = this->config_json->get_string("ChatCommandSentinel"); - if (s.size() != 1) { - throw std::runtime_error("ChatCommandSentinel must be a string of length 1"); - } - this->chat_command_sentinel = s[0]; - } catch (const std::out_of_range&) { - } - this->num_backup_character_slots = this->config_json->get_int("BackupCharacterSlots", 16); - - this->version_name_colors.reset(); - this->client_customization_name_color = 0; - try { - const auto& colors_json = this->config_json->get_list("VersionNameColors"); - if (colors_json.size() != NUM_NON_PATCH_VERSIONS) { - throw std::runtime_error("VersionNameColors list length is incorrect"); - } - auto new_colors = std::make_unique>(); - for (size_t z = 0; z < NUM_NON_PATCH_VERSIONS; z++) { - new_colors->at(z) = colors_json.at(z)->as_int(); - } - this->version_name_colors = std::move(new_colors); - } catch (const std::out_of_range&) { - } - try { - this->client_customization_name_color = this->config_json->get_int("ClientCustomizationNameColor"); - } catch (const std::out_of_range&) { - } - - for (auto& order : this->public_lobby_search_orders) { - order.clear(); - } - this->client_customization_public_lobby_search_order.clear(); - try { - const auto& orders_json = this->config_json->get_list("LobbySearchOrders"); - for (size_t v_s = 0; v_s < orders_json.size(); v_s++) { - auto& order = this->public_lobby_search_orders.at(v_s); - const auto& order_json = orders_json.at(v_s); - for (const auto& it : order_json->as_list()) { - order.emplace_back(it->as_int()); - } - } - } catch (const std::out_of_range&) { - } - try { - const auto& order_json = this->config_json->get_list("ClientCustomizationLobbySearchOrder"); - auto& order = this->client_customization_public_lobby_search_order; - for (const auto& it : order_json) { - order.emplace_back(it->as_int()); - } - } catch (const std::out_of_range&) { - } - - this->pre_lobby_event = 0; - try { - auto v = this->config_json->at("MenuEvent"); - this->pre_lobby_event = v.is_int() ? v.as_int() : event_for_name(v.as_string()); - } catch (const std::out_of_range&) { - } - - this->ep3_menu_song = this->config_json->get_int("Episode3MenuSong", -1); - - try { - this->quest_category_index = std::make_shared(this->config_json->at("QuestCategories")); - } catch (const std::exception& e) { - throw std::runtime_error(std::format( - "QuestCategories is missing or invalid in config ({}); see config.example.json for an example", e.what())); - } - - config_log.info_f("Creating menus"); - - auto information_menu_v2 = std::make_shared(MenuID::INFORMATION, "Information"); - auto information_menu_v3 = std::make_shared(MenuID::INFORMATION, "Information"); - std::shared_ptr> information_contents_v2 = std::make_shared>(); - std::shared_ptr> information_contents_v3 = std::make_shared>(); - - information_menu_v2->items.emplace_back(InformationMenuItemID::GO_BACK, "Go back", - "Return to the\nmain menu", MenuItem::Flag::INVISIBLE_IN_INFO_MENU); - information_menu_v3->items.emplace_back(InformationMenuItemID::GO_BACK, "Go back", - "Return to the\nmain menu", MenuItem::Flag::INVISIBLE_IN_INFO_MENU); - { - auto blank_json = phosg::JSON::list(); - const phosg::JSON& default_json = this->config_json->get("InformationMenuContents", blank_json); - const phosg::JSON& v2_json = this->config_json->get("InformationMenuContentsV1V2", default_json); - const phosg::JSON& v3_json = this->config_json->get("InformationMenuContentsV3", default_json); - - uint32_t item_id = 0; - for (const auto& item : v2_json.as_list()) { - std::string name = item->get_string(0); - std::string short_desc = item->get_string(1); - information_menu_v2->items.emplace_back(item_id, name, short_desc, 0); - information_contents_v2->emplace_back(item->get_string(2)); - item_id++; - } - - item_id = 0; - for (const auto& item : v3_json.as_list()) { - std::string name = item->get_string(0); - std::string short_desc = item->get_string(1); - information_menu_v3->items.emplace_back(item_id, name, short_desc, MenuItem::Flag::REQUIRES_MESSAGE_BOXES); - information_contents_v3->emplace_back(item->get_string(2)); - item_id++; - } - } - this->information_menu_v2 = information_menu_v2; - this->information_menu_v3 = information_menu_v3; - this->information_contents_v2 = information_contents_v2; - this->information_contents_v3 = information_contents_v3; - - auto generate_proxy_destinations_menu = [&](std::vector>& ret_pds, const char* key) -> std::shared_ptr { - auto ret = std::make_shared(MenuID::PROXY_DESTINATIONS, "Proxy server"); - ret_pds.clear(); - - try { - std::map sorted_jsons; - for (const auto& it : this->config_json->at(key).as_dict()) { - sorted_jsons.emplace(it.first, *it.second); - } - - ret->items.emplace_back(ProxyDestinationsMenuItemID::GO_BACK, "Go back", "Return to the\nmain menu", 0); - ret->items.emplace_back(ProxyDestinationsMenuItemID::OPTIONS, "Options", "Set proxy session\noptions", 0); - - uint32_t item_id = 0; - for (const auto& item : sorted_jsons) { - const std::string& netloc_str = item.second.as_string(); - const std::string& description = "$C7Remote server:\n$C6" + netloc_str; - ret->items.emplace_back(item_id, item.first, description, 0); - ret_pds.emplace_back(phosg::parse_netloc(netloc_str)); - item_id++; - } - } catch (const std::out_of_range&) { - } - return ret; - }; - - this->proxy_destinations_menu_dc = generate_proxy_destinations_menu(this->proxy_destinations_dc, "ProxyDestinations-DC"); - this->proxy_destinations_menu_pc = generate_proxy_destinations_menu(this->proxy_destinations_pc, "ProxyDestinations-PC"); - this->proxy_destinations_menu_gc = generate_proxy_destinations_menu(this->proxy_destinations_gc, "ProxyDestinations-GC"); - this->proxy_destinations_menu_xb = generate_proxy_destinations_menu(this->proxy_destinations_xb, "ProxyDestinations-XB"); - - try { - const std::string& netloc_str = this->config_json->get_string("ProxyDestination-Patch"); - this->proxy_destination_patch = phosg::parse_netloc(netloc_str); - config_log.info_f("Patch server proxy is enabled with destination {}", netloc_str); - } catch (const std::out_of_range&) { - this->proxy_destination_patch.reset(); - } - try { - const std::string& netloc_str = this->config_json->get_string("ProxyDestination-BB"); - this->proxy_destination_bb = phosg::parse_netloc(netloc_str); - config_log.info_f("BB proxy is enabled with destination {}", netloc_str); - } catch (const std::out_of_range&) { - this->proxy_destination_bb.reset(); - } - - this->welcome_message = this->config_json->get_string("WelcomeMessage", ""); - this->pc_patch_server_message = this->config_json->get_string("PCPatchServerMessage", ""); - this->bb_patch_server_message = this->config_json->get_string("BBPatchServerMessage", ""); - - this->team_reward_defs_json = nullptr; - try { - this->team_reward_defs_json = std::move(this->config_json->at("TeamRewards")); - } catch (const std::out_of_range&) { - } - - std::shared_ptr prev = MapState::DEFAULT_RARE_ENEMIES; - for (Difficulty difficulty : ALL_DIFFICULTIES_V234) { - size_t diff_index = static_cast(difficulty); - try { - std::string key = "RareEnemyRates-"; - key += token_name_for_difficulty(difficulty); - this->rare_enemy_rates_by_difficulty[diff_index] = std::make_shared( - this->config_json->at(key)); - prev = this->rare_enemy_rates_by_difficulty[diff_index]; - } catch (const std::out_of_range&) { - this->rare_enemy_rates_by_difficulty[diff_index] = prev; - } - } - try { - this->rare_enemy_rates_challenge = std::make_shared(this->config_json->at("RareEnemyRates-Challenge")); - } catch (const std::out_of_range&) { - this->rare_enemy_rates_challenge = MapState::DEFAULT_RARE_ENEMIES; - } - - this->min_levels_v1_v2[0] = DEFAULT_MIN_LEVELS_V123; - this->min_levels_v1_v2[1] = DEFAULT_MIN_LEVELS_V123; - this->min_levels_v1_v2[2] = DEFAULT_MIN_LEVELS_V123; - this->min_levels_v3[0] = DEFAULT_MIN_LEVELS_V123; - this->min_levels_v3[1] = DEFAULT_MIN_LEVELS_V123; - this->min_levels_v3[2] = DEFAULT_MIN_LEVELS_V123; - this->min_levels_v4[0] = DEFAULT_MIN_LEVELS_V4_EP1; - this->min_levels_v4[1] = DEFAULT_MIN_LEVELS_V4_EP2; - this->min_levels_v4[2] = DEFAULT_MIN_LEVELS_V4_EP4; - auto populate_min_levels = [&](std::array, 3>& dest, const char* key_name) -> void { - try { - for (const auto& ep_it : this->config_json->get_dict(key_name)) { - std::array levels({0, 0, 0, 0}); - for (size_t z = 0; z < 4; z++) { - levels[z] = ep_it.second->get_int(z) - 1; - } - switch (episode_for_token_name(ep_it.first)) { - case Episode::EP1: - dest[0] = levels; - break; - case Episode::EP2: - dest[1] = levels; - break; - case Episode::EP4: - dest[2] = levels; - break; - default: - throw std::runtime_error("unknown episode"); - } - } - } catch (const std::out_of_range&) { - } - }; - populate_min_levels(this->min_levels_v1_v2, "V1V2MinimumLevels"); - populate_min_levels(this->min_levels_v3, "V3MinimumLevels"); - populate_min_levels(this->min_levels_v4, "BBMinimumLevels"); - - this->bb_required_patches.clear(); - try { - for (const auto& it : this->config_json->get_list("BBRequiredPatches")) { - this->bb_required_patches.emplace(it->as_string()); - } - } catch (const std::out_of_range&) { - } - this->auto_patches.clear(); - try { - for (const auto& it : this->config_json->get_list("AutoPatches")) { - this->auto_patches.emplace(it->as_string()); - } - } catch (const std::out_of_range&) { - } - - try { - this->cheat_flags = CheatFlags(this->config_json->at("CheatingBehaviors")); - } catch (const std::out_of_range&) { - this->cheat_flags = CheatFlags(); - } -} - -void ServerState::load_config_late() { - for (size_t z = 1; z <= 20; z++) { - auto l = this->find_lobby(z); - if (l) { - l->event = 0; - } - } - try { - const auto& events_json = this->config_json->get_list("LobbyEvents"); - for (size_t z = 0; z < events_json.size(); z++) { - const auto& v = events_json.at(z); - uint8_t event = v->is_int() ? v->as_int() : event_for_name(v->as_string()); - const auto& l = this->find_lobby(z + 1); - if (l && l->check_flag(Lobby::Flag::DEFAULT)) { - l->event = event; - send_change_event(l, l->event); - } - } - } catch (const std::out_of_range&) { - } - - this->ep3_card_auction_pool.clear(); - try { - for (const auto& it : this->config_json->get_dict("CardAuctionPool")) { - uint16_t card_id; - try { - card_id = this->ep3_card_index->definition_for_name_normalized(it.first)->def.card_id; - } catch (const std::out_of_range&) { - throw std::runtime_error(std::format("Ep3 card \"{}\" in auction pool does not exist", it.first)); - } - this->ep3_card_auction_pool.emplace_back( - CardAuctionPoolEntry{ - .probability = static_cast(it.second->at(0).as_int()), - .card_id = card_id, - .min_price = static_cast(it.second->at(1).as_int())}); - } - } catch (const std::out_of_range&) { - } - - for (auto& trap_card_ids : this->ep3_trap_card_ids) { - trap_card_ids.clear(); - } - if (this->ep3_card_index) { - try { - const auto& ep3_trap_cards_json = this->config_json->get_list("Episode3TrapCards"); - if (!ep3_trap_cards_json.empty()) { - if (ep3_trap_cards_json.size() != 5) { - throw std::runtime_error("Episode3TrapCards must be a list of 5 lists"); - } - for (size_t trap_type = 0; trap_type < 5; trap_type++) { - auto& trap_card_ids = this->ep3_trap_card_ids[trap_type]; - for (const auto& card_it : ep3_trap_cards_json.at(trap_type)->as_list()) { - if (card_it->is_int()) { - int64_t card_id = card_it->as_int(); - try { - const auto& card = this->ep3_card_index->definition_for_id(card_id); - if (card->def.type != Episode3::CardType::ASSIST) { - throw std::runtime_error(std::format( - "Ep3 card \"{}\" ({:04X}) in trap card list is not an assist card", - card->def.en_name.decode(), card->def.card_id)); - } - trap_card_ids.emplace_back(card->def.card_id); - } catch (const std::out_of_range&) { - throw std::runtime_error(std::format("Ep3 card {:04X} in trap card list does not exist", card_id)); - } - } else { - const std::string& card_name = card_it->as_string(); - try { - const auto& card = this->ep3_card_index->definition_for_name_normalized(card_name); - if (card->def.type != Episode3::CardType::ASSIST) { - throw std::runtime_error(std::format( - "Ep3 card \"{}\" ({:04X}) in trap card list is not an assist card", - card->def.en_name.decode(), card->def.card_id)); - } - trap_card_ids.emplace_back(card->def.card_id); - } catch (const std::out_of_range&) { - throw std::runtime_error(std::format("Ep3 card \"{}\" in trap card list does not exist", card_name)); - } - } - } - } - } - } catch (const std::out_of_range&) { - } - } else { - config_log.warning_f("Episode 3 card definitions missing; cannot set trap card IDs from config"); - } - - this->quest_F95E_results.clear(); - this->quest_F95F_results.clear(); - this->quest_F960_success_results.clear(); - this->quest_F960_failure_results = QuestF960Result(); - if (this->item_name_index(Version::BB_V4)) { - try { - for (const auto& type_it : this->config_json->get_list("QuestF95EResultItems")) { - auto& type_res = this->quest_F95E_results.emplace_back(); - for (const auto& difficulty_it : type_it->as_list()) { - auto& difficulty_res = type_res.emplace_back(); - for (const auto& item_it : difficulty_it->as_list()) { - if (item_it->is_int()) { - difficulty_res.emplace_back(ItemData::from_primary_identifier( - *this->item_stack_limits(Version::BB_V4), item_it->as_int())); - } else { - try { - difficulty_res.emplace_back(this->parse_item_description(Version::BB_V4, item_it->as_string())); - } catch (const std::exception& e) { - config_log.warning_f("Cannot parse item description \"{}\": {} (skipping entry)", item_it->as_string(), e.what()); - } - } - } - } - } - } catch (const std::out_of_range&) { - } - try { - for (const auto& it : this->config_json->get_list("QuestF95FResultItems")) { - auto& list = it->as_list(); - size_t price = list.at(0)->as_int(); - const auto& desc = list.at(1); - if (desc->is_int()) { - this->quest_F95F_results.emplace_back(std::make_pair( - price, ItemData::from_primary_identifier(*this->item_stack_limits(Version::BB_V4), desc->as_int()))); - } else { - try { - this->quest_F95F_results.emplace_back(std::make_pair( - price, this->parse_item_description(Version::BB_V4, list.at(1)->as_string()))); - } catch (const std::exception& e) { - config_log.warning_f("Cannot parse item description \"{}\": {} (skipping entry)", list.at(1)->as_string(), e.what()); - } - } - } - } catch (const std::out_of_range&) { - } - try { - auto name_index = this->item_name_index(Version::BB_V4); - auto stack_limits = this->item_stack_limits(Version::BB_V4); - this->quest_F960_failure_results = QuestF960Result( - this->config_json->at("QuestF960FailureResultItems"), name_index, *stack_limits); - for (const auto& it : this->config_json->get_list("QuestF960SuccessResultItems")) { - this->quest_F960_success_results.emplace_back(*it, name_index, *stack_limits); - } - } catch (const std::out_of_range&) { - } - - auto parse_primary_identifier_list = [&](const char* key, Version v) -> std::unordered_set { - std::unordered_set ret; - try { - for (const auto& pi_json : this->config_json->get_list(key)) { - if (pi_json->is_int()) { - ret.emplace(pi_json->as_int()); - } else { - try { - auto item = this->parse_item_description(v, pi_json->as_string()); - ret.emplace(item.primary_identifier()); - } catch (const std::exception& e) { - config_log.warning_f("Cannot parse item description \"{}\": {} (skipping entry)", pi_json->as_string(), e.what()); - } - } - } - } catch (const std::out_of_range&) { - } - return ret; - }; - this->notify_game_for_item_primary_identifiers_v1_v2 = parse_primary_identifier_list( - "NotifyGameForItemPrimaryIdentifiersV1V2", Version::PC_V2); - this->notify_game_for_item_primary_identifiers_v3 = parse_primary_identifier_list( - "NotifyGameForItemPrimaryIdentifiersV3", Version::GC_V3); - this->notify_game_for_item_primary_identifiers_v4 = parse_primary_identifier_list( - "NotifyGameForItemPrimaryIdentifiersV4", Version::BB_V4); - this->notify_server_for_item_primary_identifiers_v1_v2 = parse_primary_identifier_list( - "NotifyServerForItemPrimaryIdentifiersV1V2", Version::PC_V2); - this->notify_server_for_item_primary_identifiers_v3 = parse_primary_identifier_list( - "NotifyServerForItemPrimaryIdentifiersV3", Version::GC_V3); - this->notify_server_for_item_primary_identifiers_v4 = parse_primary_identifier_list( - "NotifyServerForItemPrimaryIdentifiersV4", Version::BB_V4); - - } else { - config_log.warning_f("BB item name index is missing; cannot load quest reward lists from config"); - } -} - -void ServerState::load_bb_private_keys() { - std::vector> new_keys; - for (const auto& item : std::filesystem::directory_iterator("system/blueburst/keys")) { - std::string filename = item.path().filename().string(); - if (!filename.ends_with(".nsk")) { - continue; - } - new_keys.emplace_back(std::make_shared( - phosg::load_object_file("system/blueburst/keys/" + filename))); - config_log.debug_f("Loaded Blue Burst key file: {}", filename); - } - this->bb_private_keys = std::move(new_keys); -} - -void ServerState::load_bb_system_defaults() { - try { - this->bb_default_keyboard_config = std::make_shared>( - phosg::load_object_file>("system/blueburst/default-keyboard-config.bin")); - config_log.info_f("Default Blue Burst keyboard config is present"); - } catch (const phosg::cannot_open_file&) { - } - try { - this->bb_default_joystick_config = std::make_shared>( - phosg::load_object_file>("system/blueburst/default-joystick-config.bin")); - config_log.info_f("Default Blue Burst joystick config is present"); - } catch (const phosg::cannot_open_file&) { - } -} - void ServerState::load_accounts() { config_log.info_f("Indexing accounts"); this->account_index = std::make_shared(this->is_replay); @@ -1612,665 +281,17 @@ void ServerState::load_accounts() { void ServerState::load_teams() { config_log.info_f("Indexing teams"); - this->team_index = std::make_shared("system/teams", this->team_reward_defs_json); -} - -void ServerState::load_patch_indexes() { - std::shared_ptr bb_data_gsl; - std::shared_ptr pc_patch_file_index; - std::shared_ptr bb_patch_file_index; - - if (std::filesystem::is_directory("system/patch-pc")) { - config_log.info_f("Indexing PSO PC patch files"); - pc_patch_file_index = std::make_shared("system/patch-pc"); - } else { - config_log.info_f("PSO PC patch files not present"); - } - if (std::filesystem::is_directory("system/patch-bb")) { - config_log.info_f("Indexing PSO BB patch files"); - bb_patch_file_index = std::make_shared("system/patch-bb"); - try { - auto gsl_file = bb_patch_file_index->get("./data/data.gsl"); - bb_data_gsl = std::make_shared(gsl_file->load_data(), false); - config_log.info_f("data.gsl found in BB patch files"); - } catch (const std::out_of_range&) { - config_log.info_f("data.gsl is not present in BB patch files"); - } - } else { - config_log.info_f("PSO BB patch files not present"); - } - - this->bb_data_gsl = std::move(bb_data_gsl); - this->pc_patch_file_index = std::move(pc_patch_file_index); - this->bb_patch_file_index = std::move(bb_patch_file_index); -} - -void ServerState::load_maps() { - using SDT = SetDataTable; - - config_log.info_f("Loading map layouts"); - auto new_room_layout_index = std::make_shared( - phosg::JSON::parse(phosg::load_file("system/maps/room-layout-index.json"))); - - config_log.info_f("Loading Episode 3 Morgue maps"); - std::unordered_map> new_map_file_for_source_hash; - std::map, NUM_VERSIONS>> new_map_files_for_free_play_key; - { - // TODO: Ep3 NTE loads map_city00_on, but it appears there are variants. Figure this out and load those maps too. - auto objects_data = this->load_map_file(Version::GC_EP3, "map_city_on_battle_o.dat"); - auto enemies_data = this->load_map_file(Version::GC_EP3, "map_city_on_battle_e.dat"); - if (objects_data || enemies_data) { - uint32_t free_play_key = this->free_play_key(Episode::EP3, GameMode::NORMAL, Difficulty::NORMAL, 0, 0, 0); - auto map_file = std::make_shared(0, objects_data, enemies_data, nullptr); - new_map_file_for_source_hash.emplace(map_file->source_hash(), map_file); - new_map_files_for_free_play_key[free_play_key].at(static_cast(Version::GC_EP3)) = map_file; - config_log.info_f("Episode 3 map files loaded with free play key {:08X}", free_play_key); - } else { - config_log.info_f("Episode 3 map files not found; skipping"); - } - } - - config_log.info_f("Loading free play map files"); - for (Version v : ALL_ARPG_SEMANTIC_VERSIONS) { - for (Episode episode : ALL_EPISODES_V4) { - if ((episode == Episode::EP2 && is_v1_or_v2(v) && (v != Version::GC_NTE)) || - (episode == Episode::EP4 && !is_v4(v))) { - continue; - } - - for (GameMode mode : ALL_GAME_MODES_V4) { - if ((mode == GameMode::BATTLE) && is_pre_v1(v)) { - continue; - } - if ((mode == GameMode::CHALLENGE) && is_v1(v)) { - continue; - } - if ((mode == GameMode::SOLO && !is_v4(v))) { - continue; - } - for (Difficulty difficulty : ALL_DIFFICULTIES_V234) { - if ((difficulty == Difficulty::ULTIMATE) && is_v1(v)) { - continue; - } - auto sdt = this->set_data_table(v, episode, mode, difficulty); - for (uint8_t floor = 0; floor < 0x12; floor++) { - auto variation_maxes = sdt->num_free_play_variations_for_floor(episode, mode == GameMode::SOLO, floor); - for (size_t var_layout = 0; var_layout < variation_maxes.layout; var_layout++) { - for (size_t var_entities = 0; var_entities < variation_maxes.entities; var_entities++) { - uint32_t free_play_key = this->free_play_key(episode, mode, difficulty, floor, var_layout, var_entities); - - auto objects_filename = sdt->map_filename_for_variation( - episode, mode, floor, var_layout, var_entities, SDT::FilenameType::OBJECT_SETS); - auto enemies_filename = sdt->map_filename_for_variation( - episode, mode, floor, var_layout, var_entities, SDT::FilenameType::ENEMY_SETS); - auto events_filename = sdt->map_filename_for_variation( - episode, mode, floor, var_layout, var_entities, SDT::FilenameType::EVENTS); - auto objects_data = objects_filename.empty() ? nullptr : this->load_map_file(v, objects_filename); - auto enemies_data = enemies_filename.empty() ? nullptr : this->load_map_file(v, enemies_filename); - auto events_data = enemies_filename.empty() ? nullptr : this->load_map_file(v, events_filename); - - if (objects_data || enemies_data || events_data) { - // TODO: This is ugly; the hash computation probably should be factored into MapFile - uint64_t source_hash = ((objects_data ? phosg::fnv1a64(*objects_data) : 0) ^ - (enemies_data ? phosg::fnv1a64(*enemies_data) : 0) ^ - (events_data ? phosg::fnv1a64(*events_data) : 0)); - std::shared_ptr map_file; - try { - map_file = new_map_file_for_source_hash.at(source_hash); - } catch (const std::out_of_range&) { - map_file = std::make_shared(floor, objects_data, enemies_data, events_data); - if (map_file->source_hash() != source_hash) { - throw std::logic_error("incorrect source hash"); - } - new_map_file_for_source_hash.emplace(map_file->source_hash(), map_file); - } - - // Uncomment for debugging - // config_log.info_f("Maps for {} {} {} {} {:02X} {:02} {:02} ({:08X} => {:016X}): objects={}({})+0x{:X} enemies={}({})+0x{:X} events={}({})+0x{:X}", - // phosg::name_for_enum(v), - // name_for_episode(episode), - // name_for_mode(mode), - // name_for_difficulty(difficulty), - // floor, - // var_layout, - // var_entities, - // free_play_key, - // map_file->source_hash(), - // objects_filename.empty() ? "(none)" : objects_filename, - // objects_data ? "present" : "missing", - // map_file->count_object_sets(), - // enemies_filename.empty() ? "(none)" : enemies_filename, - // enemies_data ? "present" : "missing", - // map_file->count_enemy_sets(), - // events_filename.empty() ? "(none)" : events_filename, - // events_data ? "present" : "missing", - // map_file->count_events()); - - new_map_files_for_free_play_key[free_play_key].at(static_cast(v)) = map_file; - } - } - } - } - } - } - } - } - - this->map_file_for_source_hash = std::move(new_map_file_for_source_hash); - this->map_files_for_free_play_key = std::move(new_map_files_for_free_play_key); - this->room_layout_index = new_room_layout_index; - this->supermap_for_source_hash_sum.clear(); - this->supermap_for_free_play_key.clear(); -} - -std::shared_ptr ServerState::get_free_play_supermap( - Episode episode, GameMode mode, Difficulty difficulty, uint8_t floor, uint32_t layout, uint32_t entities) { - uint32_t free_play_key = this->free_play_key(episode, mode, difficulty, floor, layout, entities); - try { - return this->supermap_for_free_play_key.at(free_play_key); - } catch (const std::out_of_range&) { - } - - const std::array, NUM_VERSIONS>* map_files; - try { - map_files = &this->map_files_for_free_play_key.at(free_play_key); - } catch (const std::out_of_range&) { - static_game_data_log.info_f("No maps exist for key {:08X}; cannot construct supermap", free_play_key); - this->supermap_for_free_play_key.emplace(free_play_key, nullptr); - return nullptr; - } - - uint64_t source_hash_sum = 0; - for (auto map_file : *map_files) { - source_hash_sum += map_file ? map_file->source_hash() : 0; - } - - // Uncomment for debugging - // phosg::fwrite_fmt(stderr, "SuperMap for {} {} {} {:02X} {:02X} {:02X} ({:08X}): {:016X} from", - // name_for_episode(episode), - // name_for_mode(mode), - // name_for_difficulty(difficulty), - // floor, - // layout, - // entities, - // free_play_key, - // source_hash_sum); - // for (const auto& map_file : it.second) { - // if (map_file) { - // phosg::fwrite_fmt(stderr, " {:016X}", map_file->source_hash()); - // } else { - // phosg::fwrite_fmt(stderr, " ----------------"); - // } - // } - // fputc('\n', stderr); - - std::shared_ptr supermap; - try { - supermap = this->supermap_for_source_hash_sum.at(source_hash_sum); - static_game_data_log.info_f("Linking existing free play supermap {:016X} for key {:08X}", source_hash_sum, free_play_key); - } catch (const std::out_of_range&) { - supermap = std::make_shared(*map_files, SetDataTableBase::default_floor_to_area(Version::BB_V4, episode)); - this->supermap_for_source_hash_sum.emplace(source_hash_sum, supermap); - static_game_data_log.info_f("Constructed free play supermap {:016X} for key {:08X}", source_hash_sum, free_play_key); - } - this->supermap_for_free_play_key.emplace(free_play_key, supermap); - return supermap; -} - -std::vector> ServerState::supermaps_for_variations( - Episode episode, GameMode mode, Difficulty difficulty, const Variations& variations) { - std::vector> ret; - for (size_t floor = 0; floor < 0x12; floor++) { - Variations::Entry e; - if (floor < variations.entries.size()) { - e = variations.entries[floor]; - } - ret.push_back(this->get_free_play_supermap(episode, mode, difficulty, floor, e.layout, e.entities)); - if (ret.back()) { - static_game_data_log.info_f("Using supermap {:08X} for floor {:02X} layout {:X} entities {:X}", - this->free_play_key(episode, mode, difficulty, floor, e.layout, e.entities), - floor, e.layout, e.entities); - } else { - static_game_data_log.info_f("No supermap available for floor {:02X} layout {:X} entities {:X}", - floor, e.layout, e.entities); - } - } - return ret; -} - -void ServerState::load_set_data_tables() { - config_log.info_f("Loading set data tables"); - - std::array, NUM_VERSIONS> new_tables; - std::array, NUM_VERSIONS> new_tables_ep1_ult; - std::shared_ptr new_table_bb_solo; - std::shared_ptr new_table_bb_solo_ep1_ult; - - auto load_table = [&](Version version) -> void { - auto data = this->load_map_file(version, "SetDataTableOn.rel"); - new_tables[static_cast(version)] = std::make_shared(version, *data); - if (!is_v1(version) && (version != Version::PC_NTE)) { - auto data_ep1_ult = this->load_map_file(version, "SetDataTableOnUlti.rel"); - new_tables_ep1_ult[static_cast(version)] = std::make_shared(version, *data_ep1_ult); - } - }; - - new_tables[static_cast(Version::DC_NTE)] = std::make_shared(); - new_tables[static_cast(Version::DC_11_2000)] = std::make_shared(); - load_table(Version::DC_V1); - load_table(Version::DC_V2); - load_table(Version::PC_NTE); - load_table(Version::PC_V2); - load_table(Version::GC_NTE); - load_table(Version::GC_V3); - load_table(Version::XB_V3); - load_table(Version::BB_V4); - - auto bb_solo_data = this->load_map_file(Version::BB_V4, "SetDataTableOff.rel"); - new_table_bb_solo = std::make_shared(Version::BB_V4, *bb_solo_data); - auto bb_solo_data_ep1_ult = this->load_map_file(Version::BB_V4, "SetDataTableOffUlti.rel"); - new_table_bb_solo_ep1_ult = std::make_shared(Version::BB_V4, *bb_solo_data_ep1_ult); - - this->set_data_tables = std::move(new_tables); - this->set_data_tables_ep1_ult = std::move(new_tables_ep1_ult); - this->bb_solo_set_data_table = std::move(new_table_bb_solo); - this->bb_solo_set_data_table_ep1_ult = std::move(new_table_bb_solo_ep1_ult); -} - -void ServerState::load_battle_params() { - config_log.info_f("Loading JSON battle parameters"); - this->battle_params = std::make_shared(phosg::JSON::parse(phosg::load_file( - "system/tables/battle-params.json"))); -} - -void ServerState::load_level_tables() { - config_log.info_f("Loading level tables"); - this->level_table_v1_v2 = std::make_shared(phosg::JSON::parse(phosg::load_file( - "system/tables/level-table-v1-v2.json"))); - this->level_table_v3 = std::make_shared(phosg::JSON::parse(phosg::load_file( - "system/tables/level-table-v3.json"))); - this->level_table_v4 = std::make_shared(phosg::JSON::parse(phosg::load_file( - "system/tables/level-table-v4.json"))); -} - -void ServerState::load_text_index() { - this->text_index = std::make_shared("system/text-sets", [&](Version version, const std::string& filename) -> std::shared_ptr { - try { - if (version == Version::BB_V4) { - return this->load_bb_file(filename); - } else { - return this->pc_patch_file_index->get("Media/PSO/" + filename)->load_data(); - } - } catch (const std::out_of_range&) { - return nullptr; - } catch (const phosg::cannot_open_file&) { - return nullptr; - } - }); -} - -void ServerState::load_word_select_table() { - config_log.info_f("Loading Word Select table"); - - std::vector> name_alias_lists; - auto json = phosg::JSON::parse(phosg::load_file("system/text-sets/ws-name-alias-lists.json")); - for (const auto& coll_it : json.as_list()) { - auto& coll = name_alias_lists.emplace_back(); - for (const auto& str_it : coll_it->as_list()) { - coll.emplace_back(str_it->as_string()); - } - } - - const std::vector* pc_unitxt_collection = nullptr; - const std::vector* bb_unitxt_collection = nullptr; - std::unique_ptr pc_unitxt_data; - if (this->text_index) { - config_log.debug_f("(Word select) Using PC_V2 unitxt_e.prs from text index"); - pc_unitxt_collection = &this->text_index->get(Version::PC_V2, Language::ENGLISH, 35); - } else { - config_log.debug_f("(Word select) Loading PC_V2 unitxt_e.prs"); - pc_unitxt_data = std::make_unique(phosg::load_file("system/text-sets/pc-v2/unitxt_e.prs")); - pc_unitxt_collection = &pc_unitxt_data->get(35); - } - config_log.debug_f("(Word select) Loading BB_V4 unitxt_ws_e.prs"); - auto bb_unitxt_data = std::make_unique(phosg::load_file("system/text-sets/bb-v4/unitxt_ws_e.prs")); - bb_unitxt_collection = &bb_unitxt_data->get(0); - - config_log.debug_f("(Word select) Loading DC_NTE data"); - WordSelectSet dc_nte_ws(phosg::load_file("system/text-sets/dc-nte/ws_data.bin"), Version::DC_NTE, nullptr, true); - config_log.debug_f("(Word select) Loading DC_11_2000 data"); - WordSelectSet dc_112000_ws(phosg::load_file("system/text-sets/dc-11-2000/ws_data.bin"), Version::DC_11_2000, nullptr, false); - config_log.debug_f("(Word select) Loading DC_V1 data"); - WordSelectSet dc_v1_ws(phosg::load_file("system/text-sets/dc-v1/ws_data.bin"), Version::DC_V1, nullptr, false); - config_log.debug_f("(Word select) Loading DC_V2 data"); - WordSelectSet dc_v2_ws(phosg::load_file("system/text-sets/dc-v2/ws_data.bin"), Version::DC_V2, nullptr, false); - config_log.debug_f("(Word select) Loading PC_NTE data"); - WordSelectSet pc_nte_ws(phosg::load_file("system/text-sets/pc-nte/ws_data.bin"), Version::PC_NTE, pc_unitxt_collection, false); - config_log.debug_f("(Word select) Loading PC_V2 data"); - WordSelectSet pc_v2_ws(phosg::load_file("system/text-sets/pc-v2/ws_data.bin"), Version::PC_V2, pc_unitxt_collection, false); - config_log.debug_f("(Word select) Loading GC_NTE data"); - WordSelectSet gc_nte_ws(phosg::load_file("system/text-sets/gc-nte/ws_data.bin"), Version::GC_NTE, nullptr, false); - config_log.debug_f("(Word select) Loading GC_V3 data"); - WordSelectSet gc_v3_ws(phosg::load_file("system/text-sets/gc-v3/ws_data.bin"), Version::GC_V3, nullptr, false); - config_log.debug_f("(Word select) Loading GC_EP3_NTE data"); - WordSelectSet gc_ep3_nte_ws(phosg::load_file("system/text-sets/gc-ep3-nte/ws_data.bin"), Version::GC_EP3_NTE, nullptr, false); - config_log.debug_f("(Word select) Loading GC_EP3 data"); - WordSelectSet gc_ep3_ws(phosg::load_file("system/text-sets/gc-ep3/ws_data.bin"), Version::GC_EP3, nullptr, false); - config_log.debug_f("(Word select) Loading XB_V3 data"); - WordSelectSet xb_v3_ws(phosg::load_file("system/text-sets/xb-v3/ws_data.bin"), Version::XB_V3, nullptr, false); - config_log.debug_f("(Word select) Loading BB_V4 data"); - WordSelectSet bb_v4_ws(phosg::load_file("system/text-sets/bb-v4/ws_data.bin"), Version::BB_V4, bb_unitxt_collection, false); - - config_log.debug_f("(Word select) Generating table"); - this->word_select_table = std::make_shared( - dc_nte_ws, dc_112000_ws, dc_v1_ws, dc_v2_ws, - pc_nte_ws, pc_v2_ws, gc_nte_ws, gc_v3_ws, - gc_ep3_nte_ws, gc_ep3_ws, xb_v3_ws, bb_v4_ws, - name_alias_lists); -} - -std::shared_ptr ServerState::create_item_name_index_for_version( - std::shared_ptr pmt, - std::shared_ptr limits, - std::shared_ptr text_index) const { - switch (limits->version) { - case Version::DC_NTE: - return std::make_shared(pmt, limits, text_index->get(Version::DC_NTE, Language::JAPANESE, 2)); - case Version::DC_11_2000: - return std::make_shared(pmt, limits, text_index->get(Version::DC_11_2000, Language::ENGLISH, 2)); - case Version::DC_V1: - return std::make_shared(pmt, limits, text_index->get(Version::DC_V1, Language::ENGLISH, 2)); - case Version::DC_V2: - return std::make_shared(pmt, limits, text_index->get(Version::DC_V2, Language::ENGLISH, 3)); - case Version::PC_NTE: - return std::make_shared(pmt, limits, text_index->get(Version::PC_NTE, Language::ENGLISH, 3)); - case Version::PC_V2: - return std::make_shared(pmt, limits, text_index->get(Version::PC_V2, Language::ENGLISH, 3)); - case Version::GC_NTE: - return std::make_shared(pmt, limits, text_index->get(Version::GC_NTE, Language::ENGLISH, 0)); - case Version::GC_V3: - return std::make_shared(pmt, limits, text_index->get(Version::GC_V3, Language::ENGLISH, 0)); - case Version::XB_V3: - return std::make_shared(pmt, limits, text_index->get(Version::XB_V3, Language::ENGLISH, 0)); - case Version::BB_V4: - return std::make_shared(pmt, limits, text_index->get(Version::BB_V4, Language::ENGLISH, 1)); - default: - return nullptr; - } -} - -void ServerState::load_item_name_indexes() { - config_log.info_f("Generating item name indexes"); - for (size_t v_s = NUM_PATCH_VERSIONS; v_s < NUM_VERSIONS; v_s++) { - Version v = static_cast(v_s); - config_log.debug_f("Generating item name index for {}", phosg::name_for_enum(v)); - this->item_name_indexes[v_s] = this->create_item_name_index_for_version( - this->item_parameter_table(v), this->item_stack_limits(v), this->text_index); - } - this->item_name_indexes[static_cast(Version::GC_EP3)] = this->item_name_indexes[static_cast(Version::GC_V3)]; - this->item_name_indexes[static_cast(Version::GC_EP3_NTE)] = this->item_name_indexes[static_cast(Version::GC_V3)]; -} - -void ServerState::load_drop_tables() { - config_log.info_f("Loading item sets"); - - std::unordered_map> new_rare_item_sets; - std::unordered_map> new_common_item_sets; - for (const auto& item : std::filesystem::directory_iterator("system/tables")) { - std::string filename = item.path().filename().string(); - - if (filename.starts_with("common-table-") || filename.starts_with("ItemPT-")) { - std::string path = "system/tables/" + filename; - size_t ext_offset = filename.rfind('.'); - std::string basename = (ext_offset == std::string::npos) ? filename : filename.substr(0, ext_offset); - - if (filename.ends_with(".json")) { - config_log.info_f("Loading JSON common item table {}", filename); - new_common_item_sets.emplace(basename, std::make_shared(phosg::JSON::parse(phosg::load_file(path)))); - } else if (filename.ends_with(".afs")) { - std::string ct_filename; - if (filename.starts_with("ItemPT-")) { - ct_filename = "ItemCT-" + filename.substr(7); - } else if (filename.starts_with("common-table-")) { - ct_filename = "challenge-common-table-" + filename.substr(13); - } else { - throw std::runtime_error(std::format("cannot determine challenge table filename for common table file: {}", filename)); - } - auto data = std::make_shared(phosg::load_file(path)); - std::shared_ptr ct_data; - try { - std::string ct_path = "system/tables/" + ct_filename; - ct_data = std::make_shared(phosg::load_file(ct_path)); - config_log.info_f("Loading AFS common item table {} with challenge table {}", filename, ct_filename); - } catch (const phosg::cannot_open_file&) { - config_log.info_f("Loading AFS common item table {} without challenge table", filename); - } - new_common_item_sets.emplace(basename, std::make_shared(data, ct_data)); - } else if (filename.ends_with(".gsl")) { - config_log.info_f("Loading little-endian GSL common item table {}", filename); - auto data = std::make_shared(phosg::load_file(path)); - new_common_item_sets.emplace(basename, std::make_shared(data, false)); - } else if (filename.ends_with(".gslb")) { - config_log.info_f("Loading big-endian GSL common item table {}", filename); - auto data = std::make_shared(phosg::load_file(path)); - new_common_item_sets.emplace(basename, std::make_shared(data, true)); - } else { - throw std::runtime_error(std::format("unknown format for common table file: {}", filename)); - } - - } else if (filename.starts_with("rare-table-") || filename.starts_with("ItemRT-")) { - std::string path = "system/tables/" + filename; - size_t ext_offset = filename.rfind('.'); - std::string basename = (ext_offset == std::string::npos) ? filename : filename.substr(0, ext_offset); - - std::shared_ptr rare_set; - if (filename.ends_with("-v1.json")) { - config_log.info_f("Loading v1 JSON rare item table {}", filename); - rare_set = std::make_shared(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::DC_V1)); - } else if (filename.ends_with("-v2.json")) { - config_log.info_f("Loading v2 JSON rare item table {}", filename); - rare_set = std::make_shared(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::PC_V2)); - } else if (filename.ends_with("-v3.json")) { - config_log.info_f("Loading v3 JSON rare item table {}", filename); - rare_set = std::make_shared(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::GC_V3)); - } else if (filename.ends_with("-v4.json")) { - config_log.info_f("Loading v4 JSON rare item table {}", filename); - rare_set = std::make_shared(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::BB_V4)); - - } else if (filename.ends_with(".afs")) { - config_log.info_f("Loading AFS rare item table {}", filename); - auto data = std::make_shared(phosg::load_file(path)); - rare_set = std::make_shared(AFSArchive(data), false); - - } else if (filename.ends_with(".gsl")) { - config_log.info_f("Loading GSL rare item table {}", filename); - auto data = std::make_shared(phosg::load_file(path)); - rare_set = std::make_shared(GSLArchive(data, false), false); - - } else if (filename.ends_with(".gslb")) { - config_log.info_f("Loading GSL rare item table {}", filename); - auto data = std::make_shared(phosg::load_file(path)); - rare_set = std::make_shared(GSLArchive(data, true), true); - - } else if (filename.ends_with(".rel")) { - config_log.info_f("Loading REL rare item table {}", filename); - rare_set = std::make_shared(phosg::load_file(path), true); - - } else { - throw std::runtime_error(std::format("unknown format for rare table file: {}", filename)); - } - - if (this->server_global_drop_rate_multiplier != 1.0) { - rare_set->multiply_all_rates(this->server_global_drop_rate_multiplier); - } - new_rare_item_sets.emplace(basename, std::move(rare_set)); - } - } - - config_log.info_f("Loading armor table"); - auto armor_json = phosg::JSON::parse(phosg::load_file("system/tables/armor-shop-random-set.json")); - auto new_armor_random_set = std::make_shared(armor_json); - - config_log.info_f("Loading tool table"); - auto tool_json = phosg::JSON::parse(phosg::load_file("system/tables/tool-shop-random-set.json")); - auto new_tool_random_set = std::make_shared(tool_json); - - config_log.info_f("Loading weapon tables"); - std::array, 4> new_weapon_random_sets; - const char* filenames[4] = { - "system/tables/weapon-shop-random-set-normal.json", - "system/tables/weapon-shop-random-set-hard.json", - "system/tables/weapon-shop-random-set-very-hard.json", - "system/tables/weapon-shop-random-set-ultimate.json", - }; - for (size_t z = 0; z < 4; z++) { - new_weapon_random_sets[z] = std::make_shared( - phosg::JSON::parse(phosg::load_file(filenames[z]))); - } - - config_log.info_f("Loading tekker adjustment set"); - auto tekker_data = phosg::JSON::parse(phosg::load_file("system/tables/tekker-adjustment-set.json")); - auto new_tekker_adjustment_set = std::make_shared(tekker_data); - - this->rare_item_sets = std::move(new_rare_item_sets); - this->common_item_sets = std::move(new_common_item_sets); - this->armor_random_set = std::move(new_armor_random_set); - this->tool_random_set = std::move(new_tool_random_set); - this->weapon_random_sets = std::move(new_weapon_random_sets); - this->tekker_adjustment_set = std::move(new_tekker_adjustment_set); -} - -void ServerState::load_item_definitions() { - std::array, NUM_VERSIONS> new_item_parameter_tables; - config_log.info_f("Loading item definition tables"); - for (size_t v_s = NUM_PATCH_VERSIONS; v_s < NUM_VERSIONS; v_s++) { - Version v = static_cast(v_s); - std::string json_path = std::format("system/tables/item-parameter-table-{}.json", file_path_token_for_version(v)); - try { - config_log.debug_f("Loading item definition table {}", json_path); - new_item_parameter_tables[v_s] = ItemParameterTable::from_json(phosg::JSON::parse(phosg::load_file(json_path))); - } catch (const std::exception& e) { - std::string path = std::format("system/tables/ItemPMT-{}.prs", file_path_token_for_version(v)); - config_log.debug_f("Cannot load {} ({}); loading item definition table {}", json_path, e.what(), path); - auto data = std::make_shared(prs_decompress(phosg::load_file(path))); - new_item_parameter_tables[v_s] = ItemParameterTable::from_binary(data, v); - } - } - - auto json = phosg::JSON::parse(phosg::load_file("system/tables/translation-table.json")); - auto new_item_translation_table = std::make_shared(json, new_item_parameter_tables); - - config_log.info_f("Creating DC NTE mag metadata table"); - auto new_table_dc_nte = MagMetadataTable::from_binary(nullptr, Version::DC_NTE); - config_log.info_f("Loading DC 11/2000 mag metadata table"); - auto new_table_11_2000 = MagMetadataTable::from_json(phosg::JSON::parse(phosg::load_file( - "system/tables/mag-metadata-table-dc-11-2000.json"))); - config_log.info_f("Loading v1 mag metadata table"); - auto new_table_v1 = MagMetadataTable::from_json(phosg::JSON::parse(phosg::load_file( - "system/tables/mag-metadata-table-v1.json"))); - config_log.info_f("Loading v2 mag metadata table"); - auto new_table_v2 = MagMetadataTable::from_json(phosg::JSON::parse(phosg::load_file( - "system/tables/mag-metadata-table-v2.json"))); - config_log.info_f("Loading v3 mag metadata table"); - auto new_table_v3 = MagMetadataTable::from_json(phosg::JSON::parse(phosg::load_file( - "system/tables/mag-metadata-table-v3.json"))); - config_log.info_f("Loading v4 mag metadata table"); - auto new_table_v4 = MagMetadataTable::from_json(phosg::JSON::parse(phosg::load_file( - "system/tables/mag-metadata-table-v4.json"))); - - this->item_parameter_tables = std::move(new_item_parameter_tables); - this->item_translation_table = std::move(new_item_translation_table); - this->mag_metadata_table_dc_nte = std::move(new_table_dc_nte); - this->mag_metadata_table_dc_11_2000 = std::move(new_table_11_2000); - this->mag_metadata_table_v1 = std::move(new_table_v1); - this->mag_metadata_table_v2 = std::move(new_table_v2); - this->mag_metadata_table_v3 = std::move(new_table_v3); - this->mag_metadata_table_v4 = std::move(new_table_v4); -} - -void ServerState::load_ep3_cards() { - config_log.info_f("Loading Episode 3 card definitions"); - this->ep3_card_index = std::make_shared( - "system/ep3/card-definitions.mnr", - "system/ep3/card-definitions.mnrd", - "system/ep3/card-text.mnr", - "system/ep3/card-text.mnrd", - "system/ep3/card-dice-text.mnr", - "system/ep3/card-dice-text.mnrd"); - config_log.info_f("Loading Episode 3 trial card definitions"); - this->ep3_card_index_trial = std::make_shared( - "system/ep3/card-definitions-trial.mnr", - "system/ep3/card-definitions-trial.mnrd", - "system/ep3/card-text-trial.mnr", - "system/ep3/card-text-trial.mnrd", - "system/ep3/card-dice-text-trial.mnr", - "system/ep3/card-dice-text-trial.mnrd"); - config_log.info_f("Loading Episode 3 COM decks"); - this->ep3_com_deck_index = std::make_shared("system/ep3/com-decks.json"); -} - -void ServerState::load_ep3_maps(bool raise_on_any_failure) { - config_log.info_f("Collecting Episode 3 maps"); - this->ep3_map_index = std::make_shared("system/ep3/maps", raise_on_any_failure); + this->team_index = std::make_shared("system/teams", this->data->team_reward_defs_json); } void ServerState::load_ep3_tournament_state() { config_log.info_f("Loading Episode 3 tournament state"); const std::string& tournament_state_filename = "system/ep3/tournament-state.json"; this->ep3_tournament_index = std::make_shared( - this->ep3_map_index, this->ep3_com_deck_index, tournament_state_filename); + this->data->ep3_map_index, this->data->ep3_com_deck_index, tournament_state_filename); this->ep3_tournament_index->link_all_clients(this->shared_from_this()); } -void ServerState::load_quest_index(bool raise_on_any_failure) { - config_log.info_f("Collecting quests"); - this->quest_index = std::make_shared("system/quests", this->quest_category_index, raise_on_any_failure); -} - -void ServerState::compile_functions(bool raise_on_any_failure) { - config_log.info_f("Compiling client functions"); - this->client_functions = std::make_shared("system/client-functions", raise_on_any_failure); -} - -void ServerState::load_dol_files() { - config_log.info_f("Loading DOL files"); - this->dol_file_index = std::make_shared("system/dol"); -} - -void ServerState::generate_bb_stream_file() { - config_log.info_f("Generating BB stream file"); - auto sf = std::make_shared(); - - auto add_file = [&](const std::string& filename, const void* data, size_t size) -> void { - auto& e = sf->entries.emplace_back(); - e.offset = sf->data.size(); - e.filename = filename; - e.size = size; - e.checksum = phosg::crc32(data, size); - sf->data.append(reinterpret_cast(data), size); - config_log.debug_f( - "[BBStreamFile] Added file {} at offset {:08X} ({:08X} bytes) with checksum {:08X}; total size is now {:08X}", - filename, e.offset, e.size, e.checksum, sf->data.size()); - }; - - auto level_table_data = prs_compress_optimal(this->level_table_v4->serialize_binary_v4()); - auto pmt_data = prs_compress_optimal(this->item_parameter_table(Version::BB_V4)->serialize_binary(Version::BB_V4)); - auto mag_data = prs_compress_optimal(this->mag_metadata_table(Version::BB_V4)->serialize_binary(Version::BB_V4)); - - const auto& bps = *this->battle_params; - add_file("BattleParamEntry.dat", &bps.get_table(true, Episode::EP1), sizeof(BattleParamsIndex::Table)); - add_file("BattleParamEntry_lab.dat", &bps.get_table(true, Episode::EP2), sizeof(BattleParamsIndex::Table)); - add_file("BattleParamEntry_ep4.dat", &bps.get_table(true, Episode::EP4), sizeof(BattleParamsIndex::Table)); - add_file("BattleParamEntry_on.dat", &bps.get_table(false, Episode::EP1), sizeof(BattleParamsIndex::Table)); - add_file("BattleParamEntry_lab_on.dat", &bps.get_table(false, Episode::EP2), sizeof(BattleParamsIndex::Table)); - add_file("BattleParamEntry_ep4_on.dat", &bps.get_table(false, Episode::EP4), sizeof(BattleParamsIndex::Table)); - add_file("PlyLevelTbl.prs", level_table_data.data(), level_table_data.size()); - add_file("ItemMagEdit.prs", mag_data.data(), mag_data.size()); - add_file("ItemPMT.prs", pmt_data.data(), pmt_data.size()); - - this->bb_stream_file = sf; -} - void ServerState::create_default_lobbies() { if (this->default_lobbies_created) { return; @@ -2286,7 +307,9 @@ void ServerState::create_default_lobbies() { bool allow_non_ep3 = (x <= 14); std::shared_ptr l = this->create_lobby(false); - l->event = this->pre_lobby_event; + l->event = ((l->lobby_id - 1) < this->data->per_lobby_events.size()) + ? this->data->per_lobby_events[l->lobby_id - 1] + : this->data->pre_lobby_event; l->set_flag(Lobby::Flag::PUBLIC); l->set_flag(Lobby::Flag::DEFAULT); l->set_flag(Lobby::Flag::PERSISTENT); @@ -2316,40 +339,20 @@ void ServerState::create_default_lobbies() { } } -void ServerState::load_all(bool enable_thread_pool) { - this->collect_network_addresses(); - this->load_config_early(); - if (enable_thread_pool) { - if (this->num_worker_threads > 0) { - config_log.info_f("Starting thread pool with {} threads", this->num_worker_threads); - this->thread_pool = std::make_unique(this->num_worker_threads); - } else { - config_log.warning_f("WorkerThreads is zero or not set; using default thread count"); +void ServerState::update_default_lobby_events_from_config() { + for (size_t z = 1; z <= 20; z++) { + auto l = this->find_lobby(z); + if (l) { + l->event = 0; + } + } + for (size_t z = 0; z < this->data->per_lobby_events.size(); z++) { + const auto& l = this->find_lobby(z + 1); + if (l && l->check_flag(Lobby::Flag::DEFAULT)) { + l->event = this->data->per_lobby_events[z]; + send_change_event(l, l->event); } } - this->load_bb_private_keys(); - this->load_bb_system_defaults(); - this->load_accounts(); - this->load_patch_indexes(); - this->load_ep3_cards(); - this->load_ep3_maps(); - this->load_ep3_tournament_state(); - this->compile_functions(); - this->load_dol_files(); - this->create_default_lobbies(); - this->load_set_data_tables(); - this->load_maps(); - this->load_battle_params(); - this->load_level_tables(); - this->load_text_index(); - this->load_word_select_table(); - this->load_item_definitions(); - this->load_item_name_indexes(); - this->load_drop_tables(); - this->load_config_late(); - this->load_teams(); - this->load_quest_index(); - this->generate_bb_stream_file(); } void ServerState::reset_between_replays() { @@ -2388,8 +391,7 @@ void ServerState::disconnect_all_banned_clients() { addr = ipv4_addr_for_asio_addr(socket_channel->local_addr.address()); } } - if ((c->login && (c->login->account->ban_end_time > now_usecs)) || - this->banned_ipv4_ranges->check(addr)) { + if ((c->login && (c->login->account->ban_end_time > now_usecs)) || this->data->banned_ipv4_ranges->check(addr)) { c->channel->disconnect(); } } diff --git a/src/ServerState.hh b/src/ServerState.hh index b51ff0eb..44172a04 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -15,6 +15,7 @@ #include "CommonItemSet.hh" #include "DNSServer.hh" #include "DOLFileIndex.hh" +#include "DataIndex.hh" #include "Episode3/DataIndexes.hh" #include "Episode3/Tournament.hh" #include "GSLArchive.hh" @@ -37,291 +38,28 @@ class GameServer; class IPStackSimulator; class HTTPServer; -struct PortConfiguration { - std::string name; - std::string addr; // Blank = listen on all interfaces (default) - uint16_t port; - Version version; - ServerBehavior behavior; -}; +class ServerState : public std::enable_shared_from_this { +public: + std::shared_ptr data; -struct CheatFlags { - // This structure describes which behaviors are considered cheating (that is, require cheat mode to be enabled or the - // user to have the CHEAT_ANYWHERE account flag). A false value here means that that particular behavior is NOT - // cheating, so cheat mode is NOT required. - bool create_items = true; - bool edit_section_id = true; - bool edit_stats = true; - bool ep3_replace_assist = true; - bool ep3_unset_field_character = true; - bool infinite_hp_tp = true; - bool fast_kills = true; - bool insufficient_minimum_level = true; - bool override_random_seed = true; - bool override_section_id = true; - bool override_variations = true; - bool proxy_override_drops = true; - bool reset_materials = false; - bool warp = true; - - CheatFlags() = default; - explicit CheatFlags(const phosg::JSON& json); -}; - -struct BBStreamFile { - struct Entry { - uint32_t offset; - uint32_t size; - uint32_t checksum; // crc32 - std::string filename; - }; - std::vector entries; - std::string data; -}; - -struct ServerState : public std::enable_shared_from_this { - enum class RunShellBehavior { - DEFAULT = 0, - ALWAYS, - NEVER, - }; - enum class BehaviorSwitch { - OFF = 0, - OFF_BY_DEFAULT, - ON_BY_DEFAULT, - ON, - }; - - static inline bool behavior_enabled(BehaviorSwitch b) { - return (b == BehaviorSwitch::ON_BY_DEFAULT) || (b == BehaviorSwitch::ON); - } - static inline bool behavior_can_be_overridden(BehaviorSwitch b) { - return (b == BehaviorSwitch::OFF_BY_DEFAULT) || (b == BehaviorSwitch::ON_BY_DEFAULT); - } - - uint64_t creation_time; std::shared_ptr io_context; + std::shared_ptr thread_pool; - std::string config_filename; - std::shared_ptr config_json; - bool one_time_config_loaded = false; bool default_lobbies_created = false; bool is_replay = false; - - size_t num_worker_threads = 0; - std::unique_ptr thread_pool; - - std::string name; - std::unordered_map> name_to_port_config; - std::unordered_map> number_to_port_config; - std::string username; - std::string dns_server_addr; - uint16_t dns_server_port = 0; - std::vector ip_stack_addresses; - std::vector ppp_stack_addresses; - std::vector ppp_raw_addresses; - std::vector http_addresses; - uint64_t client_ping_interval_usecs = 30000000; - uint64_t client_idle_timeout_usecs = 60000000; - uint64_t patch_client_idle_timeout_usecs = 300000000; - bool is_debug = false; - bool ip_stack_debug = false; - bool allow_unregistered_users = false; - bool allow_pc_nte = false; - bool use_temp_accounts_for_prototypes = true; - bool allow_same_account_concurrent_logins = true; - std::array compatibility_groups = {}; - bool enable_chat_commands = true; - char chat_command_sentinel = '\0'; // 0 = default (@ on 11/2000; $ on all other versions) - size_t num_backup_character_slots = 16; - std::unique_ptr> version_name_colors; - uint32_t client_customization_name_color = 0x00000000; - uint8_t allowed_drop_modes_v1_v2_normal = 0x1F; - uint8_t allowed_drop_modes_v1_v2_battle = 0x07; - uint8_t allowed_drop_modes_v1_v2_challenge = 0x07; - uint8_t allowed_drop_modes_v3_normal = 0x1F; - uint8_t allowed_drop_modes_v3_battle = 0x07; - uint8_t allowed_drop_modes_v3_challenge = 0x07; - uint8_t allowed_drop_modes_v4_normal = 0x1D; // CLIENT not allowed - uint8_t allowed_drop_modes_v4_battle = 0x05; - uint8_t allowed_drop_modes_v4_challenge = 0x05; - ServerDropMode default_drop_mode_v1_v2_normal = ServerDropMode::CLIENT; - ServerDropMode default_drop_mode_v1_v2_battle = ServerDropMode::CLIENT; - ServerDropMode default_drop_mode_v1_v2_challenge = ServerDropMode::CLIENT; - ServerDropMode default_drop_mode_v3_normal = ServerDropMode::CLIENT; - ServerDropMode default_drop_mode_v3_battle = ServerDropMode::CLIENT; - ServerDropMode default_drop_mode_v3_challenge = ServerDropMode::CLIENT; - ServerDropMode default_drop_mode_v4_normal = ServerDropMode::SERVER_SHARED; - ServerDropMode default_drop_mode_v4_battle = ServerDropMode::SERVER_SHARED; - ServerDropMode default_drop_mode_v4_challenge = ServerDropMode::SERVER_SHARED; - std::unordered_map quest_flag_rewrites_v1_v2; - std::unordered_map quest_flag_rewrites_v3; - std::unordered_map quest_flag_rewrites_v4; - std::unordered_map> quest_counter_fields; // For $qfread command - uint64_t persistent_game_idle_timeout_usecs = 0; - std::unordered_map enable_send_function_call_quest_numbers; - bool enable_v3_v4_protected_subcommands = false; - bool ep3_infinite_meseta = false; - std::vector ep3_defeat_player_meseta_rewards = {400, 500, 600, 700, 800}; - std::vector ep3_defeat_com_meseta_rewards = {100, 200, 300, 400, 500}; - uint32_t ep3_final_round_meseta_bonus = 300; - bool ep3_jukebox_is_free = false; - uint32_t ep3_behavior_flags = 0; - bool hide_download_commands = true; - bool censor_credentials = true; - RunShellBehavior run_shell_behavior = RunShellBehavior::DEFAULT; - BehaviorSwitch cheat_mode_behavior = BehaviorSwitch::OFF_BY_DEFAULT; - bool default_switch_assist_enabled = false; - bool use_game_creator_section_id = false; bool use_psov2_rand_crypt = false; // Used in some tests bool use_legacy_item_random_behavior = false; // Used in some tests - bool rare_notifs_enabled_for_client_drops = false; - bool default_rare_notifs_enabled_v1_v2 = false; - bool default_rare_notifs_enabled_v3_v4 = false; - std::unordered_set notify_game_for_item_primary_identifiers_v1_v2; - std::unordered_set notify_game_for_item_primary_identifiers_v3; - std::unordered_set notify_game_for_item_primary_identifiers_v4; - std::unordered_set notify_server_for_item_primary_identifiers_v1_v2; - std::unordered_set notify_server_for_item_primary_identifiers_v3; - std::unordered_set notify_server_for_item_primary_identifiers_v4; - bool notify_server_for_max_level_achieved = false; - std::vector> bb_private_keys; - std::shared_ptr> bb_default_keyboard_config; - std::shared_ptr> bb_default_joystick_config; - std::shared_ptr client_functions; - std::shared_ptr pc_patch_file_index; - std::shared_ptr bb_patch_file_index; - std::unordered_map> map_file_for_source_hash; - std::map, NUM_VERSIONS>> map_files_for_free_play_key; - std::unordered_map> supermap_for_source_hash_sum; - std::unordered_map> supermap_for_free_play_key; - std::shared_ptr room_layout_index; - std::shared_ptr bb_stream_file; - std::shared_ptr dol_file_index; - std::shared_ptr ep3_card_index; - std::shared_ptr ep3_card_index_trial; - std::shared_ptr ep3_map_index; - std::shared_ptr ep3_com_deck_index; - std::shared_ptr ep3_default_ex_values; - std::shared_ptr ep3_tournament_ex_values; - std::shared_ptr ep3_tournament_final_round_ex_values; - std::shared_ptr quest_category_index; - std::shared_ptr quest_index; - std::shared_ptr level_table_v1_v2; - std::shared_ptr level_table_v3; - std::shared_ptr level_table_v4; - std::shared_ptr battle_params; - std::shared_ptr bb_data_gsl; - std::unordered_map> common_item_sets; - std::unordered_map> rare_item_sets; - std::shared_ptr armor_random_set; - std::shared_ptr tool_random_set; - std::array, 4> weapon_random_sets; // Keyed on difficulty - std::shared_ptr tekker_adjustment_set; - std::array, NUM_VERSIONS> item_parameter_tables; - std::shared_ptr item_translation_table; - std::array, NUM_VERSIONS> item_stack_limits_tables; - size_t bb_max_bank_items = 200; - size_t bb_max_bank_meseta = 999999; - std::shared_ptr mag_metadata_table_dc_nte; - std::shared_ptr mag_metadata_table_dc_11_2000; - std::shared_ptr mag_metadata_table_v1; - std::shared_ptr mag_metadata_table_v2; - std::shared_ptr mag_metadata_table_v3; - std::shared_ptr mag_metadata_table_v4; - std::shared_ptr text_index; - std::array, NUM_VERSIONS> item_name_indexes; - std::shared_ptr word_select_table; - std::array, NUM_VERSIONS> set_data_tables; - std::array, NUM_VERSIONS> set_data_tables_ep1_ult; - std::shared_ptr bb_solo_set_data_table; - std::shared_ptr bb_solo_set_data_table_ep1_ult; - std::array, 4> rare_enemy_rates_by_difficulty; - std::shared_ptr rare_enemy_rates_challenge; - std::array, 3> min_levels_v1_v2; // Indexed as [episode][difficulty] - std::array, 3> min_levels_v3; // Indexed as [episode][difficulty] - std::array, 3> min_levels_v4; // Indexed as [episode][difficulty] - std::unordered_set bb_required_patches; - std::unordered_set auto_patches; - CheatFlags cheat_flags; - - struct QuestF960Result { - uint32_t meseta_cost = 0; - uint32_t base_probability = 0; - uint32_t probability_upgrade = 0; - std::array, 7> results; - - QuestF960Result() = default; - QuestF960Result( - const phosg::JSON& json, std::shared_ptr name_index, const ItemData::StackLimits& limits); - }; - - // Indexed as [type][difficulty][random_choice] - std::vector>> quest_F95E_results; - std::vector> quest_F95F_results; // [(num_photon_tickets, item)] - std::vector quest_F960_success_results; - QuestF960Result quest_F960_failure_results; - float bb_global_exp_multiplier = 1.0f; - float exp_share_multiplier = 0.5f; - float server_global_drop_rate_multiplier = 1.0f; std::shared_ptr ep3_tournament_index; - uint16_t ep3_card_auction_points = 0; - uint16_t ep3_card_auction_min_size = 0; - uint16_t ep3_card_auction_max_size = 0; - struct CardAuctionPoolEntry { - uint64_t probability; - uint16_t card_id; - uint16_t min_price; - }; - std::vector ep3_card_auction_pool; - std::array, 5> ep3_trap_card_ids; - struct Ep3LobbyBannerEntry { - uint32_t type = 1; - uint32_t which; // See B9 documentation in CommandFormats.hh - std::string data; - }; - std::vector ep3_lobby_banners; - std::shared_ptr account_index; - std::shared_ptr banned_ipv4_ranges; std::shared_ptr team_index; - phosg::JSON team_reward_defs_json; - - std::shared_ptr information_menu_v2; - std::shared_ptr information_menu_v3; - std::shared_ptr> information_contents_v2; - std::shared_ptr> information_contents_v3; - std::shared_ptr proxy_destinations_menu_dc; - std::shared_ptr proxy_destinations_menu_pc; - std::shared_ptr proxy_destinations_menu_gc; - std::shared_ptr proxy_destinations_menu_xb; - std::vector> proxy_destinations_dc; - std::vector> proxy_destinations_pc; - std::vector> proxy_destinations_gc; - std::vector> proxy_destinations_xb; - std::optional> proxy_destination_patch; - std::optional> proxy_destination_bb; - std::string welcome_message; - std::string pc_patch_server_message; - std::string bb_patch_server_message; std::map> id_to_lobby; - std::array, NUM_VERSIONS> public_lobby_search_orders; - std::vector client_customization_public_lobby_search_order; std::atomic next_lobby_id = 1; - uint8_t pre_lobby_event = 0; - int32_t ep3_menu_song = -1; std::unordered_map> client_for_account; - std::map all_addresses; - uint32_t local_address = 0; - uint32_t external_address = 0; - - bool proxy_allow_save_files = true; - std::shared_ptr ip_stack_simulator; std::shared_ptr dns_server; std::shared_ptr game_server; @@ -329,11 +67,8 @@ struct ServerState : public std::enable_shared_from_this { std::unordered_map proxy_persistent_configs; - explicit ServerState(const std::string& config_filename = "", bool is_replay = false); - ServerState(const ServerState&) = delete; - ServerState(ServerState&&) = delete; - ServerState& operator=(const ServerState&) = delete; - ServerState& operator=(ServerState&&) = delete; + static std::shared_ptr create_shared(std::shared_ptr data_index, bool is_replay); + std::shared_ptr clone_shared(); void add_client_to_available_lobby(std::shared_ptr c, bool allow_games); void remove_client_from_lobby(std::shared_ptr c); @@ -343,8 +78,7 @@ struct ServerState : public std::enable_shared_from_this { bool send_join_notification = true, ssize_t required_client_id = -1); - void send_lobby_join_notifications(std::shared_ptr l, - std::shared_ptr joining_client); + void send_lobby_join_notifications(std::shared_ptr l, std::shared_ptr joining_client); std::shared_ptr find_lobby(uint32_t lobby_id); std::vector> all_lobbies(); @@ -356,110 +90,20 @@ struct ServerState : public std::enable_shared_from_this { std::shared_ptr find_client( const std::string* identifier = nullptr, uint64_t account_id = 0, std::shared_ptr l = nullptr); - uint32_t connect_address_for_client(std::shared_ptr c) const; - uint16_t game_server_port_for_version(Version v) const; - - std::shared_ptr information_menu(Version version) const; - std::shared_ptr proxy_destinations_menu(Version version) const; - const std::vector>& proxy_destinations(Version version) const; - - std::shared_ptr set_data_table(Version version, Episode episode, GameMode mode, Difficulty difficulty) const; - - inline std::shared_ptr weapon_random_set(Difficulty difficulty) const { - return this->weapon_random_sets.at(static_cast(difficulty)); - } - inline std::shared_ptr rare_enemy_rates(Difficulty difficulty) const { - return this->rare_enemy_rates_by_difficulty.at(static_cast(difficulty)); - } - - std::shared_ptr level_table(Version version) const; - std::shared_ptr item_parameter_table(Version version) const; - std::shared_ptr item_parameter_table_for_encode(Version version) const; - std::shared_ptr mag_metadata_table(Version version) const; - std::shared_ptr item_stack_limits(Version version) const; - std::shared_ptr item_name_index_opt(Version version) const; // Returns null if missing - std::shared_ptr item_name_index(Version version) const; // Throws if missing - std::string describe_item(Version version, const ItemData& item, uint8_t flags = 0) const; - ItemData parse_item_description(Version version, const std::string& description) const; - - std::shared_ptr common_item_set(Version logic_version, std::shared_ptr q) const; - std::shared_ptr rare_item_set(Version logic_version, std::shared_ptr q) const; - - const std::vector& public_lobby_search_order(Version version, bool is_client_customization) const; - inline const std::vector& public_lobby_search_order(std::shared_ptr c) const { - return this->public_lobby_search_order(c->version(), c->check_flag(Client::Flag::IS_CLIENT_CUSTOMIZATION)); - } - - inline uint32_t name_color_for_client(Version v, bool is_client_customization) const { - if (is_client_customization && this->client_customization_name_color) { - return this->client_customization_name_color; - } - return this->version_name_colors ? this->version_name_colors->at(static_cast(v) - NUM_PATCH_VERSIONS) : 0; - } - inline uint32_t name_color_for_client(std::shared_ptr c) const { - return this->name_color_for_client(c->version(), c->check_flag(Client::Flag::IS_CLIENT_CUSTOMIZATION)); - } - - std::shared_ptr> information_contents_for_client(std::shared_ptr c) const; - - size_t default_min_level_for_game(Version version, Episode episode, Difficulty difficulty) const; - - void set_port_configuration(const std::vector& port_configs); - - std::shared_ptr load_bb_file(const std::string& patch_index_filename) const; - std::shared_ptr load_map_file(Version version, const std::string& filename) const; - std::shared_ptr load_map_file_uncached(Version version, const std::string& filename) const; - - std::pair parse_port_spec(const phosg::JSON& json) const; - std::vector parse_port_configuration(const phosg::JSON& json) const; - - static constexpr uint32_t free_play_key( - Episode episode, GameMode mode, Difficulty difficulty, uint8_t floor, uint32_t layout, uint32_t entities) { - return (static_cast(episode) << 28) | - (static_cast(mode) << 26) | - (static_cast(difficulty) << 24) | - (static_cast(floor) << 16) | - (static_cast(layout) << 8) | - (static_cast(entities) << 0); - } - std::shared_ptr get_free_play_supermap( - Episode episode, GameMode mode, Difficulty difficulty, uint8_t floor, uint32_t layout, uint32_t entities); - std::vector> supermaps_for_variations( - Episode episode, GameMode mode, Difficulty difficulty, const Variations& variations); - void create_default_lobbies(); - void collect_network_addresses(); - void load_config_early(); - void load_config_late(); - void load_bb_private_keys(); - void load_bb_system_defaults(); void load_accounts(); void load_teams(); - void load_patch_indexes(); - void load_maps(); - void load_battle_params(); - void load_level_tables(); - void load_text_index(); - std::shared_ptr create_item_name_index_for_version( - std::shared_ptr pmt, - std::shared_ptr limits, - std::shared_ptr text_index) const; - void load_item_name_indexes(); - void load_drop_tables(); - void load_item_definitions(); - void load_set_data_tables(); - void load_word_select_table(); - void load_ep3_cards(); - void load_ep3_maps(bool raise_on_any_failure = false); void load_ep3_tournament_state(); - void load_quest_index(bool raise_on_any_failure = false); - void compile_functions(bool raise_on_any_failure = false); - void load_dol_files(); - void generate_bb_stream_file(); - - void load_all(bool enable_thread_pool); + void update_default_lobby_events_from_config(); void reset_between_replays(); void disconnect_all_banned_clients(); + +protected: + ServerState() = default; + ServerState(const ServerState&) = delete; + ServerState(ServerState&&) = delete; + ServerState& operator=(const ServerState&) = delete; + ServerState& operator=(ServerState&&) = delete; }; diff --git a/src/ShellCommands.cc b/src/ShellCommands.cc index 8b234aa9..f8181731 100644 --- a/src/ShellCommands.cc +++ b/src/ShellCommands.cc @@ -185,51 +185,55 @@ ShellCommand c_reload( auto types = phosg::split(args.args, ' '); for (const auto& type : types) { if (type == "all") { - args.s->load_all(true); + args.s->data->load_all(); + args.s->disconnect_all_banned_clients(); + args.s->update_default_lobby_events_from_config(); } else if (type == "bb-keys") { - args.s->load_bb_private_keys(); + args.s->data->load_bb_private_keys(); } else if (type == "accounts") { args.s->load_accounts(); } else if (type == "maps") { - args.s->load_maps(); + args.s->data->load_maps(); } else if (type == "patch-files") { - args.s->load_patch_indexes(); + args.s->data->load_patch_indexes(); } else if (type == "ep3-cards") { - args.s->load_ep3_cards(); + args.s->data->load_ep3_cards(); } else if (type == "ep3-maps") { - args.s->load_ep3_maps(); + args.s->data->load_ep3_maps(); } else if (type == "ep3-tournaments") { args.s->load_ep3_tournament_state(); } else if (type == "functions") { - args.s->compile_functions(); + args.s->data->compile_functions(); } else if (type == "dol-files") { - args.s->load_dol_files(); + args.s->data->load_dol_files(); } else if (type == "set-tables") { - args.s->load_set_data_tables(); + args.s->data->load_set_data_tables(); } else if (type == "battle-params") { - args.s->load_battle_params(); - args.s->generate_bb_stream_file(); + args.s->data->load_battle_params(); + args.s->data->generate_bb_stream_file(); } else if (type == "level-tables") { - args.s->load_level_tables(); - args.s->generate_bb_stream_file(); + args.s->data->load_level_tables(); + args.s->data->generate_bb_stream_file(); } else if (type == "text-index") { - args.s->load_text_index(); + args.s->data->load_text_index(); } else if (type == "word-select") { - args.s->load_word_select_table(); + args.s->data->load_word_select_table(); } else if (type == "item-definitions") { - args.s->load_item_definitions(); - args.s->generate_bb_stream_file(); + args.s->data->load_item_definitions(); + args.s->data->generate_bb_stream_file(); } else if (type == "item-name-index") { - args.s->load_item_name_indexes(); + args.s->data->load_item_name_indexes(); } else if (type == "drop-tables") { - args.s->load_drop_tables(); + args.s->data->load_drop_tables(); } else if (type == "config") { - args.s->load_config_early(); - args.s->load_config_late(); + args.s->data->load_config_early(); + args.s->data->load_config_late(); + args.s->disconnect_all_banned_clients(); + args.s->update_default_lobby_events_from_config(); } else if (type == "teams") { args.s->load_teams(); } else if (type == "quests") { - args.s->load_quest_index(); + args.s->data->load_quest_index(); } else { throw std::runtime_error("invalid data type: " + type); } @@ -663,7 +667,7 @@ ShellCommand c_announce_mail( "announce-mail", "announce-mail MESSAGE\n\ Send an announcement message via Simple Mail to all players.", +[](ShellCommand::Args& args) -> asio::awaitable> { - send_simple_mail(args.s, 0, args.s->name, args.args); + send_simple_mail(args.s, 0, args.s->data->name, args.args); co_return std::deque{}; }); @@ -698,7 +702,7 @@ ShellCommand c_create_tournament( +[](ShellCommand::Args& args) -> asio::awaitable> { std::string name = get_quoted_string(args.args); std::string map_name = get_quoted_string(args.args); - auto map = args.s->ep3_map_index->map_for_name(map_name); + auto map = args.s->data->ep3_map_index->map_for_name(map_name); uint32_t num_teams = std::stoul(get_quoted_string(args.args), nullptr, 0); Episode3::Rules rules; rules.set_defaults(); @@ -1089,13 +1093,14 @@ ShellCommand c_create_item( throw std::runtime_error("proxy session is not game leader"); } - ItemData item = args.s->parse_item_description(c->version(), args.args); + ItemData item = args.s->data->parse_item_description(c->version(), args.args); item.id = phosg::random_object() | 0x80000000; send_drop_stacked_item_to_channel(args.s, c->channel, item, c->floor, c->pos); send_drop_stacked_item_to_channel(args.s, c->proxy_session->server_channel, item, c->floor, c->pos); - std::string name = args.s->describe_item(c->version(), item, ItemNameIndex::Flag::INCLUDE_PSO_COLOR_ESCAPES); + std::string name = args.s->data->describe_item( + c->version(), item, ItemNameIndex::Flag::INCLUDE_PSO_COLOR_ESCAPES); send_text_message(c->channel, "$C7Item created:\n" + name); co_return std::deque{}; }); diff --git a/src/SignalWatcher.cc b/src/SignalWatcher.cc index 4f9dde4c..8ecfbc36 100644 --- a/src/SignalWatcher.cc +++ b/src/SignalWatcher.cc @@ -27,8 +27,10 @@ asio::awaitable SignalWatcher::signal_handler_task() { case SIGUSR1: this->log.info_f("Received SIGUSR1; reloading config.json"); try { - this->state->load_config_early(); - this->state->load_config_late(); + this->state->data->load_config_early(); + this->state->data->load_config_late(); + this->state->disconnect_all_banned_clients(); + this->state->update_default_lobby_events_from_config(); phosg::fwrite_fmt(stderr, "Configuration update complete\n"); } catch (const std::exception& e) { phosg::fwrite_fmt(stderr, "FAILED: {}\n", e.what()); @@ -38,7 +40,9 @@ asio::awaitable SignalWatcher::signal_handler_task() { case SIGUSR2: this->log.info_f("Received SIGUSR2; reloading config.json and all dependencies"); try { - this->state->load_all(true); + this->state->data->load_all(); + this->state->disconnect_all_banned_clients(); + this->state->update_default_lobby_events_from_config(); phosg::fwrite_fmt(stderr, "Configuration update complete\n"); } catch (const std::exception& e) { phosg::fwrite_fmt(stderr, "FAILED: {}\n", e.what()); diff --git a/tests/DC-11-2000-GameSmokeTest.test.txt b/tests/DC-11-2000-GameSmokeTest.test.txt index b8ba1cbb..989da931 100644 --- a/tests/DC-11-2000-GameSmokeTest.test.txt +++ b/tests/DC-11-2000-GameSmokeTest.test.txt @@ -411,6 +411,7 @@ I 72448 2025-05-20 20:08:45 - [Commands] Received from C-1 (Kira Lv.1) @ ip:172. 0010 | A6 31 06 C2 00 00 00 00 A0 4C 89 3E | 1 L > I 72448 2025-05-20 20:08:46 - [Commands] Received from C-1 (Kira Lv.1) @ ip:172.16.0.30:64731 (version=DC_11_2000 command=60 flag=00) 0000 | 60 00 10 00 4C 03 00 00 0A 00 00 00 00 40 00 00 | ` L @ +### cc @variations 00020311101000212120000000000090 I 72448 2025-05-20 20:09:02 - [Commands] Received from C-1 (Kira Lv.1) @ ip:172.16.0.30:64731 (version=DC_11_2000 command=0C flag=03) 0000 | 0C 03 2C 00 00 00 00 00 00 00 00 00 41 41 41 41 | , AAAA 0010 | 41 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | A From 3cdb5e91a87ddf1ccb8922cd5e4765e9e2c45e36 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sun, 14 Jun 2026 10:03:51 -0700 Subject: [PATCH 9/9] name Root structures appropriately --- src/CommonItemSet.cc | 2 +- src/CommonItemSet.hh | 8 ++++---- src/LevelTable.hh | 2 +- src/RareItemSet.cc | 4 ++-- src/RareItemSet.hh | 8 ++++---- system/tables/common-table-v3-v4.json | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/CommonItemSet.cc b/src/CommonItemSet.cc index 85077906..5bf7aedc 100644 --- a/src/CommonItemSet.cc +++ b/src/CommonItemSet.cc @@ -703,7 +703,7 @@ CommonItemSet::Table::Table(const phosg::StringReader& r, bool is_big_endian, bo template void CommonItemSet::Table::parse_itempt_t(const phosg::StringReader& r, bool is_v3) { - const auto& offsets = r.pget>(r.pget>(r.size() - 0x10)); + const auto& offsets = r.pget>(r.pget>(r.size() - 0x10)); this->base_weapon_type_prob_table = r.pget>(offsets.base_weapon_type_prob_table_offset); this->subtype_base_table = r.pget>(offsets.subtype_base_table_offset); diff --git a/src/CommonItemSet.hh b/src/CommonItemSet.hh index e226f676..bfaa2c02 100644 --- a/src/CommonItemSet.hh +++ b/src/CommonItemSet.hh @@ -69,7 +69,7 @@ public: void parse_itempt_t(const phosg::StringReader& r, bool is_v3); template - struct OffsetsT { + struct RootT { // This data structure uses index probability tables in multiple places. An index probability table is a table // where each entry holds the probability that that entry's index is used. For example, if the armor slot count // probability table contains [77, 17, 5, 1, 0], this means there is a 77% chance of no slots, 17% chance of 1 @@ -244,9 +244,9 @@ public: /* 50 */ U32T box_item_class_prob_table_offset; // There are several unused fields here. - } __packed_ws_be__(OffsetsT, 0x54); - using Offsets = OffsetsT; - using OffsetsBE = OffsetsT; + } __packed_ws_be__(RootT, 0x54); + using Root = RootT; + using RootBE = RootT; }; bool operator==(const CommonItemSet& other) const = default; diff --git a/src/LevelTable.hh b/src/LevelTable.hh index cbdd8d2a..503e1587 100644 --- a/src/LevelTable.hh +++ b/src/LevelTable.hh @@ -154,7 +154,7 @@ using LevelStatsDeltaBE = LevelStatsDeltaT; class LevelTable { // This is the base class for all the LevelTable implementations. The public interface here only defines functions // that the server needs to handle requests, but some subclasses implement more functionality. See the comments and - // Offsets structures inside the subclasses' constructor implementations for more details on the file formats. + // Root structures inside the subclasses' constructor implementations for more details on the file formats. public: virtual ~LevelTable() = default; diff --git a/src/RareItemSet.cc b/src/RareItemSet.cc index 41ac1ecf..10c99181 100644 --- a/src/RareItemSet.cc +++ b/src/RareItemSet.cc @@ -89,7 +89,7 @@ RareItemSet::ExpandedDrop RareItemSet::ParsedRELData::PackedDrop::expand() const template void RareItemSet::ParsedRELData::parse_t(phosg::StringReader r, bool is_v1) { const auto& footer = r.pget>(r.size() - sizeof(RELFileFooterT)); - const auto& root = r.pget>(footer.root_offset); + const auto& root = r.pget>(footer.root_offset); phosg::StringReader monsters_r = r.sub(root.monster_rares_offset); for (size_t z = 0; z < (is_v1 ? 0x33 : 0x65); z++) { @@ -114,7 +114,7 @@ template std::string RareItemSet::ParsedRELData::serialize_t(bool is_v1) const { static const PackedDrop empty_drop; - OffsetsT root; + RootT root; root.box_count = this->box_rares.size(); phosg::StringWriter w; diff --git a/src/RareItemSet.hh b/src/RareItemSet.hh index a16d716b..418b7736 100644 --- a/src/RareItemSet.hh +++ b/src/RareItemSet.hh @@ -87,15 +87,15 @@ protected: } __packed_ws__(PackedDrop, 4); template - struct OffsetsT { + struct RootT { /* 00 */ U32T monster_rares_offset; // -> parray (or 0x33 on v1) /* 04 */ U32T box_count; // Usually 30 (0x1E) /* 08 */ U32T box_areas_offset; // -> parray /* 0C */ U32T box_rares_offset; // -> parray /* 10 */ - } __packed_ws_be__(OffsetsT, 0x10); - using Offsets = OffsetsT; - using OffsetsBE = OffsetsT; + } __packed_ws_be__(RootT, 0x10); + using Root = RootT; + using RootBE = RootT; struct BoxRare { uint8_t area_norm_plus_1; diff --git a/system/tables/common-table-v3-v4.json b/system/tables/common-table-v3-v4.json index a41239e5..2d705673 100644 --- a/system/tables/common-table-v3-v4.json +++ b/system/tables/common-table-v3-v4.json @@ -1,6 +1,6 @@ { // This file specifies how non-rare items should be generated. For details on how each field works, see the comments - // in CommonItemSet::Table::OffsetsT (in CommonItemSet.hh). + // in CommonItemSet::Table::RootT (in CommonItemSet.hh). // Each scenario has defaults which are automatically propagated from the previous scenario unless overridden. The // order of precedence is: // - If the section ID is not Viridia, the defaults are from the same episode, game mode, and difficulty, but