diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5773b953..fe3eea94 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -60,10 +60,12 @@ set(SOURCES
src/ChatCommands.cc
src/ChoiceSearch.cc
src/Client.cc
+ src/ClientFunctionIndex.cc
src/CommonItemSet.cc
src/Compression.cc
src/DCSerialNumbers.cc
src/DNSServer.cc
+ src/DOLFileIndex.cc
src/DownloadSession.cc
src/EnemyType.cc
src/Episode3/AssistServer.cc
@@ -79,7 +81,6 @@ set(SOURCES
src/Episode3/Server.cc
src/Episode3/Tournament.cc
src/FileContentsCache.cc
- src/FunctionCompiler.cc
src/GameServer.cc
src/GSLArchive.cc
src/HTTPServer.cc
diff --git a/README.md b/README.md
index de03fe25..cc1e5048 100644
--- a/README.md
+++ b/README.md
@@ -594,6 +594,7 @@ Some commands only work for clients not in proxy sessions. The chat commands are
* You'll see in-game messages from the server when you take some actions, like killing enemies, opening boxes, or flipping switches.
* You'll see the rare seed value and floor variations when you join a game.
* You'll be placed into the last available slot in lobbies and games instead of the first, unless you're joining a BB solo-mode game.
+ * You'll be able to run any client function with `$patch`, not only those that are marked visible.
* You'll be able to join games with any PSO version, not only those for which cross-version play is normally enabled. See the "Cross-version play" section above for details on this.
* `$readmem
`: Read 4 bytes from the given address and show you the values.
* `$writemem `: Write data to the given address. Data is not required to be any specific size.
@@ -622,7 +623,7 @@ Some commands only work for clients not in proxy sessions. The chat commands are
* `$ln [name-or-type]`: Set the lobby number. Visible only to you. This command exists because some non-lobby maps can be loaded as lobbies with invalid lobby numbers. See the "GC lobby types" and "Ep3 lobby types" entries in the information menu for acceptable values here. Note that non-lobby maps do not have a lobby counter, so there's no way to exit the lobby without using either `$ln` again or `$exit`. On the game server, `$ln` reloads the lobby immediately; on the proxy, it doesn't take effect until you load another lobby yourself (which means you'll like have to use `$exit` to escape). Run this command with no argument to return to the default lobby.
* `$swa`: Enable or disable switch assist. When enabled, the server will unlock two-player and four-player doors in non-quest games when you step on any of the required switches.
* `$exit`: If you're in a lobby, send you to the main menu (which ends your proxy session, if you're in one). If you're in a game or spectator team, send you to the lobby (but does not end your proxy session if you're in one). Does nothing if you're in a non-Episode 3 game and no quest is in progress.
- * `$patch `: Run a patch on your client. `` must exactly match the name of a patch on the server.
+ * `$patch `: Run a client function. `` must exactly match the name of a client function on the server.
* Character data commands (non-proxy only)
* `$switchchar ` (BB only): Switch to a different character from your account without logging out.
diff --git a/notes/generate-patches.py b/notes/generate-patches.py
index 835a2db7..2542b4cb 100644
--- a/notes/generate-patches.py
+++ b/notes/generate-patches.py
@@ -4,7 +4,6 @@ import subprocess
import sys
from dataclasses import dataclass
-
version_tokens = ("3OJ2", "3OJ3", "3OJ4", "3OJ5", "3OE0", "3OE1", "3OE2", "3OP0")
@@ -62,7 +61,7 @@ def write_patches_for_code(
f.write("reloc0:\n")
f.write(" .offsetof start\n")
f.write("start:\n")
- f.write(" .include WriteCodeBlocksGC\n")
+ f.write(" .include WriteCodeBlocks\n")
for region in write_regions:
f.write(
f" # region @ {region.address:08X} ({len(region.data) * 4} bytes)\n"
diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc
index 3a242e69..116190ed 100644
--- a/src/ChatCommands.cc
+++ b/src/ChatCommands.cc
@@ -1111,9 +1111,9 @@ ChatCommandDefinition cc_exit(
a.c->check_flag(Client::Flag::SEND_FUNCTION_CALL_ACTUALLY_RUNS_CODE)) {
co_await prepare_client_for_patches(a.c);
auto s = a.c->require_server_state();
- shared_ptr fn;
+ shared_ptr fn;
try {
- fn = s->function_code_index->get_patch("ExitAnywhere", a.c->specific_version);
+ fn = s->client_functions->get("ExitAnywhere", a.c->specific_version);
} catch (const out_of_range&) {
}
if (fn) {
@@ -1571,7 +1571,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->function_code_index->get_patch("SetExtendedPlayerInfo", a.c->specific_version);
+ auto fn = s->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) {
@@ -1707,7 +1707,7 @@ ChatCommandDefinition cc_makeobj(
co_await prepare_client_for_patches(a.c);
auto s = a.c->require_server_state();
- auto fn = s->function_code_index->get_patch("CreateObject", a.c->specific_version);
+ auto fn = s->client_functions->get("CreateObject", a.c->specific_version);
co_await send_function_call(a.c, fn, label_writes);
});
@@ -1847,14 +1847,31 @@ 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->function_code_index->get_patch(patch_name, a.c->specific_version);
+ auto fn = s->client_functions->get(patch_name, a.c->specific_version);
+
+ switch (fn->visibility) {
+ case ClientFunctionIndex::Function::Visibility::DEBUG_ONLY:
+ case ClientFunctionIndex::Function::Visibility::PATCHES_MENU_ONLY:
+ a.check_debug_enabled();
+ break;
+ case ClientFunctionIndex::Function::Visibility::CHAT_COMMAND_ONLY_WITH_CHEAT_MODE:
+ a.check_cheats_enabled_or_allowed(true);
+ break;
+ case ClientFunctionIndex::Function::Visibility::CHAT_COMMAND_ONLY:
+ case ClientFunctionIndex::Function::Visibility::PATCHES_MENU_AND_CHAT_COMMAND:
+ break;
+ default:
+ throw std::logic_error("Invalid client function visibility");
+ }
+
auto ret = co_await send_function_call(a.c, fn, label_writes);
if (fn->show_return_value) {
send_text_message_fmt(a.c, "$C6Return value:$C7\nInt: {}\nHex: {:08X}\nFloat: {:g}",
ret.return_value.load(), ret.return_value.load(), std::bit_cast(ret.return_value.load()));
}
+
} catch (const out_of_range&) {
- send_text_message(a.c, "$C6Invalid patch name");
+ send_text_message(a.c, "$C6Invalid function");
}
co_return;
});
@@ -2277,15 +2294,10 @@ ChatCommandDefinition cc_readmem(
co_await prepare_client_for_patches(a.c);
- shared_ptr fn;
+ shared_ptr fn;
try {
auto s = a.c->require_server_state();
- const char* function_name = is_dc(a.c->version())
- ? "ReadMemoryWordDC"
- : is_gc(a.c->version())
- ? "ReadMemoryWordGC"
- : "ReadMemoryWordX86";
- fn = s->function_code_index->name_to_function.at(function_name);
+ fn = s->client_functions->get("ReadMemoryWord", a.c->specific_version);
} catch (const out_of_range&) {
throw precondition_failed("Invalid patch name");
}
@@ -3139,12 +3151,7 @@ ChatCommandDefinition cc_writemem(
try {
auto s = a.c->require_server_state();
- const char* function_name = is_dc(a.c->version())
- ? "WriteMemoryDC"
- : is_gc(a.c->version())
- ? "WriteMemoryGC"
- : "WriteMemoryX86";
- auto fn = s->function_code_index->name_to_function.at(function_name);
+ auto fn = s->client_functions->get("WriteMemory", a.c->specific_version);
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 out_of_range&) {
@@ -3184,12 +3191,7 @@ ChatCommandDefinition cc_nativecall(
try {
auto s = a.c->require_server_state();
- const char* function_name = is_dc(a.c->version())
- ? "CallNativeFunctionDC"
- : is_gc(a.c->version())
- ? "CallNativeFunctionGC"
- : "CallNativeFunctionX86";
- auto fn = s->function_code_index->name_to_function.at(function_name);
+ auto fn = s->client_functions->get("CallNativeFunction", a.c->specific_version);
co_await send_function_call(a.c, fn, label_writes);
} catch (const out_of_range&) {
throw precondition_failed("Invalid patch name");
diff --git a/src/Client.hh b/src/Client.hh
index e9f44733..09c896f3 100644
--- a/src/Client.hh
+++ b/src/Client.hh
@@ -6,11 +6,11 @@
#include "Account.hh"
#include "AsyncUtils.hh"
#include "Channel.hh"
+#include "ClientFunctionIndex.hh"
#include "CommandFormats.hh"
#include "Episode3/BattleRecord.hh"
#include "Episode3/Tournament.hh"
#include "FileContentsCache.hh"
-#include "FunctionCompiler.hh"
#include "PSOEncryption.hh"
#include "PSOProtocol.hh"
#include "PatchFileIndex.hh"
diff --git a/src/ClientFunctionIndex.cc b/src/ClientFunctionIndex.cc
new file mode 100644
index 00000000..abb39a73
--- /dev/null
+++ b/src/ClientFunctionIndex.cc
@@ -0,0 +1,558 @@
+#include "ClientFunctionIndex.hh"
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#include "CommandFormats.hh"
+#include "CommonFileFormats.hh"
+#include "Compression.hh"
+#include "Loggers.hh"
+
+using namespace std;
+
+using Arch = ClientFunctionIndex::Function::Architecture;
+
+const char* name_for_architecture(Arch arch) {
+ switch (arch) {
+ case Arch::SH4:
+ return "SH-4";
+ case Arch::POWERPC:
+ return "PowerPC";
+ case Arch::X86:
+ return "x86";
+ default:
+ throw logic_error("invalid architecture");
+ }
+}
+
+uint32_t specific_version_for_architecture(Arch arch) {
+ switch (arch) {
+ case Arch::SH4:
+ return SPECIFIC_VERSION_SH4_INDETERMINATE;
+ case Arch::POWERPC:
+ return SPECIFIC_VERSION_PPC_INDETERMINATE;
+ case Arch::X86:
+ return SPECIFIC_VERSION_X86_INDETERMINATE;
+ default:
+ throw logic_error("invalid architecture");
+ }
+}
+
+Arch architecture_for_specific_version(uint32_t specific_version) {
+ if (specific_version == SPECIFIC_VERSION_SH4_INDETERMINATE) {
+ return Arch::SH4;
+ } else if (specific_version == SPECIFIC_VERSION_PPC_INDETERMINATE) {
+ return Arch::POWERPC;
+ } else if (specific_version == SPECIFIC_VERSION_X86_INDETERMINATE) {
+ return Arch::X86;
+ } else if (specific_version_is_dc(specific_version)) {
+ return Arch::SH4;
+ } else if (specific_version_is_gc(specific_version)) {
+ return Arch::POWERPC;
+ } else {
+ return Arch::X86;
+ }
+}
+
+static inline std::string cache_key(const std::string& name, uint32_t specific_version) {
+ return std::format("{}-{:08X}", name, specific_version);
+}
+
+template
+const T& get_with_sv_fallback(
+ const std::unordered_map& index, const std::string& name, uint32_t specific_version) {
+ try {
+ return index.at(cache_key(name, specific_version));
+ } catch (const std::out_of_range&) {
+ }
+ uint32_t arch_specific_version = specific_version_for_architecture(architecture_for_specific_version(
+ specific_version));
+ if (arch_specific_version != specific_version) {
+ try {
+ return index.at(cache_key(name, arch_specific_version));
+ } catch (const std::out_of_range&) {
+ }
+ }
+ return index.at(name);
+}
+
+template
+string ClientFunctionIndex::Function::generate_client_command_t(
+ const unordered_map& label_writes,
+ const void* suffix_data,
+ size_t suffix_size,
+ uint32_t override_relocations_offset) const {
+ using FooterT = RELFileFooterT;
+
+ FooterT footer;
+ footer.num_relocations = this->relocation_deltas.size();
+ footer.unused1.clear(0);
+ footer.root_offset = this->entrypoint_offset_offset;
+ footer.unused2.clear(0);
+
+ phosg::StringWriter w;
+ if (!label_writes.empty()) {
+ string modified_code = this->code;
+ for (const auto& it : label_writes) {
+ size_t offset = this->label_offsets.at(it.first);
+ if (offset > modified_code.size() - 4) {
+ throw runtime_error("label out of range");
+ }
+ *reinterpret_cast*>(modified_code.data() + offset) = it.second;
+ }
+ w.write(modified_code);
+ } else {
+ w.write(this->code);
+ }
+ if (suffix_size) {
+ w.write(suffix_data, suffix_size);
+ }
+ while (w.size() & 3) {
+ w.put_u8(0);
+ }
+
+ footer.relocations_offset = w.size();
+
+ // Always write at least 4 bytes even if there are no relocations
+ if (this->relocation_deltas.empty()) {
+ w.put_u32(0);
+ }
+
+ if (override_relocations_offset) {
+ footer.relocations_offset = override_relocations_offset;
+ } else {
+ for (uint16_t delta : this->relocation_deltas) {
+ w.put>(delta);
+ }
+ if (this->relocation_deltas.size() & 1) {
+ w.put_u16(0);
+ }
+ }
+
+ w.put(footer);
+ return std::move(w.str());
+}
+
+string ClientFunctionIndex::Function::generate_client_command(
+ const unordered_map& label_writes,
+ const void* suffix_data,
+ size_t suffix_size,
+ uint32_t override_relocations_offset) const {
+ if (this->is_big_endian()) {
+ return this->generate_client_command_t(label_writes, suffix_data, suffix_size, override_relocations_offset);
+ } else if ((this->arch == Architecture::X86) || (this->arch == Architecture::SH4)) {
+ return this->generate_client_command_t(label_writes, suffix_data, suffix_size, override_relocations_offset);
+ } else {
+ throw logic_error("invalid architecture");
+ }
+}
+
+static unordered_map preprocess_function_code(const std::string& text) {
+ std::unordered_set all_specific_versions;
+ struct Line {
+ std::string text;
+ std::unordered_map new_specific_versions; // Nonempty iff line is a .versions directive
+ bool enable_all_versions = false;
+ };
+
+ std::vector lines;
+ for (auto& line_text : phosg::split(text, '\n')) {
+ auto& line = lines.emplace_back();
+ line.text = std::move(line_text);
+
+ string stripped_line = line.text;
+ phosg::strip_whitespace(stripped_line);
+
+ if (stripped_line == ".all_versions") {
+ line.enable_all_versions = true;
+ } else if (stripped_line.starts_with(".versions ")) {
+ for (auto& vers_token : phosg::split(stripped_line.substr(10), ' ')) {
+ phosg::strip_whitespace(vers_token);
+ if (!vers_token.empty()) {
+ uint32_t specific_version = specific_version_for_str(vers_token);
+ size_t version_index = line.new_specific_versions.size();
+ all_specific_versions.emplace(specific_version);
+ line.new_specific_versions.emplace(std::move(specific_version), version_index);
+ }
+ }
+ }
+ }
+
+ static const std::string empty_str = "";
+
+ unordered_map ret;
+ for (uint32_t specific_version : all_specific_versions) {
+ std::deque version_lines;
+ bool include_current_line = true;
+ size_t current_vers_index = all_specific_versions.size();
+ for (size_t line_znum = 0; line_znum < lines.size(); line_znum++) {
+ const auto& line = lines[line_znum];
+
+ if (line.enable_all_versions) {
+ include_current_line = true;
+ current_vers_index = all_specific_versions.size();
+ version_lines.emplace_back(empty_str);
+
+ } else if (!line.new_specific_versions.empty()) {
+ auto it = line.new_specific_versions.find(specific_version);
+ if (it == line.new_specific_versions.end()) {
+ include_current_line = false;
+ current_vers_index = all_specific_versions.size();
+ } else {
+ include_current_line = true;
+ current_vers_index = it->second;
+ }
+ version_lines.emplace_back(empty_str);
+
+ } else if (!include_current_line) {
+ version_lines.emplace_back(empty_str);
+
+ } else {
+ std::string line_text = line.text;
+ size_t vers_offset = line_text.find("', vers_offset + 6);
+ if (end_offset == string::npos) {
+ throw runtime_error(std::format("(version {}) (line {}) unterminated replacement",
+ str_for_specific_version(specific_version), line_znum + 1));
+ }
+ auto tokens = phosg::split(line_text.substr(vers_offset + 6, end_offset - vers_offset - 6), ' ');
+ if (current_vers_index >= tokens.size()) {
+ throw runtime_error(std::format("(version {}) (line {}) invalid replacement",
+ str_for_specific_version(specific_version), line_znum + 1));
+ }
+ line_text = line_text.substr(0, vers_offset) + tokens[current_vers_index] + line_text.substr(end_offset + 1);
+ vers_offset = line_text.find(" source_files;
+ std::function add_directory = [&](const std::string& dir) -> void {
+ for (const auto& item : std::filesystem::directory_iterator(dir)) {
+ string item_name = item.path().filename().string();
+ string item_path = dir.ends_with("/") ? (dir + item_name) : (dir + "/" + item_name);
+ if (std::filesystem::is_directory(item_path)) {
+ add_directory(item_path);
+ } else if (item_path.ends_with(".s") && std::filesystem::is_regular_file(item_path)) {
+ client_functions_log.debug_f("Adding {} from {}", item_name, item_path);
+ if (!source_files.emplace(item_name, phosg::load_file(item_path)).second) {
+ throw std::runtime_error(std::format("Duplicate source filename: {}", item_name));
+ }
+ } else if (item_path.ends_with(".bin") && std::filesystem::is_regular_file(item_path)) {
+ client_functions_log.debug_f("Adding {} from {}", item_name, item_path);
+ if (!source_files.emplace(item_name, phosg::load_file(item_path)).second) {
+ throw std::runtime_error(std::format("Duplicate binary filename: {}", item_name));
+ }
+ } else {
+ client_functions_log.debug_f("Ignoring {}", item_path);
+ }
+ }
+ };
+ add_directory(root_dir);
+
+ unordered_map include_cache;
+ uint32_t last_menu_item_id = 0;
+ for (const auto& [source_filename, source] : source_files) {
+ if (!source_filename.ends_with(".s")) {
+ client_functions_log.debug_f("Skipping root compile for {} because it is not a .s file", source_filename);
+ continue;
+ }
+ if (source_filename.ends_with(".inc.s")) {
+ client_functions_log.debug_f("Skipping root compile for {} because it is an include", source_filename);
+ continue;
+ }
+
+ std::unordered_map preprocessed;
+ try {
+ preprocessed = preprocess_function_code(source);
+ } catch (const std::exception& e) {
+ throw std::runtime_error(std::format("({} preprocessing) {}", source_filename, e.what()));
+ }
+
+ for (const auto& [specific_version, source] : preprocessed) {
+ shared_ptr fn = make_shared();
+ fn->short_name = source_filename.substr(0, source_filename.size() - 2);
+ fn->specific_version = specific_version;
+ fn->menu_item_id = ++last_menu_item_id;
+ fn->arch = architecture_for_specific_version(fn->specific_version);
+
+ try {
+ unordered_set get_include_stack;
+ function get_include_for_sv = [&include_cache, &source_files, &get_include_stack, &get_include_for_sv](const string& name, uint32_t specific_version) -> string {
+ try {
+ return get_with_sv_fallback(include_cache, name, specific_version);
+ } catch (const std::out_of_range&) {
+ }
+ if (client_functions_log.should_log(phosg::LogLevel::L_DEBUG)) {
+ client_functions_log.debug_f("({}) Include {}-{} needs to be compiled",
+ get_include_stack.size(), name, str_for_specific_version(specific_version));
+ }
+
+ auto it = source_files.find(name + ".inc.s");
+ if (it != source_files.end()) {
+ if (!get_include_stack.emplace(name).second) {
+ throw runtime_error("Mutual recursion between includes: " + name);
+ }
+ for (const auto& [include_specific_version, include_source] : preprocess_function_code(it->second)) {
+ ResourceDASM::EmulatorBase::AssembleResult ret;
+ auto get_include = std::bind(get_include_for_sv, std::placeholders::_1, include_specific_version);
+ switch (architecture_for_specific_version(include_specific_version)) {
+ case Arch::POWERPC:
+ ret = ResourceDASM::PPC32Emulator::assemble(include_source, get_include);
+ break;
+ case Arch::X86:
+ ret = ResourceDASM::X86Emulator::assemble(include_source, get_include);
+ break;
+ case Arch::SH4:
+ ret = ResourceDASM::SH4Emulator::assemble(include_source, get_include);
+ break;
+ default:
+ throw runtime_error("unknown architecture");
+ }
+ if (client_functions_log.should_log(phosg::LogLevel::L_DEBUG)) {
+ client_functions_log.debug_f("({}) Compiled include {}-{}",
+ get_include_stack.size(), name, str_for_specific_version(include_specific_version));
+ }
+ include_cache.emplace(cache_key(name, include_specific_version), std::move(ret.code));
+ }
+ get_include_stack.erase(name);
+
+ } else {
+ it = source_files.find(name + ".inc.bin");
+ if (it != source_files.end()) {
+ include_cache.emplace(name, it->second).first->second;
+ client_functions_log.debug_f("({}) Cached binary include {}", get_include_stack.size(), name);
+ }
+ }
+
+ try {
+ return get_with_sv_fallback(include_cache, name, specific_version);
+ } catch (const std::out_of_range&) {
+ }
+ throw runtime_error(std::format(
+ "Data not found for include {} ({})", name, str_for_specific_version(specific_version)));
+ };
+
+ try {
+ ResourceDASM::EmulatorBase::AssembleResult assembled;
+ auto get_include = std::bind(get_include_for_sv, std::placeholders::_1, specific_version);
+ switch (fn->arch) {
+ case Arch::POWERPC:
+ assembled = ResourceDASM::PPC32Emulator::assemble(source, get_include);
+ break;
+ case Arch::X86:
+ assembled = ResourceDASM::X86Emulator::assemble(source, get_include);
+ break;
+ case Arch::SH4:
+ assembled = ResourceDASM::SH4Emulator::assemble(source, get_include);
+ break;
+ default:
+ throw runtime_error("invalid architecture");
+ }
+
+ fn->code = std::move(assembled.code);
+ fn->label_offsets = std::move(assembled.label_offsets);
+ for (const auto& [key, value] : assembled.metadata_keys) {
+ if (key == "visibility") {
+ if (value == "hidden") {
+ fn->visibility = Function::Visibility::DEBUG_ONLY;
+ } else if (value == "cheat") {
+ fn->visibility = Function::Visibility::CHAT_COMMAND_ONLY_WITH_CHEAT_MODE;
+ } else if (value == "chat") {
+ fn->visibility = Function::Visibility::CHAT_COMMAND_ONLY;
+ } else if (value == "menu") {
+ fn->visibility = Function::Visibility::PATCHES_MENU_ONLY;
+ } else if (value == "all") {
+ fn->visibility = Function::Visibility::PATCHES_MENU_AND_CHAT_COMMAND;
+ } else {
+ throw std::runtime_error("Invalid visibility value");
+ }
+ } else if (key == "key") {
+ fn->short_name = value;
+ } else if (key == "name") {
+ fn->long_name = value;
+ } else if (key == "description") {
+ fn->description = value;
+ } else if (key == "client_flag") {
+ fn->client_flag = stoull(value, nullptr, 0);
+ } else if (key == "show_return_value") {
+ fn->show_return_value = true;
+ } else {
+ throw runtime_error("unknown metadata key: " + key);
+ }
+ }
+
+ try {
+ fn->entrypoint_offset_offset = fn->label_offsets.at("entry_ptr");
+ } catch (const out_of_range&) {
+ throw runtime_error("code does not contain entry_ptr label");
+ }
+
+ set reloc_indexes;
+ for (const auto& it : fn->label_offsets) {
+ if (it.first.starts_with("reloc")) {
+ reloc_indexes.emplace(it.second / 4);
+ }
+ }
+ uint32_t prev_index = 0;
+ for (const auto& it : reloc_indexes) {
+ uint32_t delta = it - prev_index;
+ if (delta > 0xFFFF) {
+ throw runtime_error("relocation delta too far away");
+ }
+ fn->relocation_deltas.emplace_back(delta);
+ prev_index = it;
+ }
+
+ } catch (const exception& e) {
+ if (raise_on_any_failure) {
+ throw;
+ }
+ client_functions_log.warning_f("Failed to compile function {} ({}): {}",
+ fn->short_name, str_for_specific_version(specific_version), e.what());
+ }
+
+ auto key = cache_key(fn->short_name, specific_version);
+ if (!this->all_functions.emplace(key, fn).second) {
+ throw std::runtime_error("Duplicate function key: " + key);
+ }
+ this->functions_by_specific_version[specific_version].emplace(key, fn);
+ this->functions_by_menu_item_id.emplace(fn->menu_item_id, fn);
+
+ client_functions_log.debug_f("Compiled function {} ({}; {}; {})",
+ fn->short_name, str_for_specific_version(fn->specific_version), name_for_architecture(fn->arch),
+ phosg::name_for_enum(fn->visibility));
+ } catch (const std::exception& e) {
+ throw std::runtime_error(std::format(
+ "({}-{}) {}", fn->short_name, str_for_specific_version(specific_version), e.what()));
+ }
+ }
+ }
+}
+
+shared_ptr ClientFunctionIndex::patch_switches_menu(
+ uint32_t specific_version,
+ const std::unordered_set& server_auto_patches_enabled,
+ const std::unordered_set& client_auto_patches_enabled) const {
+ auto ret = make_shared