diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index ce68c5a0..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()) { @@ -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())) { @@ -5570,9 +5572,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. @@ -5582,6 +5586,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 a45443ee..f30dd957 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -775,6 +775,166 @@ static std::string bb_stream_file_data_for_client(std::shared_ptr c) { } +static std::shared_ptr> send_brutal_peeps_hp_patch_bb_now( + std::shared_ptr c, + int64_t tier) { + if (c->version() != Version::BB_V4) { + 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"); + return nullptr; + } + if (!c->channel->connected()) { + c->log.warning_f("Skipping Brutal Peeps HP client patch because client is disconnected"); + return nullptr; + } + + 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 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; + + 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) { + 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); + }; + + 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 HP patch generation + suffix.append(vanilla_data, signature_size); + + uint32_t patch_entry_count = 0; + 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, 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++; + } + + 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>(); + 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; + + 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; + + } 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()) { + 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) { + 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(); 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..13d3b1aa --- /dev/null +++ b/system/client-functions/PsoPeepsBrutalPeepsHPBB.s @@ -0,0 +1,93 @@ +.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 # 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