From f0366a3550a76c83f5b3d411014e064267523df3 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sat, 10 Aug 2024 00:29:24 -0700 Subject: [PATCH] add BB stack limits patch --- src/ReceiveCommands.cc | 19 +++- src/ServerState.cc | 8 ++ src/ServerState.hh | 1 + .../StackLimits/StackLimits.59NL.patch.s | 87 +++++++++++++++++++ system/config.example.json | 17 +++- tests/config.json | 1 + 6 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 system/client-functions/StackLimits/StackLimits.59NL.patch.s diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index ba538452..0018f0c1 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -123,8 +123,10 @@ static shared_ptr proxy_options_menu_for_client(shared_ptr c, std::function on_complete) { // TODO: This function is bad. Ideally we would use coroutines and clean up // all these terrible callbacks. + auto s = c->require_server_state(); - if (c->login->account->auto_patches_enabled.empty()) { + if (c->login->account->auto_patches_enabled.empty() && + ((c->version() != Version::BB_V4) || s->bb_required_patches.empty())) { c->config.set_flag(Client::Flag::HAS_AUTO_PATCHES); } @@ -138,7 +140,22 @@ void send_first_pre_lobby_commands(shared_ptr c, std::function o } auto s = c->require_server_state(); + size_t num_patches_sent = 0; + if (c->version() == Version::BB_V4) { + for (const auto& patch_name : s->bb_required_patches) { + try { + send_function_call(c, s->function_code_index->get_patch(patch_name, c->config.specific_version)); + num_patches_sent++; + } catch (const out_of_range&) { + string message = phosg::string_printf( + "Your client is not compatible with a\nrequired patch on this server.\n\nClient version: %08" PRIX32 "\nPatch name: %s", c->config.specific_version, patch_name.c_str()); + send_message_box(c, message); + c->should_disconnect = true; + return; + } + } + } for (const auto& patch_name : c->login->account->auto_patches_enabled) { try { send_function_call(c, s->function_code_index->get_patch(patch_name, c->config.specific_version)); diff --git a/src/ServerState.cc b/src/ServerState.cc index d84b3ad0..773de63d 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -1170,6 +1170,14 @@ void ServerState::load_config_early() { } catch (const out_of_range&) { } + this->bb_required_patches.clear(); + try { + for (const auto& it : this->config_json->get_list("BBRequiredPatches")) { + this->bb_required_patches.emplace_back(it->as_string()); + } + } catch (const out_of_range&) { + } + this->update_dependent_server_configs(); } diff --git a/src/ServerState.hh b/src/ServerState.hh index 12e23c28..be79d2c2 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -187,6 +187,7 @@ struct ServerState : public std::enable_shared_from_this { std::array, 4> rare_enemy_rates_by_difficulty; std::shared_ptr rare_enemy_rates_challenge; std::array, 3> min_levels_v4; // Indexed as [episode][difficulty] + std::vector bb_required_patches; struct QuestF960Result { uint32_t meseta_cost = 0; diff --git a/system/client-functions/StackLimits/StackLimits.59NL.patch.s b/system/client-functions/StackLimits/StackLimits.59NL.patch.s new file mode 100644 index 00000000..64bc6e9a --- /dev/null +++ b/system/client-functions/StackLimits/StackLimits.59NL.patch.s @@ -0,0 +1,87 @@ +# It would be a bad idea to remove `.meta hide_from_patches_menu` to make this +# patch an option for players to be able to select; either all players on the +# server should have this patch, or none should have it. + +# If you change the stack limits in config.json away from the defaults, you +# should change the limits array below to match config.json and add this patch +# to the BBRequiredPatches list. + +.meta name="Item stacks" +.meta description="" +.meta hide_from_patches_menu + +entry_ptr: +reloc0: + .offsetof start +start: + .include WriteCodeBlocksBB + + # Patch 1: rewrite item_is_stackable + .data 0x005C502C + .deltaof item_is_stackable_start, item_is_stackable_end + +item_is_stackable_start: + mov eax, [esp + 4] + cmp al, 4 + je return_1 + + cmp al, 3 + jne return_0 + + mov ah, [esp + 8] + push eax + mov ecx, esp + + .binary E8EC130100 # call max_stack_size_for_tool_start + pop ecx + cmp eax, 1 + jg return_1 + # Fallthrough to return_0 + +return_0: + xor eax, eax + ret +return_1: + xor eax, eax + inc eax + ret +item_is_stackable_end: + + # Patch 2: rewrite max_stack_size_for_tool + .data 0x005D6430 + .deltaof max_stack_size_for_tool_start, max_stack_size_for_tool_end + +max_stack_size_for_tool_start: + xor eax, eax + inc eax + + # if (data1[0] != 3) return 1 + cmp byte [ecx], 3 + jne not_tool2 + + # declare return values array + call data_end + # This array specifies the stack limits for each tool class. The array index + # is the second byte of the item data (see names-v4.json for the values; for + # e.g. tech disks this would be 02). For classes beyond 15, the value for 15 + # is used. + # Index: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 + .binary 0A 0A 01 0A 0A 0A 0A 0A 0A 01 63 63 01 01 01 01 63 01 01 01 01 01 +data_end: + + # eax = min(data1[1], 0x11) + mov al, [ecx + 1] + xor edx, edx + mov dl, 0x11 + cmp eax, edx + cmovg eax, edx + + # return data[eax] + pop edx + mov al, [edx + eax] +not_tool2: + ret +max_stack_size_for_tool_end: + + .data 0x00000000 + .data 0x00000000 diff --git a/system/config.example.json b/system/config.example.json index d5cd2fb6..e012ff2c 100644 --- a/system/config.example.json +++ b/system/config.example.json @@ -133,7 +133,6 @@ // (below). "console-login": [5100, "gc", "login_server"], "pc-login": [5101, "pc", "login_server"], - "xb-login": [5102, "xb", "login_server"], "xb-lobby": [5105, "xb", "login_server"], "console-lobby": [5110, "gc", "lobby_server"], "pc-lobby": [5111, "pc", "lobby_server"], @@ -705,9 +704,13 @@ // Item stack limits. Note that changing these does not affect the client's // behavior automatically - this only exists to allow the server to understand // the behavior of clients that are already patched with different stack - // limits. The ToolLimits list is indexed by data1[1] (that is, the second - // byte of the item data); for items beyond the end of the list, the last - // entry's value is used. + // limits. If you want to use an unpatched BB client but still have custom + // stack limits, you can use the StackLimits runtime patch by editing + // system/client-functions/StackLimits/StackLimits.59NL.patch.s to match the + // BB stack limits and adding "StackLimits" to the BBRequiredPatches list. + // The ToolLimits list is indexed by data1[1] (that is, the second byte of the + // item data); for items beyond the end of the list, the last entry's value is + // used. "ItemStackLimits": [ {"MesetaLimit": 999999, "ToolLimits": [10]}, // DC NTE {"MesetaLimit": 999999, "ToolLimits": [10]}, // DC 11/2000 @@ -849,6 +852,12 @@ // action instead. "ServerGlobalDropRateMultiplier": 1.0, + // Client functions listed here are always enabled as auto patches for BB + // clients. For example, you can set this to ["StackLimits"] if you've edited + // the StackLimits patch and the ItemStackLimits field in this file, and want + // the limits to take effect on BB clients. + "BBRequiredPatches": [], + // Whether to retain server drop tables when game leaders change. The client // reloads its drop tables when the leader joins a game or returns to Pioneer // 2; this leads to some edge cases that could be confusing for players, so diff --git a/tests/config.json b/tests/config.json index 5c9d0749..b5982a24 100644 --- a/tests/config.json +++ b/tests/config.json @@ -317,6 +317,7 @@ "BBGlobalEXPMultiplier": 1, "BBEXPShareMultiplier": 0.5, "ServerGlobalDropRateMultiplier": 1.0, + "BBRequiredPatches": [], "UseGameCreatorSectionID": false, "TeamRewards": [