From 30e645fdeb2f0b06edcf84e31c67ffdc0e8e45d7 Mon Sep 17 00:00:00 2001 From: James Osborne Date: Sun, 7 Jun 2026 02:57:26 -0400 Subject: [PATCH 01/11] Add Brutal Peeps PC client memory patch --- src/SendCommands.cc | 221 +++++++++++++++++- .../client-functions/PsoPeepsBrutalPeepsPC.s | 93 ++++++++ 2 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 system/client-functions/PsoPeepsBrutalPeepsPC.s diff --git a/src/SendCommands.cc b/src/SendCommands.cc index b7281f29..8e45cb91 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -989,11 +989,230 @@ static std::vector>>> send_brutal_peeps_hp_patch_pc_now( + std::shared_ptr c, + int64_t tier) { + std::vector>>> promises; + + if (c->version() != Version::PC_V2) { + return promises; + } + 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 PC client patch because client does not support executable send_function_call"); + return promises; + } + if (!c->channel->connected()) { + c->log.warning_f("Skipping Brutal Peeps PC client patch because client is disconnected"); + return promises; + } + + try { + 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 PC client patch for invalid tier {}", tier); + return promises; + } + + const double hp_mult = brutal_peeps_def ? brutal_peeps_def->enemy_hp_multiplier : 1.0; + const double exp_mult = brutal_peeps_def ? brutal_peeps_def->exp_multiplier : 1.0; + const double atp_mult = [&]() -> double { + switch (tier) { + case 1: + return 1.01; + case 2: + return 1.02; + case 3: + return 1.03; + case 4: + return 1.04; + case 5: + return 1.05; + case 6: + return 1.06; + case 7: + return 1.07; + case 8: + return 1.08; + case 9: + return 1.09; + case 10: + case 11: + return 1.10; + default: + return 1.00; + } + }(); + + constexpr const char* bp_filename = "BattleParamEntry_on.dat"; + std::string vanilla_data = phosg::load_file("system/patch-pc/Media/PSO/BattleParamEntry_on.dat"); + + constexpr uint32_t scan_start = 0x01A10000; + constexpr uint32_t scan_end = 0x02E21000; + constexpr uint32_t signature_size = 64; + + // PC V2 BattleParamEntry_on.dat layout: + // Ultimate stats rows start at 0x2880 and each stats row is 0x24 bytes. + // Within each row: ATP is +0x00, HP is +0x06, EXP is +0x1C. + // + // Important: PC memory does not appear to contain a byte-perfect full file + // image, but the Ultimate block is present. Therefore the signature starts + // at ultimate_block_offset, and all patch offsets are relative to the + // Ultimate block address returned by the client function. + constexpr uint32_t ultimate_block_offset = 0x00002880; + constexpr uint32_t ultimate_atp_row_offset = 0x00; + constexpr uint32_t ultimate_hp_row_offset = 0x06; + constexpr uint32_t ultimate_exp_row_offset = 0x1C; + constexpr uint32_t stats_row_size = 0x24; + constexpr uint32_t num_bp_rows = 0x60; + + 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 read_u16l = +[](const std::string& data, uint32_t offset) -> uint16_t { + return static_cast(data[offset]) | + (static_cast(static_cast(data[offset + 1])) << 8); + }; + + auto read_u32l = +[](const std::string& data, uint32_t offset) -> uint32_t { + return static_cast(data[offset]) | + (static_cast(static_cast(data[offset + 1])) << 8) | + (static_cast(static_cast(data[offset + 2])) << 16) | + (static_cast(static_cast(data[offset + 3])) << 24); + }; + + auto scale_u16 = +[](uint32_t v, double scale) -> uint16_t { + if (v == 0) { + return 0; + } + uint32_t scaled = static_cast((static_cast(v) * scale) + 0.5); + if (scaled < 1) { + scaled = 1; + } + if (scaled > 0xFFFF) { + scaled = 0xFFFF; + } + return static_cast(scaled); + }; + + auto scale_u32 = +[](uint32_t v, double scale) -> uint32_t { + if (v == 0) { + return 0; + } + double scaled_f = (static_cast(v) * scale) + 0.5; + if (scaled_f < 1.0) { + return 1; + } + if (scaled_f > 4294967295.0) { + return 0xFFFFFFFF; + } + return static_cast(scaled_f); + }; + + constexpr uint32_t last_needed_offset = + ultimate_block_offset + ((num_bp_rows - 1) * stats_row_size) + ultimate_exp_row_offset + 4; + if (vanilla_data.size() < last_needed_offset) { + c->log.warning_f("Skipping Brutal Peeps PC client patch: {} too small for Ultimate stats table", bp_filename); + return promises; + } + + 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 patch generation + suffix.append(vanilla_data.data() + ultimate_block_offset, signature_size); + + uint32_t patch_entry_count = 0; + + for (uint32_t z = 0; z < num_bp_rows; z++) { + const uint32_t row_file_offset = ultimate_block_offset + (z * stats_row_size); + const uint32_t row_patch_offset = z * stats_row_size; + + const uint32_t atp_file_offset = row_file_offset + ultimate_atp_row_offset; + const uint32_t atp_patch_offset = row_patch_offset + ultimate_atp_row_offset; + uint16_t old_atp = read_u16l(vanilla_data, atp_file_offset); + uint16_t new_atp = scale_u16(old_atp, atp_mult); + + append_u32l(suffix, atp_patch_offset); + suffix.push_back(static_cast(new_atp & 0xFF)); + patch_entry_count++; + + append_u32l(suffix, atp_patch_offset + 1); + suffix.push_back(static_cast((new_atp >> 8) & 0xFF)); + patch_entry_count++; + + const uint32_t hp_file_offset = row_file_offset + ultimate_hp_row_offset; + const uint32_t hp_patch_offset = row_patch_offset + ultimate_hp_row_offset; + uint16_t old_hp = read_u16l(vanilla_data, hp_file_offset); + uint16_t new_hp = scale_u16(old_hp, hp_mult); + + append_u32l(suffix, hp_patch_offset); + suffix.push_back(static_cast(new_hp & 0xFF)); + patch_entry_count++; + + append_u32l(suffix, hp_patch_offset + 1); + suffix.push_back(static_cast((new_hp >> 8) & 0xFF)); + patch_entry_count++; + + const uint32_t exp_file_offset = row_file_offset + ultimate_exp_row_offset; + const uint32_t exp_patch_offset = row_patch_offset + ultimate_exp_row_offset; + uint32_t old_exp = read_u32l(vanilla_data, exp_file_offset); + uint32_t new_exp = scale_u32(old_exp, exp_mult); + + for (uint32_t x = 0; x < 4; x++) { + append_u32l(suffix, exp_patch_offset + x); + suffix.push_back(static_cast((new_exp >> (x * 8)) & 0xFF)); + 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("PsoPeepsBrutalPeepsPC", c->specific_version); + + auto promise = std::make_shared>(); + c->function_call_response_queue.emplace_back(promise); + + send_function_call( + c->channel, + c->enabled_flags, + fn, + {}, + suffix.data(), + suffix.size()); + + c->enabled_flags |= fn->client_flag; + promises.emplace_back(bp_filename, promise); + + c->log.info_f("Brutal Peeps PC ATP/HP/EXP client patch sent for {}: tier={} hp_mult={:g} atp_mult={:g} exp_mult={:g} patch_entries={} suffix_size={} scan={:08X}-{:08X}", + bp_filename, tier, hp_mult, atp_mult, exp_mult, patch_entry_count, suffix.size(), scan_start, scan_end); + + return promises; + + } catch (const std::exception& e) { + c->log.warning_f("Failed to send Brutal Peeps PC client patch: {}", e.what()); + return promises; + } +} + + asio::awaitable send_brutal_peeps_hp_patch_bb(std::shared_ptr c, int64_t tier) { try { co_await prepare_client_for_patches(c); - auto promises = send_brutal_peeps_hp_patch_bb_now(c, tier); + auto promises = (c->version() == Version::PC_V2) + ? send_brutal_peeps_hp_patch_pc_now(c, tier) + : send_brutal_peeps_hp_patch_bb_now(c, tier); for (auto& it : promises) { const auto& filename = it.first; auto& promise = it.second; diff --git a/system/client-functions/PsoPeepsBrutalPeepsPC.s b/system/client-functions/PsoPeepsBrutalPeepsPC.s new file mode 100644 index 00000000..be7e2012 --- /dev/null +++ b/system/client-functions/PsoPeepsBrutalPeepsPC.s @@ -0,0 +1,93 @@ +.meta key="PsoPeepsBrutalPeepsPC" +.meta name="Brutal Peeps PC" +.meta description="Applies Brutal Peeps\nPC ATP/HP/EXP scaling" +.meta show_return_value + +.versions 2OJW 2OJZ + +entry_ptr: +reloc0: + .offsetof start + +start: + push ebx + push esi + push edi + push ebp + + jmp get_data_ptr + +get_data_ptr_ret: + 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 + +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 + 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 + + 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 + +# Server suffix starts here: +# uint32_t scan_start +# uint32_t scan_end +# uint32_t signature_size +# uint32_t patch_entry_count +# signature bytes from table start +# repeated patch entries: +# uint32_t offset +# uint8_t value From a1c3beafacea2ef2a26f360e5f9a8eaa9b68ce0d Mon Sep 17 00:00:00 2001 From: James Osborne Date: Sun, 7 Jun 2026 03:06:09 -0400 Subject: [PATCH 02/11] Allow Brutal Peeps menu for PC --- src/ReceiveCommands.cc | 19 ++++++++++++++----- src/SendCommands.cc | 2 +- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 0ff9d7c6..e31c2e9e 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -314,8 +314,16 @@ static void send_main_menu(std::shared_ptr c) { ? max_brutal_peeps_tier_for_level(character_level) : -1; + bool supports_brutal_peeps_menu = + bb_destination_transport_menu || + (c->version() == Version::PC_V2); + + auto brutal_peeps_menu_item_flags = (c->version() == Version::BB_V4) + ? MenuItem::Flag::BB_ONLY + : static_cast(0); + bool show_brutal_peeps_menu_items = - bb_destination_transport_menu && + supports_brutal_peeps_menu && s->enable_brutal_peeps_mode && (max_brutal_peeps_menu_tier >= 1); @@ -325,7 +333,7 @@ static void send_main_menu(std::shared_ptr c) { MainMenuItemID::BRUTAL_PEEPS_PLUS1 + static_cast(tier - 1), std::format("Brutal Peeps +{}", tier), std::format("Enter Brutal Peeps\n+{}", tier), - MenuItem::Flag::BB_ONLY); + brutal_peeps_menu_item_flags); } } @@ -2748,7 +2756,7 @@ static asio::awaitable on_10_main_menu(std::shared_ptr c, uint32_t : 0; if (!s->enable_brutal_peeps_mode || - !is_v4(c->version()) || + !((c->version() == Version::BB_V4) || (c->version() == Version::PC_V2)) || !brutal_peeps_def || (character_level < brutal_peeps_def->required_level)) { send_message_box(c, std::format( @@ -2759,7 +2767,7 @@ static asio::awaitable on_10_main_menu(std::shared_ptr c, uint32_t } c->selected_brutal_peeps_tier = tier; - c->log.info_f("Brutal Peeps +{} selected from BB menu at level {}", tier, character_level); + c->log.info_f("Brutal Peeps +{} selected from ship menu at level {}", tier, character_level); co_await send_auto_patches_if_needed(c); co_await send_brutal_peeps_hp_patch_bb(c, tier); @@ -5586,7 +5594,8 @@ static asio::awaitable on_6F(std::shared_ptr c, Channel::Message& } } - if (loading_flag_cleared && (c->version() == Version::BB_V4)) { + if (loading_flag_cleared && + ((c->version() == Version::BB_V4) || (c->version() == Version::PC_V2))) { 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); } diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 8e45cb91..e87b2e18 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -1049,7 +1049,7 @@ static std::vector Date: Sun, 7 Jun 2026 03:18:24 -0400 Subject: [PATCH 03/11] Allow PC to create Brutal Peeps rooms --- src/ReceiveCommands.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index e31c2e9e..18b7be89 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -5133,14 +5133,14 @@ std::shared_ptr create_game_generic( if (requested_brutal_peeps_tier >= 0) { const auto* brutal_peeps_def = brutal_peeps_tier_definition(requested_brutal_peeps_tier); if (s->enable_brutal_peeps_mode && - is_v4(creator_c->version()) && + ((creator_c->version() == Version::BB_V4) || (creator_c->version() == Version::PC_V2)) && (difficulty == Difficulty::ULTIMATE) && brutal_peeps_def && (creator_character_level >= brutal_peeps_def->required_level)) { game->brutal_peeps_tier = requested_brutal_peeps_tier; game->set_flag(Lobby::Flag::BRUTAL_PEEPS_MODE); creator_c->selected_brutal_peeps_tier = requested_brutal_peeps_tier; - game->log.info_f("Brutal Peeps +{} enabled for BB Ultimate game via {} at creator level {}", + game->log.info_f("Brutal Peeps +{} enabled for BB/PC Ultimate game via {} at creator level {}", static_cast(game->brutal_peeps_tier), requested_brutal_peeps_source, creator_character_level); From 1ef2a7e1e212641bec609d7d7c7178aa3460bced Mon Sep 17 00:00:00 2001 From: James Osborne Date: Sun, 7 Jun 2026 03:24:53 -0400 Subject: [PATCH 04/11] Defer PC Brutal Peeps memory patch until room load --- src/ReceiveCommands.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 18b7be89..a16b7e8f 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -2770,7 +2770,9 @@ static asio::awaitable on_10_main_menu(std::shared_ptr c, uint32_t c->log.info_f("Brutal Peeps +{} selected from ship menu at level {}", tier, character_level); co_await send_auto_patches_if_needed(c); - co_await send_brutal_peeps_hp_patch_bb(c, tier); + if (c->version() == Version::BB_V4) { + 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 e3c7f77440258899a7b79e90a9e9763f2aff9456 Mon Sep 17 00:00:00 2001 From: James Osborne Date: Sun, 7 Jun 2026 03:32:00 -0400 Subject: [PATCH 05/11] Compact PC Brutal Peeps memory patch payload --- src/SendCommands.cc | 29 +++++++---------- .../client-functions/PsoPeepsBrutalPeepsPC.s | 32 +++++++++++++------ 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/SendCommands.cc b/src/SendCommands.cc index e87b2e18..7cc2c50a 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -1115,6 +1115,14 @@ static std::vector(scaled_f); }; + auto append_patch_entry = [&](std::string& out, uint32_t offset, uint32_t value, uint8_t size) { + append_u32l(out, offset); + out.push_back(static_cast(size)); + for (uint8_t x = 0; x < size; x++) { + out.push_back(static_cast((value >> (x * 8)) & 0xFF)); + } + }; + constexpr uint32_t last_needed_offset = ultimate_block_offset + ((num_bp_rows - 1) * stats_row_size) + ultimate_exp_row_offset + 4; if (vanilla_data.size() < last_needed_offset) { @@ -1140,12 +1148,7 @@ static std::vector(new_atp & 0xFF)); - patch_entry_count++; - - append_u32l(suffix, atp_patch_offset + 1); - suffix.push_back(static_cast((new_atp >> 8) & 0xFF)); + append_patch_entry(suffix, atp_patch_offset, new_atp, 2); patch_entry_count++; const uint32_t hp_file_offset = row_file_offset + ultimate_hp_row_offset; @@ -1153,12 +1156,7 @@ static std::vector(new_hp & 0xFF)); - patch_entry_count++; - - append_u32l(suffix, hp_patch_offset + 1); - suffix.push_back(static_cast((new_hp >> 8) & 0xFF)); + append_patch_entry(suffix, hp_patch_offset, new_hp, 2); patch_entry_count++; const uint32_t exp_file_offset = row_file_offset + ultimate_exp_row_offset; @@ -1166,11 +1164,8 @@ static std::vector((new_exp >> (x * 8)) & 0xFF)); - patch_entry_count++; - } + append_patch_entry(suffix, exp_patch_offset, new_exp, 4); + patch_entry_count++; } suffix[12] = static_cast(patch_entry_count & 0xFF); diff --git a/system/client-functions/PsoPeepsBrutalPeepsPC.s b/system/client-functions/PsoPeepsBrutalPeepsPC.s index be7e2012..62dbd5ea 100644 --- a/system/client-functions/PsoPeepsBrutalPeepsPC.s +++ b/system/client-functions/PsoPeepsBrutalPeepsPC.s @@ -48,7 +48,7 @@ next_candidate: jmp scan_again found_table: - # esi = BattleParamEntry_on.dat base + # esi = matched Ultimate BattleParam block base mov ecx, [ebx + 12] # patch entry count mov edi, [ebx + 8] # signature_size lea edi, [ebx + edi + 16] # patch entries after header+signature @@ -57,16 +57,27 @@ patch_again: test ecx, ecx jz done - mov edx, [edi] # offset from table base - mov al, [edi + 4] # byte value - mov [esi + edx], al + mov edx, [edi] # offset from matched block base + movzx ebp, byte [edi + 4] # byte count + add edi, 5 # edi = source bytes - add edi, 5 +copy_patch_bytes_again: + test ebp, ebp + jz patch_entry_done + + mov al, [edi] + mov [esi + edx], al + inc edi + inc edx + dec ebp + jmp copy_patch_bytes_again + +patch_entry_done: dec ecx jmp patch_again done: - mov eax, esi # return found table base + mov eax, esi # return found block base jmp return not_found: @@ -82,12 +93,13 @@ return: get_data_ptr: call get_data_ptr_ret -# Server suffix starts here: +# Server suffix: # uint32_t scan_start # uint32_t scan_end # uint32_t signature_size # uint32_t patch_entry_count -# signature bytes from table start -# repeated patch entries: +# signature bytes +# repeated compact patch entries: # uint32_t offset -# uint8_t value +# uint8_t size +# uint8_t data[size] From 17ddfe4945f76821f8e03b4df03d967a2a34dce5 Mon Sep 17 00:00:00 2001 From: James Osborne Date: Sun, 7 Jun 2026 03:37:45 -0400 Subject: [PATCH 06/11] Retry PC Brutal Peeps memory patch after load --- src/SendCommands.cc | 58 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 7cc2c50a..e3986915 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -1205,20 +1205,52 @@ asio::awaitable send_brutal_peeps_hp_patch_bb(std::shared_ptr c, i try { co_await prepare_client_for_patches(c); - auto promises = (c->version() == Version::PC_V2) - ? send_brutal_peeps_hp_patch_pc_now(c, tier) - : send_brutal_peeps_hp_patch_bb_now(c, tier); - for (auto& it : promises) { - const auto& filename = it.first; - auto& promise = it.second; - if (promise && c->channel->connected()) { - auto result = co_await promise->get(); - c->log.info_f("Brutal Peeps HP/ATP client patch result for {}: tier={} return_value={:08X} checksum={:08X}", - filename, - tier, - static_cast(result.return_value), - static_cast(result.checksum)); + const bool is_pc_bp_patch = (c->version() == Version::PC_V2); + const size_t max_attempts = is_pc_bp_patch ? 5 : 1; + + for (size_t attempt = 1; attempt <= max_attempts; attempt++) { + if (is_pc_bp_patch) { + asio::steady_timer timer(co_await asio::this_coro::executor); + timer.expires_after(std::chrono::milliseconds(750)); + co_await timer.async_wait(asio::use_awaitable); } + + auto promises = is_pc_bp_patch + ? send_brutal_peeps_hp_patch_pc_now(c, tier) + : send_brutal_peeps_hp_patch_bb_now(c, tier); + + bool any_zero_return = false; + bool any_success = false; + + for (auto& it : promises) { + const auto& filename = it.first; + auto& promise = it.second; + if (promise && c->channel->connected()) { + auto result = co_await promise->get(); + uint32_t return_value = static_cast(result.return_value); + c->log.info_f("Brutal Peeps HP/ATP client patch result for {}: tier={} attempt={}/{} return_value={:08X} checksum={:08X}", + filename, + tier, + attempt, + max_attempts, + return_value, + static_cast(result.checksum)); + + if (return_value) { + any_success = true; + } else { + any_zero_return = true; + } + } + } + + if (!is_pc_bp_patch || any_success || !any_zero_return || !c->channel->connected()) { + break; + } + + c->log.warning_f("Brutal Peeps PC client patch did not find BattleParam table on attempt {}/{}; retrying", + attempt, + max_attempts); } } catch (const std::exception& e) { From f0bc3639c964e6083b762747f2b05b71f7ce9de6 Mon Sep 17 00:00:00 2001 From: James Osborne Date: Sun, 7 Jun 2026 03:43:25 -0400 Subject: [PATCH 07/11] Force-send PC Brutal Peeps dynamic patch --- src/SendCommands.cc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/SendCommands.cc b/src/SendCommands.cc index e3986915..a37b13a0 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -1178,15 +1178,17 @@ static std::vector>(); c->function_call_response_queue.emplace_back(promise); + // This is a dynamic patch: the suffix changes by tier and by restore state. + // Force the code+suffix to be sent every time instead of treating it as a + // cached/enabled client function. send_function_call( c->channel, - c->enabled_flags, + c->enabled_flags & (~fn->client_flag), fn, {}, suffix.data(), suffix.size()); - c->enabled_flags |= fn->client_flag; promises.emplace_back(bp_filename, promise); c->log.info_f("Brutal Peeps PC ATP/HP/EXP client patch sent for {}: tier={} hp_mult={:g} atp_mult={:g} exp_mult={:g} patch_entries={} suffix_size={} scan={:08X}-{:08X}", From b80cf85f48972031e889d410ea7ad37cb3823f51 Mon Sep 17 00:00:00 2001 From: James Osborne Date: Sun, 7 Jun 2026 03:54:55 -0400 Subject: [PATCH 08/11] Patch PC Brutal Peeps after entering combat floor --- src/ReceiveSubcommands.cc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 01f9cd75..77b3cc16 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -3742,6 +3742,15 @@ static asio::awaitable on_trigger_set_event(std::shared_ptr c, Sub auto event_sts = l->map_state->event_states_for_id(c->version(), cmd.floor, cmd.event_id); l->log.info_f("Client triggered set events with floor {:02X} and ID {:X} ({} events)", cmd.floor, cmd.event_id, event_sts.size()); + + // PSO Peeps: PSO PC does not load the BattleParam table while still in + // Pioneer 2, so the Brutal Peeps ATP/HP/EXP memory patch must be delayed + // until the client reaches a combat floor. + if ((c->version() == Version::PC_V2) && + (cmd.floor >= 1) && + (l->brutal_peeps_tier >= 1)) { + co_await send_brutal_peeps_hp_patch_bb(c, l->brutal_peeps_tier); + } for (auto ev_st : event_sts) { ev_st->flags |= 0x04; if (c->check_flag(Client::Flag::DEBUG_ENABLED)) { From fc1fe53b63aa7f8f9071e995349da72309f00c4c Mon Sep 17 00:00:00 2001 From: James Osborne Date: Sun, 7 Jun 2026 04:00:35 -0400 Subject: [PATCH 09/11] Only patch PC Brutal Peeps after combat floor event --- src/ReceiveCommands.cc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index a16b7e8f..dcb3eef7 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -5596,8 +5596,7 @@ static asio::awaitable on_6F(std::shared_ptr c, Channel::Message& } } - if (loading_flag_cleared && - ((c->version() == Version::BB_V4) || (c->version() == Version::PC_V2))) { + 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); } From 892b12535cfd329a702b743a2bc9615262409c1a Mon Sep 17 00:00:00 2001 From: James Osborne Date: Sun, 7 Jun 2026 04:12:44 -0400 Subject: [PATCH 10/11] Keep retrying PC Brutal Peeps patch after room load --- src/ReceiveCommands.cc | 24 ++++++++++++++++++++++++ src/SendCommands.cc | 10 ++-------- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index dcb3eef7..2aec0ca0 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -5546,6 +5546,27 @@ static asio::awaitable on_8A(std::shared_ptr c, Channel::Message& co_return; } + +static asio::awaitable send_brutal_peeps_pc_patch_until_area_load(std::shared_ptr c) { + for (size_t attempt = 1; attempt <= 120; attempt++) { + asio::steady_timer timer(co_await asio::this_coro::executor); + timer.expires_after(std::chrono::milliseconds(500)); + co_await timer.async_wait(asio::use_awaitable); + + if (!c->channel->connected() || (c->version() != Version::PC_V2)) { + co_return; + } + + auto l = c->lobby.lock(); + if (!l || !l->is_game()) { + co_return; + } + + 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); + } +} + static asio::awaitable on_6F(std::shared_ptr c, Channel::Message& msg) { check_size_v(msg.data.size(), 0); @@ -5599,6 +5620,9 @@ 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); + } else if (loading_flag_cleared && (c->version() == Version::PC_V2)) { + auto s = c->require_server_state(); + asio::co_spawn(*s->io_context, send_brutal_peeps_pc_patch_until_area_load(c), asio::detached); } // DC NTE creates players in the invisible state by default; if the joiner is not DC NTE, it won't send 6x23 to make diff --git a/src/SendCommands.cc b/src/SendCommands.cc index a37b13a0..3c705d65 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -1208,15 +1208,9 @@ asio::awaitable send_brutal_peeps_hp_patch_bb(std::shared_ptr c, i co_await prepare_client_for_patches(c); const bool is_pc_bp_patch = (c->version() == Version::PC_V2); - const size_t max_attempts = is_pc_bp_patch ? 5 : 1; + const size_t max_attempts = 1; for (size_t attempt = 1; attempt <= max_attempts; attempt++) { - if (is_pc_bp_patch) { - asio::steady_timer timer(co_await asio::this_coro::executor); - timer.expires_after(std::chrono::milliseconds(750)); - co_await timer.async_wait(asio::use_awaitable); - } - auto promises = is_pc_bp_patch ? send_brutal_peeps_hp_patch_pc_now(c, tier) : send_brutal_peeps_hp_patch_bb_now(c, tier); @@ -1246,7 +1240,7 @@ asio::awaitable send_brutal_peeps_hp_patch_bb(std::shared_ptr c, i } } - if (!is_pc_bp_patch || any_success || !any_zero_return || !c->channel->connected()) { + if (!is_pc_bp_patch || any_success || !any_zero_return || !c->channel->connected() || (attempt >= max_attempts)) { break; } From 3c779b9e1f8929b82e71b0922016b98ee54b445f Mon Sep 17 00:00:00 2001 From: James Osborne Date: Sun, 7 Jun 2026 04:30:47 -0400 Subject: [PATCH 11/11] Track PC Brutal Peeps BattleParam patch tier --- src/Client.hh | 1 + src/ReceiveCommands.cc | 11 ++++ src/SendCommands.cc | 119 +++++++++++++++++++++++++++++------------ 3 files changed, 98 insertions(+), 33 deletions(-) diff --git a/src/Client.hh b/src/Client.hh index d05c605a..5bdf509d 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -153,6 +153,7 @@ public: uint8_t override_lobby_number = 0x80; // 80 = no override int64_t override_random_seed = -1; int8_t selected_brutal_peeps_tier = -1; // -1 = normal lobby/game; 1..11 = requested Brutal Peeps tier + int8_t brutal_peeps_pc_battleparam_patch_tier = -1; // -1 = vanilla; 1..11 = currently applied PC BattleParam BP tier std::unique_ptr override_variations; VectorXYZF pos; uint32_t floor = 0x0F; diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 2aec0ca0..e1115f3d 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -5563,7 +5563,15 @@ static asio::awaitable send_brutal_peeps_pc_patch_until_area_load(std::sha } int64_t brutal_peeps_hp_patch_tier = (l->brutal_peeps_tier >= 1) ? l->brutal_peeps_tier : -1; + if (c->brutal_peeps_pc_battleparam_patch_tier == brutal_peeps_hp_patch_tier) { + co_return; + } + co_await send_brutal_peeps_hp_patch_bb(c, brutal_peeps_hp_patch_tier); + + if (c->brutal_peeps_pc_battleparam_patch_tier == brutal_peeps_hp_patch_tier) { + co_return; + } } } @@ -5621,6 +5629,9 @@ static asio::awaitable on_6F(std::shared_ptr c, Channel::Message& 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); } else if (loading_flag_cleared && (c->version() == Version::PC_V2)) { + // PC unloads/reloads the area BattleParam table between room loads; when it + // appears again, assume it starts from the file's vanilla values. + c->brutal_peeps_pc_battleparam_patch_tier = -1; auto s = c->require_server_state(); asio::co_spawn(*s->io_context, send_brutal_peeps_pc_patch_until_area_load(c), asio::detached); } diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 3c705d65..1522112b 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -1010,16 +1010,33 @@ static std::vectorrequire_server_state(); - const auto* brutal_peeps_def = brutal_peeps_tier_definition(tier); - if ((tier >= 0) && !brutal_peeps_def) { + + if (tier < -1) { c->log.warning_f("Skipping Brutal Peeps PC client patch for invalid tier {}", tier); return promises; } - const double hp_mult = brutal_peeps_def ? brutal_peeps_def->enemy_hp_multiplier : 1.0; - const double exp_mult = brutal_peeps_def ? brutal_peeps_def->exp_multiplier : 1.0; - const double atp_mult = [&]() -> double { - switch (tier) { + const auto* target_brutal_peeps_def = brutal_peeps_tier_definition(tier); + if ((tier >= 0) && !target_brutal_peeps_def) { + c->log.warning_f("Skipping Brutal Peeps PC client patch for invalid tier {}", tier); + return promises; + } + + int64_t source_tier = c->brutal_peeps_pc_battleparam_patch_tier; + const auto* source_brutal_peeps_def = brutal_peeps_tier_definition(source_tier); + if ((source_tier >= 0) && !source_brutal_peeps_def) { + c->log.warning_f("PC BattleParam patch source tier {} is invalid; resetting assumed source tier to vanilla", source_tier); + source_tier = -1; + source_brutal_peeps_def = nullptr; + c->brutal_peeps_pc_battleparam_patch_tier = -1; + } + + if (source_tier == tier) { + return promises; + } + + auto atp_mult_for_tier = +[](int64_t t) -> double { + switch (t) { case 1: return 1.01; case 2: @@ -1044,7 +1061,7 @@ static std::vector(v & 0xFF)); @@ -1080,6 +1098,11 @@ static std::vector(static_cast(data[offset + 1])) << 8); }; + auto write_u16l = +[](std::string& data, uint32_t offset, uint16_t v) { + data[offset] = static_cast(v & 0xFF); + data[offset + 1] = static_cast((v >> 8) & 0xFF); + }; + auto read_u32l = +[](const std::string& data, uint32_t offset) -> uint32_t { return static_cast(data[offset]) | (static_cast(static_cast(data[offset + 1])) << 8) | @@ -1087,6 +1110,13 @@ static std::vector(static_cast(data[offset + 3])) << 24); }; + auto write_u32l = +[](std::string& data, uint32_t offset, uint32_t v) { + data[offset] = static_cast(v & 0xFF); + data[offset + 1] = static_cast((v >> 8) & 0xFF); + data[offset + 2] = static_cast((v >> 16) & 0xFF); + data[offset + 3] = static_cast((v >> 24) & 0xFF); + }; + auto scale_u16 = +[](uint32_t v, double scale) -> uint16_t { if (v == 0) { return 0; @@ -1123,48 +1153,61 @@ static std::vectorlog.warning_f("Skipping Brutal Peeps PC client patch: {} too small for Ultimate stats table", bp_filename); return promises; } + auto build_block_for_tier = [&](int64_t block_tier) -> std::string { + std::string block = vanilla_data.substr(ultimate_block_offset, ultimate_block_size); + + const auto* def = brutal_peeps_tier_definition(block_tier); + const double hp_mult = def ? def->enemy_hp_multiplier : 1.0; + const double exp_mult = def ? def->exp_multiplier : 1.0; + const double atp_mult = atp_mult_for_tier(block_tier); + + for (uint32_t z = 0; z < num_bp_rows; z++) { + const uint32_t row_offset = z * stats_row_size; + + const uint32_t atp_offset = row_offset + ultimate_atp_row_offset; + const uint32_t hp_offset = row_offset + ultimate_hp_row_offset; + const uint32_t exp_offset = row_offset + ultimate_exp_row_offset; + + write_u16l(block, atp_offset, scale_u16(read_u16l(block, atp_offset), atp_mult)); + write_u16l(block, hp_offset, scale_u16(read_u16l(block, hp_offset), hp_mult)); + write_u32l(block, exp_offset, scale_u32(read_u32l(block, exp_offset), exp_mult)); + } + + return block; + }; + + const std::string source_block = build_block_for_tier(source_tier); + const std::string target_block = build_block_for_tier(tier); + 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 patch generation - suffix.append(vanilla_data.data() + ultimate_block_offset, signature_size); + suffix.append(source_block.data(), signature_size); uint32_t patch_entry_count = 0; for (uint32_t z = 0; z < num_bp_rows; z++) { - const uint32_t row_file_offset = ultimate_block_offset + (z * stats_row_size); const uint32_t row_patch_offset = z * stats_row_size; - const uint32_t atp_file_offset = row_file_offset + ultimate_atp_row_offset; const uint32_t atp_patch_offset = row_patch_offset + ultimate_atp_row_offset; - uint16_t old_atp = read_u16l(vanilla_data, atp_file_offset); - uint16_t new_atp = scale_u16(old_atp, atp_mult); - - append_patch_entry(suffix, atp_patch_offset, new_atp, 2); - patch_entry_count++; - - const uint32_t hp_file_offset = row_file_offset + ultimate_hp_row_offset; const uint32_t hp_patch_offset = row_patch_offset + ultimate_hp_row_offset; - uint16_t old_hp = read_u16l(vanilla_data, hp_file_offset); - uint16_t new_hp = scale_u16(old_hp, hp_mult); + const uint32_t exp_patch_offset = row_patch_offset + ultimate_exp_row_offset; - append_patch_entry(suffix, hp_patch_offset, new_hp, 2); + append_patch_entry(suffix, atp_patch_offset, read_u16l(target_block, atp_patch_offset), 2); patch_entry_count++; - const uint32_t exp_file_offset = row_file_offset + ultimate_exp_row_offset; - const uint32_t exp_patch_offset = row_patch_offset + ultimate_exp_row_offset; - uint32_t old_exp = read_u32l(vanilla_data, exp_file_offset); - uint32_t new_exp = scale_u32(old_exp, exp_mult); + append_patch_entry(suffix, hp_patch_offset, read_u16l(target_block, hp_patch_offset), 2); + patch_entry_count++; - append_patch_entry(suffix, exp_patch_offset, new_exp, 4); + append_patch_entry(suffix, exp_patch_offset, read_u32l(target_block, exp_patch_offset), 4); patch_entry_count++; } @@ -1191,8 +1234,13 @@ static std::vectorlog.info_f("Brutal Peeps PC ATP/HP/EXP client patch sent for {}: tier={} hp_mult={:g} atp_mult={:g} exp_mult={:g} patch_entries={} suffix_size={} scan={:08X}-{:08X}", - bp_filename, tier, hp_mult, atp_mult, exp_mult, patch_entry_count, suffix.size(), scan_start, scan_end); + const auto* target_def_for_log = brutal_peeps_tier_definition(tier); + const double hp_mult_for_log = target_def_for_log ? target_def_for_log->enemy_hp_multiplier : 1.0; + const double exp_mult_for_log = target_def_for_log ? target_def_for_log->exp_multiplier : 1.0; + const double atp_mult_for_log = atp_mult_for_tier(tier); + + c->log.info_f("Brutal Peeps PC ATP/HP/EXP client patch sent for {}: source_tier={} target_tier={} hp_mult={:g} atp_mult={:g} exp_mult={:g} patch_entries={} suffix_size={} scan={:08X}-{:08X}", + bp_filename, source_tier, tier, hp_mult_for_log, atp_mult_for_log, exp_mult_for_log, patch_entry_count, suffix.size(), scan_start, scan_end); return promises; @@ -1232,6 +1280,11 @@ asio::awaitable send_brutal_peeps_hp_patch_bb(std::shared_ptr c, i return_value, static_cast(result.checksum)); + if (is_pc_bp_patch && return_value) { + c->brutal_peeps_pc_battleparam_patch_tier = static_cast(tier); + c->log.info_f("Brutal Peeps PC BattleParam patch state is now tier {}", tier); + } + if (return_value) { any_success = true; } else {