brutal peeps hp scaling

Feature/brutal peeps hp scaling
This commit is contained in:
James Osborne
2026-06-06 22:54:24 -04:00
committed by GitHub
4 changed files with 263 additions and 0 deletions
+9
View File
@@ -2762,6 +2762,7 @@ static asio::awaitable<void> on_10_main_menu(std::shared_ptr<Client> 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<void> on_10_main_menu(std::shared_ptr<Client> 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<void> on_6F(std::shared_ptr<Client> 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<void> on_6F(std::shared_ptr<Client> 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) {
+160
View File
@@ -775,6 +775,166 @@ static std::string bb_stream_file_data_for_client(std::shared_ptr<Client> c) {
}
static std::shared_ptr<AsyncPromise<C_ExecuteCodeResult_B3>> send_brutal_peeps_hp_patch_bb_now(
std::shared_ptr<Client> 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<char>(v & 0xFF));
out.push_back(static_cast<char>((v >> 8) & 0xFF));
out.push_back(static_cast<char>((v >> 16) & 0xFF));
out.push_back(static_cast<char>((v >> 24) & 0xFF));
};
auto scale_u16 = [mult](uint32_t v) -> uint16_t {
if (v == 0) {
return 0;
}
uint32_t scaled = static_cast<uint32_t>((static_cast<double>(v) * mult) + 0.5);
if (scaled < 1) {
scaled = 1;
}
if (scaled > 0xFFFF) {
scaled = 0xFFFF;
}
return static_cast<uint16_t>(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<uint8_t>(vanilla_data[hp_offset]) |
(static_cast<uint16_t>(static_cast<uint8_t>(vanilla_data[hp_offset + 1])) << 8);
uint16_t new_hp = scale_u16(old_hp);
append_u32l(suffix, hp_offset);
suffix.push_back(static_cast<char>(new_hp & 0xFF));
patch_entry_count++;
append_u32l(suffix, hp_offset + 1);
suffix.push_back(static_cast<char>((new_hp >> 8) & 0xFF));
patch_entry_count++;
}
suffix[12] = static_cast<char>(patch_entry_count & 0xFF);
suffix[13] = static_cast<char>((patch_entry_count >> 8) & 0xFF);
suffix[14] = static_cast<char>((patch_entry_count >> 16) & 0xFF);
suffix[15] = static_cast<char>((patch_entry_count >> 24) & 0xFF);
auto fn = s->client_functions->get("PsoPeepsBrutalPeepsHP", c->specific_version);
auto promise = std::make_shared<AsyncPromise<C_ExecuteCodeResult_B3>>();
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<void> send_brutal_peeps_hp_patch_bb(std::shared_ptr<Client> 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<uint32_t>(result.return_value),
static_cast<uint32_t>(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<Client> c) {
auto s = c->require_server_state();
+1
View File
@@ -198,6 +198,7 @@ void send_guild_card_header_bb(std::shared_ptr<Client> c);
void send_guild_card_chunk_bb(std::shared_ptr<Client> c, size_t chunk_index);
void send_stream_file_index_bb(std::shared_ptr<Client> c);
void send_stream_file_chunk_bb(std::shared_ptr<Client> c, uint32_t chunk_index);
asio::awaitable<void> send_brutal_peeps_hp_patch_bb(std::shared_ptr<Client> c, int64_t tier);
void send_approve_player_choice_bb(std::shared_ptr<Client> c);
void send_complete_player_bb(std::shared_ptr<Client> c);
@@ -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