From 3844c9881cd99c512c751ce9c79db36fc9c61e1e Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Thu, 12 Jun 2025 18:15:47 -0700 Subject: [PATCH] add AccurateKillCount patch --- src/FunctionCompiler.cc | 8 +- src/FunctionCompiler.hh | 5 +- src/ItemNameIndex.cc | 21 +++-- src/ReceiveCommands.cc | 21 +++-- src/SendCommands.cc | 3 +- src/SendCommands.hh | 2 +- src/ServerState.cc | 9 +- src/ServerState.hh | 3 +- .../AccurateKillCount.3___.patch.s | 42 +++++++++ .../AccurateKillCount.4___.patch.s | 36 ++++++++ .../AccurateKillCount.59NL.patch.s | 86 +++++++++++++++++++ system/config.example.json | 11 ++- 12 files changed, 227 insertions(+), 20 deletions(-) create mode 100644 system/client-functions/AccurateKillCount/AccurateKillCount.3___.patch.s create mode 100644 system/client-functions/AccurateKillCount/AccurateKillCount.4___.patch.s create mode 100644 system/client-functions/AccurateKillCount/AccurateKillCount.59NL.patch.s diff --git a/src/FunctionCompiler.cc b/src/FunctionCompiler.cc index ece1b766..9dbd48ec 100644 --- a/src/FunctionCompiler.cc +++ b/src/FunctionCompiler.cc @@ -405,18 +405,20 @@ FunctionCodeIndex::FunctionCodeIndex(const string& directory) { } shared_ptr FunctionCodeIndex::patch_switches_menu( - uint32_t specific_version, const std::unordered_set& auto_patches_enabled) const { + uint32_t specific_version, + const std::unordered_set& server_auto_patches_enabled, + const std::unordered_set& client_auto_patches_enabled) const { auto suffix = std::format("-{:08X}", specific_version); auto ret = make_shared(MenuID::PATCH_SWITCHES, "Patches"); ret->items.emplace_back(PatchesMenuItemID::GO_BACK, "Go back", "Return to the\nmain menu", 0); for (const auto& it : this->name_and_specific_version_to_patch_function) { const auto& fn = it.second; - if (fn->hide_from_patches_menu || !it.first.ends_with(suffix)) { + if (fn->hide_from_patches_menu || !it.first.ends_with(suffix) || server_auto_patches_enabled.count(fn->short_name)) { continue; } string name; - name.push_back(auto_patches_enabled.count(fn->short_name) ? '*' : '-'); + name.push_back(client_auto_patches_enabled.count(fn->short_name) ? '*' : '-'); name += fn->long_name.empty() ? fn->short_name : fn->long_name; ret->items.emplace_back(fn->menu_item_id, name, fn->description, MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL_RUNS_CODE); } diff --git a/src/FunctionCompiler.hh b/src/FunctionCompiler.hh index c4277c7f..dae44a84 100644 --- a/src/FunctionCompiler.hh +++ b/src/FunctionCompiler.hh @@ -61,7 +61,10 @@ struct FunctionCodeIndex { // Key here is e.g. "PATCHNAME-SPECIFICVERSION", with the latter in hex std::map> name_and_specific_version_to_patch_function; - std::shared_ptr patch_switches_menu(uint32_t specific_version, const std::unordered_set& auto_patches_enabled) const; + std::shared_ptr patch_switches_menu( + uint32_t specific_version, + const std::unordered_set& server_auto_patches_enabled, + const std::unordered_set& client_auto_patches_enabled) const; bool patch_menu_empty(uint32_t specific_version) const; std::shared_ptr get_patch(const std::string& name, uint32_t specific_version) const; diff --git a/src/ItemNameIndex.cc b/src/ItemNameIndex.cc index 8684e538..7cbf3e35 100644 --- a/src/ItemNameIndex.cc +++ b/src/ItemNameIndex.cc @@ -216,17 +216,21 @@ std::string ItemNameIndex::describe_item(const ItemData& item, uint8_t flags) co } } else if (!name_only) { // Not S-rank (extended name bits not set) + size_t num_bonuses = 3; + + if (item.data1[10] & 0x80) { + ret_tokens.emplace_back(std::format("K:{}", item.get_kill_count())); + num_bonuses = 2; + } + parray bonuses(0); - for (size_t x = 0; x < 3; x++) { + for (size_t x = 0; x < num_bonuses; x++) { uint8_t which = item.data1[6 + 2 * x]; uint8_t value = item.data1[7 + 2 * x]; if (which == 0) { continue; } - if (which & 0x80) { - uint16_t kill_count = ((which << 8) & 0x7F00) | (value & 0xFF); - ret_tokens.emplace_back(std::format("K:{}", kill_count)); - } else if (which > 5) { + if (which > 5) { ret_tokens.emplace_back(std::format("!PC:{:02X}{:02X}", which, value)); } else { bonuses[which - 1] = value; @@ -263,6 +267,10 @@ std::string ItemNameIndex::describe_item(const ItemData& item, uint8_t flags) co ret_tokens.emplace_back(std::format("!MD:{:04X}", modifier)); } + if (item.data1[10] & 0x80) { + ret_tokens.emplace_back(std::format("K:{}", item.get_kill_count())); + } + } else if (!name_only) { // Armor/shields if (item.data1[5] > 0) { if (item.data1[5] == 1) { @@ -527,6 +535,9 @@ ItemData ItemNameIndex::parse_item_description_phase(const std::string& descript ret.data1w[4] = phosg::bswap16(0x8000 | (char_indexes[4] & 0x1F) | ((char_indexes[3] & 0x1F) << 5) | ((char_indexes[2] & 0x1F) << 10)); ret.data1w[5] = phosg::bswap16(0x8000 | (char_indexes[7] & 0x1F) | ((char_indexes[6] & 0x1F) << 5) | ((char_indexes[5] & 0x1F) << 10)); + } else if (token.starts_with("k:")) { + ret.set_kill_count(stoul(token.substr(2), nullptr, 0)); + } else { auto p_tokens = phosg::split(token, '/'); if (p_tokens.size() > 5) { diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 652551ee..35cb413b 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -232,7 +232,8 @@ static asio::awaitable send_auto_patches_if_needed(shared_ptr c) { auto s = c->require_server_state(); if (c->login->account->auto_patches_enabled.empty() && - ((c->version() != Version::BB_V4) || s->bb_required_patches.empty())) { + ((c->version() != Version::BB_V4) || s->bb_required_patches.empty()) && + s->auto_patches.empty()) { c->set_flag(Client::Flag::HAS_AUTO_PATCHES); } @@ -240,11 +241,11 @@ static asio::awaitable send_auto_patches_if_needed(shared_ptr c) { c->set_flag(Client::Flag::HAS_AUTO_PATCHES); co_await prepare_client_for_patches(c); - vector> functions_to_send; + unordered_set> functions_to_send; if (c->version() == Version::BB_V4) { for (const auto& patch_name : s->bb_required_patches) { try { - functions_to_send.emplace_back(s->function_code_index->get_patch(patch_name, c->specific_version)); + functions_to_send.emplace(s->function_code_index->get_patch(patch_name, c->specific_version)); } catch (const out_of_range&) { string message = std::format( "Your client is not compatible with a\nrequired patch on this server.\n\nClient version: {:08X}\nPatch name: {}", c->specific_version, patch_name); @@ -254,9 +255,17 @@ static asio::awaitable send_auto_patches_if_needed(shared_ptr c) { } } } + for (const auto& patch_name : s->auto_patches) { + try { + functions_to_send.emplace(s->function_code_index->get_patch(patch_name, c->specific_version)); + } catch (const out_of_range&) { + c->log.warning_f("Server has auto patch {} enabled, but it is not available for specific_version {:08X}", + patch_name, c->specific_version); + } + } for (const auto& patch_name : c->login->account->auto_patches_enabled) { try { - functions_to_send.emplace_back(s->function_code_index->get_patch(patch_name, c->specific_version)); + functions_to_send.emplace(s->function_code_index->get_patch(patch_name, c->specific_version)); } catch (const out_of_range&) { c->log.warning_f("Client has auto patch {} enabled, but it is not available for specific_version {:08X}", patch_name, c->specific_version); @@ -2528,7 +2537,7 @@ static asio::awaitable on_10_main_menu(shared_ptr c, uint32_t item // 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()->function_code_index->patch_switches_menu(c->specific_version, c->login->account->auto_patches_enabled)); + send_menu(c, c->require_server_state()->function_code_index->patch_switches_menu(c->specific_version, s->auto_patches, c->login->account->auto_patches_enabled)); break; } @@ -2863,7 +2872,7 @@ static asio::awaitable on_10_patch_switches(shared_ptr c, uint32_t c->login->account->auto_patches_enabled.erase(fn->short_name); } c->login->account->save(); - send_menu(c, s->function_code_index->patch_switches_menu(c->specific_version, c->login->account->auto_patches_enabled)); + send_menu(c, s->function_code_index->patch_switches_menu(c->specific_version, s->auto_patches, c->login->account->auto_patches_enabled)); } co_return; } diff --git a/src/SendCommands.cc b/src/SendCommands.cc index a62ea6e2..2c628374 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -453,7 +453,8 @@ asio::awaitable send_function_call( co_return co_await promise->get(); } -asio::awaitable send_function_call_multi(shared_ptr c, vector> codes) { +asio::awaitable send_function_call_multi( + shared_ptr c, unordered_set> codes) { if (codes.empty()) { co_return; } diff --git a/src/SendCommands.hh b/src/SendCommands.hh index 588e58e5..0d29ea13 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -186,7 +186,7 @@ asio::awaitable send_function_call( bool ignore_actually_runs_code_flag = false); asio::awaitable send_function_call_multi( std::shared_ptr c, - std::vector> codes); + std::unordered_set> codes); asio::awaitable send_protected_command(std::shared_ptr c, const void* data, size_t size, bool echo_to_lobby); asio::awaitable send_dol_file(std::shared_ptr c, std::shared_ptr dol); diff --git a/src/ServerState.cc b/src/ServerState.cc index a4f5fb35..b95b69b0 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -1302,7 +1302,14 @@ void ServerState::load_config_early() { 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()); + this->bb_required_patches.emplace(it->as_string()); + } + } catch (const 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 out_of_range&) { } diff --git a/src/ServerState.hh b/src/ServerState.hh index ddcc4b63..03e87d63 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -219,7 +219,8 @@ 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; + std::unordered_set bb_required_patches; + std::unordered_set auto_patches; CheatFlags cheat_flags; struct QuestF960Result { diff --git a/system/client-functions/AccurateKillCount/AccurateKillCount.3___.patch.s b/system/client-functions/AccurateKillCount/AccurateKillCount.3___.patch.s new file mode 100644 index 00000000..56a8d33e --- /dev/null +++ b/system/client-functions/AccurateKillCount/AccurateKillCount.3___.patch.s @@ -0,0 +1,42 @@ +.meta name="Kill count fix" +.meta description="Fixes client-side\nkill counts when\nmultiple enemies are\nkilled on the same\nframe" +.meta hide_from_patches_menu + +.versions 3OJ2 3OJ3 3OJ4 3OJ5 3OE0 3OE1 3OE2 3OP0 + +entry_ptr: +reloc0: + .offsetof start +start: + .include WriteCodeBlocksGC + + .data + .deltaof TItemWeapon_SealedJSword_count_kill, TItemWeapon_SealedJSword_count_kill_end + .address +TItemWeapon_SealedJSword_count_kill: # [std] (TItemWeapon_SealedJSword* this @ r3) -> void + lwz r4, [r3 + 0xF0] # r4 = this->owner_player + lha r5, [r4 + 0x11A] # r5 = this->owner_player->num_kills_since_map_load + lha r6, [r3 + 0x1F8] # r6 = this->last_owner_player_kill_count + lhz r7, [r3 + 0xE8] # r7 = this->kill_count + cmp r6, r5 + bge TItemWeapon_SealedJSword_count_kill_skip_update + lwz r8, [r3 + 0xDC] + andi. r8, r8, 0x100 + beq TItemWeapon_SealedJSword_count_kill_skip_incr # if (!(flags & 0x100)) don't incr kill count + sub r8, r5, r6 + add r7, r7, r8 + sth [r3 + 0xE8], r7 +TItemWeapon_SealedJSword_count_kill_skip_incr: + sth [r3 + 0x1F8], r5 +TItemWeapon_SealedJSword_count_kill_skip_update: + cmplwi r7, 23000 + blt TItemWeapon_SealedJSword_count_kill_skip_set_flag + lwz r8, [r3 + 0xDC] + ori r8, r8, 0x200 + stw [r3 + 0xDC], r8 +TItemWeapon_SealedJSword_count_kill_skip_set_flag: + blr +TItemWeapon_SealedJSword_count_kill_end: + + .data 0x00000000 + .data 0x00000000 diff --git a/system/client-functions/AccurateKillCount/AccurateKillCount.4___.patch.s b/system/client-functions/AccurateKillCount/AccurateKillCount.4___.patch.s new file mode 100644 index 00000000..c4d71fac --- /dev/null +++ b/system/client-functions/AccurateKillCount/AccurateKillCount.4___.patch.s @@ -0,0 +1,36 @@ +.meta name="Kill count fix" +.meta description="Fixes client-side\nkill counts when\nmultiple enemies are\nkilled on the same\nframe" +.meta hide_from_patches_menu + +.versions 4OJB 4OJD 4OJU 4OED 4OEU 4OPD 4OPU + +entry_ptr: +reloc0: + .offsetof start +start: + .include WriteCodeBlocksXB + + .data + .deltaof TItemWeapon_SealedJSword_count_kill, TItemWeapon_SealedJSword_count_kill_end + .address +TItemWeapon_SealedJSword_count_kill: + mov eax, [ecx + 0xF0] + movsx eax, word [eax + 0x11A] + movsx edx, word [ecx + 0x1F8] + sub edx, eax + jge TItemWeapon_SealedJSword_count_kill_skip_update + test dword [ecx + 0xDC], 0x100 + jz TItemWeapon_SealedJSword_count_kill_skip_incr + sub [ecx + 0xE8], dx +TItemWeapon_SealedJSword_count_kill_skip_incr: + mov [ecx + 0x1F8], ax +TItemWeapon_SealedJSword_count_kill_skip_update: + cmp word [ecx + 0xE8], 23000 + jb TItemWeapon_SealedJSword_count_kill_skip_set_flag + or dword [ecx + 0xDC], 0x200 +TItemWeapon_SealedJSword_count_kill_skip_set_flag: + ret +TItemWeapon_SealedJSword_count_kill_end: + + .data 0x00000000 + .data 0x00000000 diff --git a/system/client-functions/AccurateKillCount/AccurateKillCount.59NL.patch.s b/system/client-functions/AccurateKillCount/AccurateKillCount.59NL.patch.s new file mode 100644 index 00000000..0e4675cd --- /dev/null +++ b/system/client-functions/AccurateKillCount/AccurateKillCount.59NL.patch.s @@ -0,0 +1,86 @@ +.meta name="Kill count fix" +.meta description="Fixes client-side\nkill counts when\nmultiple enemies are\nkilled on the same\nframe" +.meta hide_from_patches_menu + +entry_ptr: +reloc0: + .offsetof start +start: + .include WriteCodeBlocksBB + + + + .data 0x005E32C8 + .deltaof TItemUnitUnsealable_count_kill, TItemUnitUnsealable_count_kill_end + .address 0x005E32C8 +TItemUnitUnsealable_count_kill: # [std] (TItemUnitUnsealable* this @ ecx) -> void + mov eax, [ecx + 0xF8] + movsx eax, word [eax + 0x11A] # eax = this->owner_player->num_kills_since_map_load + movsx edx, word [ecx + 0x1E4] # edx = this->last_owner_player_kill_count + sub edx, eax # edx = this->last_owner_player_kill_count - this->owner_player->num_kills_since_map_load (edx should be 0 or negative) + jge TItemUnitUnsealable_count_kill_skip_update + test dword [ecx + 0xDC], 0x100 + jz TItemUnitUnsealable_count_kill_skip_incr # if (!(this->flags & 0x100)) don't incr kill count + sub [ecx + 0xE8], dx # this->kill_count -= edx +TItemUnitUnsealable_count_kill_skip_incr: + mov [ecx + 0x1E4], ax # this->last_owner_player_kill_count = this->owner_player->num_kills_since_map_load +TItemUnitUnsealable_count_kill_skip_update: + cmp word [ecx + 0xE8], 20000 + jb TItemUnitUnsealable_count_kill_skip_set_flag + or dword [ecx + 0xDC], 0x200 +TItemUnitUnsealable_count_kill_skip_set_flag: + jmp 0x005E2C34 +TItemUnitUnsealable_count_kill_end: + + + + .data 0x005F3EFC + .deltaof TItemWeapon_LameDArgent_count_kill, TItemWeapon_LameDArgent_count_kill_end + .address 0x005F3EFC +TItemWeapon_LameDArgent_count_kill: + mov eax, [ecx + 0xF8] + movsx eax, word [eax + 0x11A] + movsx edx, word [ecx + 0x240] + sub edx, eax + jge TItemWeapon_LameDArgent_count_kill_skip_update + test dword [ecx + 0xDC], 0x100 + jz TItemWeapon_LameDArgent_count_kill_skip_incr + sub [ecx + 0xE8], dx +TItemWeapon_LameDArgent_count_kill_skip_incr: + mov [ecx + 0x240], ax +TItemWeapon_LameDArgent_count_kill_skip_update: + cmp word [ecx + 0xE8], 10000 + jb TItemWeapon_LameDArgent_count_kill_skip_set_flag + or dword [ecx + 0xDC], 0x200 +TItemWeapon_LameDArgent_count_kill_skip_set_flag: + ret +TItemWeapon_LameDArgent_count_kill_end: + + + + .data 0x005FCA74 + .deltaof TItemWeapon_SealedJSword_count_kill, TItemWeapon_SealedJSword_count_kill_end + .address 0x005FCA74 +TItemWeapon_SealedJSword_count_kill: + mov eax, [ecx + 0xF8] + movsx eax, word [eax + 0x11A] + movsx edx, word [ecx + 0x240] + sub edx, eax + jge TItemWeapon_SealedJSword_count_kill_skip_update + test dword [ecx + 0xDC], 0x100 + jz TItemWeapon_SealedJSword_count_kill_skip_incr + sub [ecx + 0xE8], dx +TItemWeapon_SealedJSword_count_kill_skip_incr: + mov [ecx + 0x240], ax +TItemWeapon_SealedJSword_count_kill_skip_update: + cmp word [ecx + 0xE8], 23000 + jb TItemWeapon_SealedJSword_count_kill_skip_set_flag + or dword [ecx + 0xDC], 0x200 +TItemWeapon_SealedJSword_count_kill_skip_set_flag: + ret +TItemWeapon_SealedJSword_count_kill_end: + + + + .data 0x00000000 + .data 0x00000000 diff --git a/system/config.example.json b/system/config.example.json index dcc2e4f9..fc6eaaa9 100644 --- a/system/config.example.json +++ b/system/config.example.json @@ -826,12 +826,21 @@ // Client functions listed here are always enabled as auto patches for BB // clients. For example, you can add "StackLimits" here if you've edited the // StackLimits patch and the ItemStackLimits field in this file, and want the - // limits to take effect on BB clients. + // limits to take effect on BB clients. If a client connects using a client + // version that isn't compatible with one of these patches, the client will + // be disconnected. "BBRequiredPatches": [ // You will probably want to uncomment the following line if you want to // use items that were unreleased on the original Sega servers. // "ClearUnreleasedItemList", ], + // Client functions listed here are automatically sent to all clients. If a + // client connects using a client version that isn't compatible with some of + // these patches, the incompatible patches are skipped and the client is + // allowed to connect. This option applies to all PSO versions, not only BB. + "AutoPatches": [ + // "AccurateKillCount", + ], // 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