From e44dad83447609ace3dcb4ca939996150440e2ba Mon Sep 17 00:00:00 2001 From: James Osborne Date: Sat, 6 Jun 2026 21:07:52 -0400 Subject: [PATCH 1/9] Add Brutal Peeps HP client patch --- src/ReceiveCommands.cc | 2 + src/SendCommands.cc | 118 ++++++++++++++++++ src/SendCommands.hh | 1 + .../PsoPeepsBrutalPeepsHPBB.s | 99 +++++++++++++++ 4 files changed, 220 insertions(+) create mode 100644 system/client-functions/PsoPeepsBrutalPeepsHPBB.s diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index ce68c5a0..8f730650 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -2762,6 +2762,7 @@ static asio::awaitable on_10_main_menu(std::shared_ptr c, uint32_t c->log.info_f("Brutal Peeps +{} selected from BB menu at level {}", tier, character_level); co_await send_auto_patches_if_needed(c); + co_await send_brutal_peeps_hp_patch_bb(c, tier); co_await enable_save_if_needed(c); send_lobby_list(c); if (!c->lobby.lock()) { @@ -2774,6 +2775,7 @@ static asio::awaitable on_10_main_menu(std::shared_ptr c, uint32_t case MainMenuItemID::GO_TO_LOBBY: { c->selected_brutal_peeps_tier = -1; co_await send_auto_patches_if_needed(c); + co_await send_brutal_peeps_hp_patch_bb(c, -1); co_await enable_save_if_needed(c); send_lobby_list(c); if (is_pre_v1(c->version())) { diff --git a/src/SendCommands.cc b/src/SendCommands.cc index a45443ee..c8780f82 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -775,6 +775,124 @@ static std::string bb_stream_file_data_for_client(std::shared_ptr c) { } +asio::awaitable send_brutal_peeps_hp_patch_bb(std::shared_ptr c, int64_t tier) { + if (c->version() != Version::BB_V4) { + co_return; + } + if (!c->check_flag(Client::Flag::HAS_SEND_FUNCTION_CALL) || + !c->check_flag(Client::Flag::SEND_FUNCTION_CALL_ACTUALLY_RUNS_CODE)) { + c->log.warning_f("Skipping Brutal Peeps HP client patch because client does not support executable send_function_call"); + co_return; + } + + auto s = c->require_server_state(); + const auto* brutal_peeps_def = brutal_peeps_tier_definition(tier); + if ((tier >= 0) && !brutal_peeps_def) { + c->log.warning_f("Skipping Brutal Peeps HP client patch for invalid tier {}", tier); + co_return; + } + + const double mult = brutal_peeps_def ? brutal_peeps_def->enemy_hp_multiplier : 1.0; + const BBStreamFile::Entry* bp_entry = nullptr; + + for (const auto& sf_entry : s->bb_stream_file->entries) { + if (sf_entry.filename == "BattleParamEntry_on.dat") { + bp_entry = &sf_entry; + break; + } + } + if (!bp_entry) { + c->log.warning_f("Skipping Brutal Peeps HP client patch: BattleParamEntry_on.dat not found in BB stream file"); + co_return; + } + if ((bp_entry->offset > s->bb_stream_file->data.size()) || + (bp_entry->size > (s->bb_stream_file->data.size() - bp_entry->offset)) || + (bp_entry->size < sizeof(BattleParamsIndex::Table))) { + c->log.warning_f("Skipping Brutal Peeps HP client patch: invalid BattleParamEntry_on.dat range"); + co_return; + } + + const char* vanilla_data = s->bb_stream_file->data.data() + bp_entry->offset; + std::string target_data(vanilla_data, bp_entry->size); + + auto* table = reinterpret_cast(target_data.data()); + size_t ultimate_index = static_cast(Difficulty::ULTIMATE); + + auto scale_u16 = [mult](uint32_t v) -> uint16_t { + if (v == 0) { + return 0; + } + uint32_t scaled = static_cast((static_cast(v) * mult) + 0.5); + if (scaled < 1) { + scaled = 1; + } + if (scaled > 0xFFFF) { + scaled = 0xFFFF; + } + return static_cast(scaled); + }; + + for (size_t z = 0; z < 0x60; z++) { + auto& stats = table->stats[ultimate_index][z]; + stats.char_stats.hp = scale_u16(stats.char_stats.hp); + } + + constexpr uint32_t scan_start = 0x16760000; + constexpr uint32_t scan_end = 0x16A90000; + constexpr uint32_t signature_size = 64; + constexpr uint32_t hp_patch_bytes = 0x60 * 2; + + if (bp_entry->size < signature_size) { + c->log.warning_f("Skipping Brutal Peeps HP client patch: BattleParamEntry_on.dat too small for signature"); + co_return; + } + + std::string suffix; + suffix.append(vanilla_data, signature_size); + + auto append_u32l = +[](std::string& out, uint32_t v) { + out.push_back(static_cast(v & 0xFF)); + out.push_back(static_cast((v >> 8) & 0xFF)); + out.push_back(static_cast((v >> 16) & 0xFF)); + out.push_back(static_cast((v >> 24) & 0xFF)); + }; + + for (size_t z = 0; z < 0x60; z++) { + const auto& hp = table->stats[ultimate_index][z].char_stats.hp; + uint32_t hp_offset = reinterpret_cast(&hp) - target_data.data(); + const uint8_t* hp_bytes = reinterpret_cast(&hp); + + append_u32l(suffix, hp_offset); + suffix.push_back(static_cast(hp_bytes[0])); + + append_u32l(suffix, hp_offset + 1); + suffix.push_back(static_cast(hp_bytes[1])); + } + + try { + co_await prepare_client_for_patches(c); + auto fn = s->client_functions->get("PsoPeepsBrutalPeepsHP", c->specific_version); + co_await send_function_call( + c, + fn, + { + {"scan_start", scan_start}, + {"scan_end", scan_end}, + {"signature_size", signature_size}, + {"patch_count", hp_patch_bytes}, + }, + suffix.data(), + suffix.size()); + + c->log.info_f("Brutal Peeps HP client patch sent: tier={} mult={:g} patch_bytes={} scan={:08X}-{:08X}", + tier, mult, hp_patch_bytes, scan_start, scan_end); + + } catch (const std::exception& e) { + c->log.warning_f("Failed to send Brutal Peeps HP client patch: {}", e.what()); + } +} + + void send_stream_file_index_bb(std::shared_ptr c) { auto s = c->require_server_state(); diff --git a/src/SendCommands.hh b/src/SendCommands.hh index c8795063..b09c48cc 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -198,6 +198,7 @@ void send_guild_card_header_bb(std::shared_ptr c); void send_guild_card_chunk_bb(std::shared_ptr c, size_t chunk_index); void send_stream_file_index_bb(std::shared_ptr c); void send_stream_file_chunk_bb(std::shared_ptr c, uint32_t chunk_index); +asio::awaitable send_brutal_peeps_hp_patch_bb(std::shared_ptr c, int64_t tier); void send_approve_player_choice_bb(std::shared_ptr c); void send_complete_player_bb(std::shared_ptr c); diff --git a/system/client-functions/PsoPeepsBrutalPeepsHPBB.s b/system/client-functions/PsoPeepsBrutalPeepsHPBB.s new file mode 100644 index 00000000..efc462c1 --- /dev/null +++ b/system/client-functions/PsoPeepsBrutalPeepsHPBB.s @@ -0,0 +1,99 @@ +.meta key="PsoPeepsBrutalPeepsHP" +.meta name="Brutal Peeps HP" +.meta description="Applies Brutal Peeps\nenemy HP scaling" +.meta show_return_value + +.versions 50YJ 59NJ 59NL + +entry_ptr: +reloc0: + .offsetof start + +start: + push ebx + push esi + push edi + push ebp + + jmp get_data_ptr + +get_data_ptr_ret: + pop ebx + + mov esi, [ebx + scan_start - data] # candidate ptr + mov edx, [ebx + scan_end - data] # scan end + mov ecx, [ebx + signature_size - data] # signature size + sub edx, ecx # scan limit = end - sig_size + lea edi, [ebx + payload - data] # signature ptr + +scan_again: + cmp esi, edx + ja not_found + + xor ebp, ebp + +compare_again: + cmp ebp, ecx + jae found_table + + mov al, [esi + ebp] + cmp al, [edi + ebp] + jne next_candidate + + inc ebp + jmp compare_again + +next_candidate: + inc esi + jmp scan_again + +found_table: + # esi = BattleParamEntry_on.dat base + mov ecx, [ebx + patch_count - data] + mov edi, [ebx + signature_size - data] + lea edi, [ebx + payload - data + edi] # patch entry ptr after signature + +patch_again: + test ecx, ecx + jz done + + mov edx, [edi] # offset from table base + mov al, [edi + 4] # byte value + mov [esi + edx], al + + add edi, 5 + dec ecx + jmp patch_again + +done: + mov eax, esi # return found table base + jmp return + +not_found: + xor eax, eax + +return: + pop ebp + pop edi + pop esi + pop ebx + ret + +get_data_ptr: + call get_data_ptr_ret + +data: +scan_start: + .data 0 +scan_end: + .data 0 +signature_size: + .data 0 +patch_count: + .data 0 +payload: + # Server suffix: + # signature bytes + # repeated patch entries: + # uint32_t offset + # uint8_t value From f424d6ed3c7d0b359cec080b014f3f86a0dd5bd1 Mon Sep 17 00:00:00 2001 From: James Osborne Date: Sat, 6 Jun 2026 21:15:13 -0400 Subject: [PATCH 2/9] Avoid coroutine ICE in Brutal Peeps HP patch --- src/SendCommands.cc | 208 +++++++++++++++++++++++++------------------- 1 file changed, 119 insertions(+), 89 deletions(-) diff --git a/src/SendCommands.cc b/src/SendCommands.cc index c8780f82..7800ff03 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -775,105 +775,115 @@ static std::string bb_stream_file_data_for_client(std::shared_ptr c) { } -asio::awaitable send_brutal_peeps_hp_patch_bb(std::shared_ptr c, int64_t tier) { +static std::shared_ptr> send_brutal_peeps_hp_patch_bb_now( + std::shared_ptr c, + int64_t tier) { if (c->version() != Version::BB_V4) { - co_return; + return nullptr; } if (!c->check_flag(Client::Flag::HAS_SEND_FUNCTION_CALL) || !c->check_flag(Client::Flag::SEND_FUNCTION_CALL_ACTUALLY_RUNS_CODE)) { c->log.warning_f("Skipping Brutal Peeps HP client patch because client does not support executable send_function_call"); - co_return; + return nullptr; } - - auto s = c->require_server_state(); - const auto* brutal_peeps_def = brutal_peeps_tier_definition(tier); - if ((tier >= 0) && !brutal_peeps_def) { - c->log.warning_f("Skipping Brutal Peeps HP client patch for invalid tier {}", tier); - co_return; - } - - const double mult = brutal_peeps_def ? brutal_peeps_def->enemy_hp_multiplier : 1.0; - const BBStreamFile::Entry* bp_entry = nullptr; - - for (const auto& sf_entry : s->bb_stream_file->entries) { - if (sf_entry.filename == "BattleParamEntry_on.dat") { - bp_entry = &sf_entry; - break; - } - } - if (!bp_entry) { - c->log.warning_f("Skipping Brutal Peeps HP client patch: BattleParamEntry_on.dat not found in BB stream file"); - co_return; - } - if ((bp_entry->offset > s->bb_stream_file->data.size()) || - (bp_entry->size > (s->bb_stream_file->data.size() - bp_entry->offset)) || - (bp_entry->size < sizeof(BattleParamsIndex::Table))) { - c->log.warning_f("Skipping Brutal Peeps HP client patch: invalid BattleParamEntry_on.dat range"); - co_return; - } - - const char* vanilla_data = s->bb_stream_file->data.data() + bp_entry->offset; - std::string target_data(vanilla_data, bp_entry->size); - - auto* table = reinterpret_cast(target_data.data()); - size_t ultimate_index = static_cast(Difficulty::ULTIMATE); - - auto scale_u16 = [mult](uint32_t v) -> uint16_t { - if (v == 0) { - return 0; - } - uint32_t scaled = static_cast((static_cast(v) * mult) + 0.5); - if (scaled < 1) { - scaled = 1; - } - if (scaled > 0xFFFF) { - scaled = 0xFFFF; - } - return static_cast(scaled); - }; - - for (size_t z = 0; z < 0x60; z++) { - auto& stats = table->stats[ultimate_index][z]; - stats.char_stats.hp = scale_u16(stats.char_stats.hp); - } - - constexpr uint32_t scan_start = 0x16760000; - constexpr uint32_t scan_end = 0x16A90000; - constexpr uint32_t signature_size = 64; - constexpr uint32_t hp_patch_bytes = 0x60 * 2; - - if (bp_entry->size < signature_size) { - c->log.warning_f("Skipping Brutal Peeps HP client patch: BattleParamEntry_on.dat too small for signature"); - co_return; - } - - std::string suffix; - suffix.append(vanilla_data, signature_size); - - auto append_u32l = +[](std::string& out, uint32_t v) { - out.push_back(static_cast(v & 0xFF)); - out.push_back(static_cast((v >> 8) & 0xFF)); - out.push_back(static_cast((v >> 16) & 0xFF)); - out.push_back(static_cast((v >> 24) & 0xFF)); - }; - - for (size_t z = 0; z < 0x60; z++) { - const auto& hp = table->stats[ultimate_index][z].char_stats.hp; - uint32_t hp_offset = reinterpret_cast(&hp) - target_data.data(); - const uint8_t* hp_bytes = reinterpret_cast(&hp); - - append_u32l(suffix, hp_offset); - suffix.push_back(static_cast(hp_bytes[0])); - - append_u32l(suffix, hp_offset + 1); - suffix.push_back(static_cast(hp_bytes[1])); + if (!c->channel->connected()) { + c->log.warning_f("Skipping Brutal Peeps HP client patch because client is disconnected"); + return nullptr; } try { - co_await prepare_client_for_patches(c); + auto s = c->require_server_state(); + const auto* brutal_peeps_def = brutal_peeps_tier_definition(tier); + if ((tier >= 0) && !brutal_peeps_def) { + c->log.warning_f("Skipping Brutal Peeps HP client patch for invalid tier {}", tier); + return nullptr; + } + + const double mult = brutal_peeps_def ? brutal_peeps_def->enemy_hp_multiplier : 1.0; + const BBStreamFile::Entry* bp_entry = nullptr; + + for (const auto& sf_entry : s->bb_stream_file->entries) { + if (sf_entry.filename == "BattleParamEntry_on.dat") { + bp_entry = &sf_entry; + break; + } + } + if (!bp_entry) { + c->log.warning_f("Skipping Brutal Peeps HP client patch: BattleParamEntry_on.dat not found in BB stream file"); + return nullptr; + } + if ((bp_entry->offset > s->bb_stream_file->data.size()) || + (bp_entry->size > (s->bb_stream_file->data.size() - bp_entry->offset)) || + (bp_entry->size < sizeof(BattleParamsIndex::Table))) { + c->log.warning_f("Skipping Brutal Peeps HP client patch: invalid BattleParamEntry_on.dat range"); + return nullptr; + } + + const char* vanilla_data = s->bb_stream_file->data.data() + bp_entry->offset; + std::string target_data(vanilla_data, bp_entry->size); + + auto* table = reinterpret_cast(target_data.data()); + size_t ultimate_index = static_cast(Difficulty::ULTIMATE); + + auto scale_u16 = [mult](uint32_t v) -> uint16_t { + if (v == 0) { + return 0; + } + uint32_t scaled = static_cast((static_cast(v) * mult) + 0.5); + if (scaled < 1) { + scaled = 1; + } + if (scaled > 0xFFFF) { + scaled = 0xFFFF; + } + return static_cast(scaled); + }; + + for (size_t z = 0; z < 0x60; z++) { + auto& stats = table->stats[ultimate_index][z]; + stats.char_stats.hp = scale_u16(stats.char_stats.hp); + } + + constexpr uint32_t scan_start = 0x16760000; + constexpr uint32_t scan_end = 0x16A90000; + constexpr uint32_t signature_size = 64; + constexpr uint32_t hp_patch_bytes = 0x60 * 2; + + if (bp_entry->size < signature_size) { + c->log.warning_f("Skipping Brutal Peeps HP client patch: BattleParamEntry_on.dat too small for signature"); + return nullptr; + } + + std::string suffix; + suffix.append(vanilla_data, signature_size); + + auto append_u32l = +[](std::string& out, uint32_t v) { + out.push_back(static_cast(v & 0xFF)); + out.push_back(static_cast((v >> 8) & 0xFF)); + out.push_back(static_cast((v >> 16) & 0xFF)); + out.push_back(static_cast((v >> 24) & 0xFF)); + }; + + for (size_t z = 0; z < 0x60; z++) { + const auto& hp = table->stats[ultimate_index][z].char_stats.hp; + uint32_t hp_offset = reinterpret_cast(&hp) - target_data.data(); + const uint8_t* hp_bytes = reinterpret_cast(&hp); + + append_u32l(suffix, hp_offset); + suffix.push_back(static_cast(hp_bytes[0])); + + append_u32l(suffix, hp_offset + 1); + suffix.push_back(static_cast(hp_bytes[1])); + } + auto fn = s->client_functions->get("PsoPeepsBrutalPeepsHP", c->specific_version); - co_await send_function_call( - c, + + auto promise = std::make_shared>(); + c->function_call_response_queue.emplace_back(promise); + + send_function_call( + c->channel, + c->enabled_flags, fn, { {"scan_start", scan_start}, @@ -884,14 +894,34 @@ asio::awaitable send_brutal_peeps_hp_patch_bb(std::shared_ptr c, i suffix.data(), suffix.size()); + c->enabled_flags |= fn->client_flag; + c->log.info_f("Brutal Peeps HP client patch sent: tier={} mult={:g} patch_bytes={} scan={:08X}-{:08X}", tier, mult, hp_patch_bytes, scan_start, scan_end); + return promise; + } catch (const std::exception& e) { c->log.warning_f("Failed to send Brutal Peeps HP client patch: {}", e.what()); + return nullptr; } } +asio::awaitable send_brutal_peeps_hp_patch_bb(std::shared_ptr c, int64_t tier) { + try { + co_await prepare_client_for_patches(c); + + auto promise = send_brutal_peeps_hp_patch_bb_now(c, tier); + if (promise && c->channel->connected()) { + co_await promise->get(); + } + + } catch (const std::exception& e) { + c->log.warning_f("Failed to complete Brutal Peeps HP client patch: {}", e.what()); + } +} + + void send_stream_file_index_bb(std::shared_ptr c) { auto s = c->require_server_state(); From 976cb132a2ce709eba3e1b84585b2ae02743d910 Mon Sep 17 00:00:00 2001 From: James Osborne Date: Sat, 6 Jun 2026 21:24:29 -0400 Subject: [PATCH 3/9] Pass Brutal Peeps HP patch config in suffix --- src/SendCommands.cc | 17 ++++--- .../PsoPeepsBrutalPeepsHPBB.s | 47 +++++++++---------- 2 files changed, 29 insertions(+), 35 deletions(-) diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 7800ff03..aa8767ca 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -854,9 +854,6 @@ static std::shared_ptr> send_brutal_peeps_h return nullptr; } - std::string suffix; - suffix.append(vanilla_data, signature_size); - auto append_u32l = +[](std::string& out, uint32_t v) { out.push_back(static_cast(v & 0xFF)); out.push_back(static_cast((v >> 8) & 0xFF)); @@ -864,6 +861,13 @@ static std::shared_ptr> send_brutal_peeps_h out.push_back(static_cast((v >> 24) & 0xFF)); }; + std::string suffix; + append_u32l(suffix, scan_start); + append_u32l(suffix, scan_end); + append_u32l(suffix, signature_size); + append_u32l(suffix, hp_patch_bytes); + suffix.append(vanilla_data, signature_size); + for (size_t z = 0; z < 0x60; z++) { const auto& hp = table->stats[ultimate_index][z].char_stats.hp; uint32_t hp_offset = reinterpret_cast(&hp) - target_data.data(); @@ -885,12 +889,7 @@ static std::shared_ptr> send_brutal_peeps_h c->channel, c->enabled_flags, fn, - { - {"scan_start", scan_start}, - {"scan_end", scan_end}, - {"signature_size", signature_size}, - {"patch_count", hp_patch_bytes}, - }, + {}, suffix.data(), suffix.size()); diff --git a/system/client-functions/PsoPeepsBrutalPeepsHPBB.s b/system/client-functions/PsoPeepsBrutalPeepsHPBB.s index efc462c1..76cc29cc 100644 --- a/system/client-functions/PsoPeepsBrutalPeepsHPBB.s +++ b/system/client-functions/PsoPeepsBrutalPeepsHPBB.s @@ -18,13 +18,13 @@ start: jmp get_data_ptr get_data_ptr_ret: - pop ebx + pop ebx # ebx = suffix payload - mov esi, [ebx + scan_start - data] # candidate ptr - mov edx, [ebx + scan_end - data] # scan end - mov ecx, [ebx + signature_size - data] # signature size + mov esi, [ebx] # scan_start + mov edx, [ebx + 4] # scan_end + mov ecx, [ebx + 8] # signature_size sub edx, ecx # scan limit = end - sig_size - lea edi, [ebx + payload - data] # signature ptr + lea edi, [ebx + 16] # signature ptr scan_again: cmp esi, edx @@ -49,16 +49,17 @@ next_candidate: found_table: # esi = BattleParamEntry_on.dat base - mov ecx, [ebx + patch_count - data] - mov edi, [ebx + signature_size - data] - lea edi, [ebx + payload - data + edi] # patch entry ptr after signature + mov ecx, [ebx + 12] # patch entry count + mov edi, [ebx + 8] # signature_size + add edi, ebx + add edi, 16 # patch entries after header+signature patch_again: test ecx, ecx jz done - mov edx, [edi] # offset from table base - mov al, [edi + 4] # byte value + mov edx, [edi] # offset from table base + mov al, [edi + 4] # byte value mov [esi + edx], al add edi, 5 @@ -66,7 +67,7 @@ patch_again: jmp patch_again done: - mov eax, esi # return found table base + mov eax, esi # return found table base jmp return not_found: @@ -82,18 +83,12 @@ return: get_data_ptr: call get_data_ptr_ret -data: -scan_start: - .data 0 -scan_end: - .data 0 -signature_size: - .data 0 -patch_count: - .data 0 -payload: - # Server suffix: - # signature bytes - # repeated patch entries: - # uint32_t offset - # uint8_t value +# Server suffix starts here: +# uint32_t scan_start +# uint32_t scan_end +# uint32_t signature_size +# uint32_t patch_entry_count +# signature bytes +# repeated patch entries: +# uint32_t offset +# uint8_t value From b4a7374faeebcce92e54b0877ecc8130e82681fe Mon Sep 17 00:00:00 2001 From: James Osborne Date: Sat, 6 Jun 2026 21:35:22 -0400 Subject: [PATCH 4/9] Apply Brutal Peeps HP patch after BB area load --- src/ReceiveCommands.cc | 8 +++++++- src/SendCommands.cc | 6 +++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 8f730650..48af1bba 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -2762,7 +2762,6 @@ static asio::awaitable on_10_main_menu(std::shared_ptr c, uint32_t c->log.info_f("Brutal Peeps +{} selected from BB menu at level {}", tier, character_level); co_await send_auto_patches_if_needed(c); - co_await send_brutal_peeps_hp_patch_bb(c, tier); co_await enable_save_if_needed(c); send_lobby_list(c); if (!c->lobby.lock()) { @@ -5572,9 +5571,11 @@ static asio::awaitable on_6F(std::shared_ptr c, Channel::Message& // Episode 3 sends a 6F after a CAx21 (end battle) command, so we shouldn't reassign the item IDs again in that case // (even though item IDs really don't matter for Ep3) + bool loading_flag_cleared = false; if (c->check_flag(Client::Flag::LOADING)) { c->clear_flag(Client::Flag::LOADING); c->log.info_f("LOADING flag cleared"); + loading_flag_cleared = true; // The client sends 6F when it has created its TObjPlayer and assigned its item IDs. For the leader, however, this // happens before any inbound commands are processed, so we already did it when the client was added to the lobby. @@ -5584,6 +5585,11 @@ static asio::awaitable on_6F(std::shared_ptr c, Channel::Message& } } + if (loading_flag_cleared && (c->version() == Version::BB_V4)) { + int64_t brutal_peeps_hp_patch_tier = (l->brutal_peeps_tier >= 1) ? l->brutal_peeps_tier : -1; + co_await send_brutal_peeps_hp_patch_bb(c, brutal_peeps_hp_patch_tier); + } + // DC NTE creates players in the invisible state by default; if the joiner is not DC NTE, it won't send 6x23 to make // itself visible, so we have to do it for (const auto& lc : l->clients) { diff --git a/src/SendCommands.cc b/src/SendCommands.cc index aa8767ca..bc5309dc 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -912,7 +912,11 @@ asio::awaitable send_brutal_peeps_hp_patch_bb(std::shared_ptr c, i auto promise = send_brutal_peeps_hp_patch_bb_now(c, tier); if (promise && c->channel->connected()) { - co_await promise->get(); + auto result = co_await promise->get(); + c->log.info_f("Brutal Peeps HP client patch result: tier={} return_value={:08X} checksum={:08X}", + tier, + static_cast(result.return_value), + static_cast(result.checksum)); } } catch (const std::exception& e) { From 942dbbc5b9afbb423bc34850d8169dcc22ba3b1f Mon Sep 17 00:00:00 2001 From: James Osborne Date: Sat, 6 Jun 2026 21:52:42 -0400 Subject: [PATCH 5/9] Patch matching BB battle param tables with raw HP diffs --- src/SendCommands.cc | 31 +++++++------ .../PsoPeepsBrutalPeepsHPBB.s | 43 ++++++++++--------- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/src/SendCommands.cc b/src/SendCommands.cc index bc5309dc..b96fe47a 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -847,7 +847,6 @@ static std::shared_ptr> send_brutal_peeps_h constexpr uint32_t scan_start = 0x16760000; constexpr uint32_t scan_end = 0x16A90000; constexpr uint32_t signature_size = 64; - constexpr uint32_t hp_patch_bytes = 0x60 * 2; if (bp_entry->size < signature_size) { c->log.warning_f("Skipping Brutal Peeps HP client patch: BattleParamEntry_on.dat too small for signature"); @@ -865,21 +864,27 @@ static std::shared_ptr> send_brutal_peeps_h append_u32l(suffix, scan_start); append_u32l(suffix, scan_end); append_u32l(suffix, signature_size); - append_u32l(suffix, hp_patch_bytes); + append_u32l(suffix, 0); // patched below after diff generation suffix.append(vanilla_data, signature_size); - for (size_t z = 0; z < 0x60; z++) { - const auto& hp = table->stats[ultimate_index][z].char_stats.hp; - uint32_t hp_offset = reinterpret_cast(&hp) - target_data.data(); - const uint8_t* hp_bytes = reinterpret_cast(&hp); + uint32_t patch_entry_count = 0; + for (uint32_t offset = 0; offset < target_data.size(); offset++) { + uint8_t old_byte = static_cast(vanilla_data[offset]); + uint8_t new_byte = static_cast(target_data[offset]); + if (old_byte == new_byte) { + continue; + } - append_u32l(suffix, hp_offset); - suffix.push_back(static_cast(hp_bytes[0])); - - append_u32l(suffix, hp_offset + 1); - suffix.push_back(static_cast(hp_bytes[1])); + append_u32l(suffix, offset); + suffix.push_back(static_cast(new_byte)); + patch_entry_count++; } + suffix[12] = static_cast(patch_entry_count & 0xFF); + suffix[13] = static_cast((patch_entry_count >> 8) & 0xFF); + suffix[14] = static_cast((patch_entry_count >> 16) & 0xFF); + suffix[15] = static_cast((patch_entry_count >> 24) & 0xFF); + auto fn = s->client_functions->get("PsoPeepsBrutalPeepsHP", c->specific_version); auto promise = std::make_shared>(); @@ -895,8 +900,8 @@ static std::shared_ptr> send_brutal_peeps_h c->enabled_flags |= fn->client_flag; - c->log.info_f("Brutal Peeps HP client patch sent: tier={} mult={:g} patch_bytes={} scan={:08X}-{:08X}", - tier, mult, hp_patch_bytes, scan_start, scan_end); + c->log.info_f("Brutal Peeps HP client patch sent: tier={} mult={:g} patch_entries={} scan={:08X}-{:08X}", + tier, mult, patch_entry_count, scan_start, scan_end); return promise; diff --git a/system/client-functions/PsoPeepsBrutalPeepsHPBB.s b/system/client-functions/PsoPeepsBrutalPeepsHPBB.s index 76cc29cc..0350735d 100644 --- a/system/client-functions/PsoPeepsBrutalPeepsHPBB.s +++ b/system/client-functions/PsoPeepsBrutalPeepsHPBB.s @@ -14,23 +14,24 @@ start: push esi push edi push ebp + push 0 # [esp] = last matched table base / 0 jmp get_data_ptr get_data_ptr_ret: - pop ebx # ebx = suffix payload + pop ebx # ebx = suffix payload - mov esi, [ebx] # scan_start - mov edx, [ebx + 4] # scan_end - mov ecx, [ebx + 8] # signature_size - sub edx, ecx # scan limit = end - sig_size - lea edi, [ebx + 16] # signature ptr + mov esi, [ebx] # scan_start scan_again: + mov edx, [ebx + 4] # scan_end + mov ecx, [ebx + 8] # signature_size + sub edx, ecx # scan limit = end - sig_size cmp esi, edx - ja not_found + ja return xor ebp, ebp + lea edi, [ebx + 16] # signature ptr compare_again: cmp ebp, ecx @@ -48,32 +49,32 @@ next_candidate: jmp scan_again found_table: - # esi = BattleParamEntry_on.dat base - mov ecx, [ebx + 12] # patch entry count - mov edi, [ebx + 8] # signature_size - add edi, ebx - add edi, 16 # patch entries after header+signature + # esi = one matching BattleParam table base + mov [esp], esi # remember last match for return_value + + mov ecx, [ebx + 12] # patch entry count + mov edi, [ebx + 8] # signature_size + lea edi, [ebx + edi + 16] # patch entries after header+signature patch_again: test ecx, ecx - jz done + jz after_patch - mov edx, [edi] # offset from table base - mov al, [edi + 4] # byte value + mov edx, [edi] # offset from table base + mov al, [edi + 4] # byte value mov [esi + edx], al add edi, 5 dec ecx jmp patch_again -done: - mov eax, esi # return found table base - jmp return - -not_found: - xor eax, eax +after_patch: + inc esi # continue scanning after this match + jmp scan_again return: + mov eax, [esp] # 0 if none found, else last matched base + add esp, 4 pop ebp pop edi pop esi From eba565c381bb38810f1670add6299aa718464396 Mon Sep 17 00:00:00 2001 From: James Osborne Date: Sat, 6 Jun 2026 21:59:05 -0400 Subject: [PATCH 6/9] Use table-offset signature for Brutal Peeps HP patch --- src/SendCommands.cc | 20 +++---- .../PsoPeepsBrutalPeepsHPBB.s | 52 ++++++++++--------- 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/src/SendCommands.cc b/src/SendCommands.cc index b96fe47a..ddf9c658 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -846,9 +846,10 @@ static std::shared_ptr> send_brutal_peeps_h constexpr uint32_t scan_start = 0x16760000; constexpr uint32_t scan_end = 0x16A90000; - constexpr uint32_t signature_size = 64; + constexpr uint32_t signature_offset = 0x00002800; + constexpr uint32_t signature_size = 0x80; - if (bp_entry->size < signature_size) { + if (bp_entry->size < (signature_offset + signature_size)) { c->log.warning_f("Skipping Brutal Peeps HP client patch: BattleParamEntry_on.dat too small for signature"); return nullptr; } @@ -863,9 +864,10 @@ static std::shared_ptr> send_brutal_peeps_h std::string suffix; append_u32l(suffix, scan_start); append_u32l(suffix, scan_end); + append_u32l(suffix, signature_offset); append_u32l(suffix, signature_size); append_u32l(suffix, 0); // patched below after diff generation - suffix.append(vanilla_data, signature_size); + suffix.append(vanilla_data + signature_offset, signature_size); uint32_t patch_entry_count = 0; for (uint32_t offset = 0; offset < target_data.size(); offset++) { @@ -880,10 +882,10 @@ static std::shared_ptr> send_brutal_peeps_h patch_entry_count++; } - suffix[12] = static_cast(patch_entry_count & 0xFF); - suffix[13] = static_cast((patch_entry_count >> 8) & 0xFF); - suffix[14] = static_cast((patch_entry_count >> 16) & 0xFF); - suffix[15] = static_cast((patch_entry_count >> 24) & 0xFF); + suffix[16] = static_cast(patch_entry_count & 0xFF); + suffix[17] = static_cast((patch_entry_count >> 8) & 0xFF); + suffix[18] = static_cast((patch_entry_count >> 16) & 0xFF); + suffix[19] = static_cast((patch_entry_count >> 24) & 0xFF); auto fn = s->client_functions->get("PsoPeepsBrutalPeepsHP", c->specific_version); @@ -900,8 +902,8 @@ static std::shared_ptr> send_brutal_peeps_h c->enabled_flags |= fn->client_flag; - c->log.info_f("Brutal Peeps HP client patch sent: tier={} mult={:g} patch_entries={} scan={:08X}-{:08X}", - tier, mult, patch_entry_count, scan_start, scan_end); + c->log.info_f("Brutal Peeps HP client patch sent: tier={} mult={:g} patch_entries={} signature_offset={:05X} scan={:08X}-{:08X}", + tier, mult, patch_entry_count, signature_offset, scan_start, scan_end); return promise; diff --git a/system/client-functions/PsoPeepsBrutalPeepsHPBB.s b/system/client-functions/PsoPeepsBrutalPeepsHPBB.s index 0350735d..fb40fc2c 100644 --- a/system/client-functions/PsoPeepsBrutalPeepsHPBB.s +++ b/system/client-functions/PsoPeepsBrutalPeepsHPBB.s @@ -14,28 +14,27 @@ start: push esi push edi push ebp - push 0 # [esp] = last matched table base / 0 jmp get_data_ptr get_data_ptr_ret: - pop ebx # ebx = suffix payload + pop ebx # ebx = suffix payload - mov esi, [ebx] # scan_start + mov esi, [ebx] # scan_start, scans for signature address, not table base scan_again: - mov edx, [ebx + 4] # scan_end - mov ecx, [ebx + 8] # signature_size - sub edx, ecx # scan limit = end - sig_size + mov edx, [ebx + 4] # scan_end + mov ecx, [ebx + 12] # signature_size + sub edx, ecx # scan limit = end - sig_size cmp esi, edx - ja return + ja not_found xor ebp, ebp - lea edi, [ebx + 16] # signature ptr + lea edi, [ebx + 20] # signature ptr compare_again: cmp ebp, ecx - jae found_table + jae found_signature mov al, [esi + ebp] cmp al, [edi + ebp] @@ -48,33 +47,35 @@ next_candidate: inc esi jmp scan_again -found_table: - # esi = one matching BattleParam table base - mov [esp], esi # remember last match for return_value +found_signature: + # esi = signature address; table base = esi - signature_offset + mov ebp, esi + sub ebp, [ebx + 8] # ebp = BattleParam table base - mov ecx, [ebx + 12] # patch entry count - mov edi, [ebx + 8] # signature_size - lea edi, [ebx + edi + 16] # patch entries after header+signature + mov ecx, [ebx + 16] # patch entry count + mov edi, [ebx + 12] # signature_size + lea edi, [ebx + edi + 20] # patch entries after header+signature patch_again: test ecx, ecx - jz after_patch + jz done - mov edx, [edi] # offset from table base - mov al, [edi + 4] # byte value - mov [esi + edx], al + mov edx, [edi] # offset from table base + mov al, [edi + 4] # byte value + mov [ebp + edx], al add edi, 5 dec ecx jmp patch_again -after_patch: - inc esi # continue scanning after this match - jmp scan_again +done: + mov eax, ebp # return found table base + jmp return + +not_found: + xor eax, eax return: - mov eax, [esp] # 0 if none found, else last matched base - add esp, 4 pop ebp pop edi pop esi @@ -87,9 +88,10 @@ get_data_ptr: # Server suffix starts here: # uint32_t scan_start # uint32_t scan_end +# uint32_t signature_offset # uint32_t signature_size # uint32_t patch_entry_count -# signature bytes +# signature bytes from table+signature_offset # repeated patch entries: # uint32_t offset # uint8_t value From c7fb0cf5f6bf36f3c42310ea313692fcad27543f Mon Sep 17 00:00:00 2001 From: James Osborne Date: Sat, 6 Jun 2026 22:06:06 -0400 Subject: [PATCH 7/9] Use raw HP diffs with simple BattleParam scanner --- src/SendCommands.cc | 20 ++++++------ .../PsoPeepsBrutalPeepsHPBB.s | 32 ++++++++----------- 2 files changed, 23 insertions(+), 29 deletions(-) diff --git a/src/SendCommands.cc b/src/SendCommands.cc index ddf9c658..b96fe47a 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -846,10 +846,9 @@ static std::shared_ptr> send_brutal_peeps_h constexpr uint32_t scan_start = 0x16760000; constexpr uint32_t scan_end = 0x16A90000; - constexpr uint32_t signature_offset = 0x00002800; - constexpr uint32_t signature_size = 0x80; + constexpr uint32_t signature_size = 64; - if (bp_entry->size < (signature_offset + signature_size)) { + if (bp_entry->size < signature_size) { c->log.warning_f("Skipping Brutal Peeps HP client patch: BattleParamEntry_on.dat too small for signature"); return nullptr; } @@ -864,10 +863,9 @@ static std::shared_ptr> send_brutal_peeps_h std::string suffix; append_u32l(suffix, scan_start); append_u32l(suffix, scan_end); - append_u32l(suffix, signature_offset); append_u32l(suffix, signature_size); append_u32l(suffix, 0); // patched below after diff generation - suffix.append(vanilla_data + signature_offset, signature_size); + suffix.append(vanilla_data, signature_size); uint32_t patch_entry_count = 0; for (uint32_t offset = 0; offset < target_data.size(); offset++) { @@ -882,10 +880,10 @@ static std::shared_ptr> send_brutal_peeps_h patch_entry_count++; } - suffix[16] = static_cast(patch_entry_count & 0xFF); - suffix[17] = static_cast((patch_entry_count >> 8) & 0xFF); - suffix[18] = static_cast((patch_entry_count >> 16) & 0xFF); - suffix[19] = static_cast((patch_entry_count >> 24) & 0xFF); + suffix[12] = static_cast(patch_entry_count & 0xFF); + suffix[13] = static_cast((patch_entry_count >> 8) & 0xFF); + suffix[14] = static_cast((patch_entry_count >> 16) & 0xFF); + suffix[15] = static_cast((patch_entry_count >> 24) & 0xFF); auto fn = s->client_functions->get("PsoPeepsBrutalPeepsHP", c->specific_version); @@ -902,8 +900,8 @@ static std::shared_ptr> send_brutal_peeps_h c->enabled_flags |= fn->client_flag; - c->log.info_f("Brutal Peeps HP client patch sent: tier={} mult={:g} patch_entries={} signature_offset={:05X} scan={:08X}-{:08X}", - tier, mult, patch_entry_count, signature_offset, scan_start, scan_end); + c->log.info_f("Brutal Peeps HP client patch sent: tier={} mult={:g} patch_entries={} scan={:08X}-{:08X}", + tier, mult, patch_entry_count, scan_start, scan_end); return promise; diff --git a/system/client-functions/PsoPeepsBrutalPeepsHPBB.s b/system/client-functions/PsoPeepsBrutalPeepsHPBB.s index fb40fc2c..13d3b1aa 100644 --- a/system/client-functions/PsoPeepsBrutalPeepsHPBB.s +++ b/system/client-functions/PsoPeepsBrutalPeepsHPBB.s @@ -20,21 +20,21 @@ start: get_data_ptr_ret: pop ebx # ebx = suffix payload - mov esi, [ebx] # scan_start, scans for signature address, not table base + mov esi, [ebx] # scan_start + mov edx, [ebx + 4] # scan_end + mov ecx, [ebx + 8] # signature_size + sub edx, ecx # scan limit = end - sig_size + lea edi, [ebx + 16] # signature ptr scan_again: - mov edx, [ebx + 4] # scan_end - mov ecx, [ebx + 12] # signature_size - sub edx, ecx # scan limit = end - sig_size cmp esi, edx ja not_found xor ebp, ebp - lea edi, [ebx + 20] # signature ptr compare_again: cmp ebp, ecx - jae found_signature + jae found_table mov al, [esi + ebp] cmp al, [edi + ebp] @@ -47,14 +47,11 @@ next_candidate: inc esi jmp scan_again -found_signature: - # esi = signature address; table base = esi - signature_offset - mov ebp, esi - sub ebp, [ebx + 8] # ebp = BattleParam table base - - mov ecx, [ebx + 16] # patch entry count - mov edi, [ebx + 12] # signature_size - lea edi, [ebx + edi + 20] # patch entries after header+signature +found_table: + # esi = BattleParamEntry_on.dat base + mov ecx, [ebx + 12] # patch entry count + mov edi, [ebx + 8] # signature_size + lea edi, [ebx + edi + 16] # patch entries after header+signature patch_again: test ecx, ecx @@ -62,14 +59,14 @@ patch_again: mov edx, [edi] # offset from table base mov al, [edi + 4] # byte value - mov [ebp + edx], al + mov [esi + edx], al add edi, 5 dec ecx jmp patch_again done: - mov eax, ebp # return found table base + mov eax, esi # return found table base jmp return not_found: @@ -88,10 +85,9 @@ get_data_ptr: # Server suffix starts here: # uint32_t scan_start # uint32_t scan_end -# uint32_t signature_offset # uint32_t signature_size # uint32_t patch_entry_count -# signature bytes from table+signature_offset +# signature bytes from table start # repeated patch entries: # uint32_t offset # uint8_t value From 672be0b6b8513c87671988531fc9d6519a288e08 Mon Sep 17 00:00:00 2001 From: James Osborne Date: Sat, 6 Jun 2026 22:11:31 -0400 Subject: [PATCH 8/9] Patch Brutal Peeps HP before BB room load --- src/ReceiveCommands.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 48af1bba..0ff9d7c6 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -2762,6 +2762,7 @@ static asio::awaitable on_10_main_menu(std::shared_ptr c, uint32_t c->log.info_f("Brutal Peeps +{} selected from BB menu at level {}", tier, character_level); co_await send_auto_patches_if_needed(c); + co_await send_brutal_peeps_hp_patch_bb(c, tier); co_await enable_save_if_needed(c); send_lobby_list(c); if (!c->lobby.lock()) { From 5feffea722fdb1f48ac923e1b9e743f1c7705653 Mon Sep 17 00:00:00 2001 From: James Osborne Date: Sat, 6 Jun 2026 22:18:52 -0400 Subject: [PATCH 9/9] Use raw BattleParam HP offsets for Brutal Peeps patch --- src/SendCommands.cc | 70 ++++++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/src/SendCommands.cc b/src/SendCommands.cc index b96fe47a..f30dd957 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -820,10 +820,32 @@ static std::shared_ptr> send_brutal_peeps_h } const char* vanilla_data = s->bb_stream_file->data.data() + bp_entry->offset; - std::string target_data(vanilla_data, bp_entry->size); - auto* table = reinterpret_cast(target_data.data()); - size_t ultimate_index = static_cast(Difficulty::ULTIMATE); + constexpr uint32_t scan_start = 0x16760000; + constexpr uint32_t scan_end = 0x16A90000; + constexpr uint32_t signature_size = 64; + + // Raw BattleParamEntry*.dat layout: + // Ultimate stats HP starts at 0x2886 and each stats row is 0x24 bytes. + constexpr uint32_t ultimate_hp_base_offset = 0x00002886; + constexpr uint32_t stats_row_size = 0x24; + constexpr uint32_t num_bp_rows = 0x60; + + if (bp_entry->size < signature_size) { + c->log.warning_f("Skipping Brutal Peeps HP client patch: BattleParamEntry_on.dat too small for signature"); + return nullptr; + } + if (bp_entry->size < (ultimate_hp_base_offset + ((num_bp_rows - 1) * stats_row_size) + 2)) { + c->log.warning_f("Skipping Brutal Peeps HP client patch: BattleParamEntry_on.dat too small for Ultimate HP table"); + return nullptr; + } + + auto append_u32l = +[](std::string& out, uint32_t v) { + out.push_back(static_cast(v & 0xFF)); + out.push_back(static_cast((v >> 8) & 0xFF)); + out.push_back(static_cast((v >> 16) & 0xFF)); + out.push_back(static_cast((v >> 24) & 0xFF)); + }; auto scale_u16 = [mult](uint32_t v) -> uint16_t { if (v == 0) { @@ -839,44 +861,26 @@ static std::shared_ptr> send_brutal_peeps_h return static_cast(scaled); }; - for (size_t z = 0; z < 0x60; z++) { - auto& stats = table->stats[ultimate_index][z]; - stats.char_stats.hp = scale_u16(stats.char_stats.hp); - } - - constexpr uint32_t scan_start = 0x16760000; - constexpr uint32_t scan_end = 0x16A90000; - constexpr uint32_t signature_size = 64; - - if (bp_entry->size < signature_size) { - c->log.warning_f("Skipping Brutal Peeps HP client patch: BattleParamEntry_on.dat too small for signature"); - return nullptr; - } - - auto append_u32l = +[](std::string& out, uint32_t v) { - out.push_back(static_cast(v & 0xFF)); - out.push_back(static_cast((v >> 8) & 0xFF)); - out.push_back(static_cast((v >> 16) & 0xFF)); - out.push_back(static_cast((v >> 24) & 0xFF)); - }; - std::string suffix; append_u32l(suffix, scan_start); append_u32l(suffix, scan_end); append_u32l(suffix, signature_size); - append_u32l(suffix, 0); // patched below after diff generation + append_u32l(suffix, 0); // patched below after HP patch generation suffix.append(vanilla_data, signature_size); uint32_t patch_entry_count = 0; - for (uint32_t offset = 0; offset < target_data.size(); offset++) { - uint8_t old_byte = static_cast(vanilla_data[offset]); - uint8_t new_byte = static_cast(target_data[offset]); - if (old_byte == new_byte) { - continue; - } + for (uint32_t z = 0; z < num_bp_rows; z++) { + uint32_t hp_offset = ultimate_hp_base_offset + (z * stats_row_size); + uint16_t old_hp = static_cast(vanilla_data[hp_offset]) | + (static_cast(static_cast(vanilla_data[hp_offset + 1])) << 8); + uint16_t new_hp = scale_u16(old_hp); - append_u32l(suffix, offset); - suffix.push_back(static_cast(new_byte)); + append_u32l(suffix, hp_offset); + suffix.push_back(static_cast(new_hp & 0xFF)); + patch_entry_count++; + + append_u32l(suffix, hp_offset + 1); + suffix.push_back(static_cast((new_hp >> 8) & 0xFF)); patch_entry_count++; }