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