rewrite BB EXP generation to handle non-player kills properly

This commit is contained in:
Martin Michelsen
2026-05-29 07:43:05 -07:00
parent c09ee2da85
commit e4054d95d9
2 changed files with 99 additions and 67 deletions
+23 -4
View File
@@ -766,10 +766,12 @@ public:
std::shared_ptr<const SuperMap::Enemy> super_ene; std::shared_ptr<const SuperMap::Enemy> super_ene;
enum Flag { enum Flag {
LAST_HIT_MASK = 0x0003, LAST_HIT_MASK = 0x0003,
EXP_GIVEN = 0x0004, ITEM_DROPPED = 0x0004,
ITEM_DROPPED = 0x0008, SHARED_EXP_GIVEN = 0x0008,
ALL_HITS_MASK_FIRST = 0x0010, FULL_EXP_GIVEN_MASK_FIRST = 0x0010,
ALL_HITS_MASK = 0x00F0, FULL_EXP_GIVEN_MASK = 0x00F0,
ALL_HITS_MASK_FIRST = 0x0100,
ALL_HITS_MASK = 0x0F00,
}; };
size_t e_id = 0; size_t e_id = 0;
size_t set_id = 0; size_t set_id = 0;
@@ -817,6 +819,23 @@ public:
return this->super_ene->type; return this->super_ene->type;
} }
} }
inline bool should_give_shared_exp() {
if (this->server_flags & Flag::SHARED_EXP_GIVEN) {
return false;
} else {
this->server_flags |= Flag::SHARED_EXP_GIVEN;
return true;
}
}
inline bool should_give_full_exp_for_client_id(uint8_t client_id) {
uint16_t flag = (Flag::FULL_EXP_GIVEN_MASK_FIRST << client_id);
if (this->server_flags & flag) {
return false;
} else {
this->server_flags |= flag;
return true;
}
}
inline bool ever_hit_by_client_id(uint8_t client_id) const { inline bool ever_hit_by_client_id(uint8_t client_id) const {
return this->server_flags & (Flag::ALL_HITS_MASK_FIRST << client_id); return this->server_flags & (Flag::ALL_HITS_MASK_FIRST << client_id);
} }
+76 -63
View File
@@ -4132,25 +4132,62 @@ static asio::awaitable<void> on_enemy_exp_request_bb(std::shared_ptr<Client> c,
ene_st = ene_st->alias_target_ene_st; ene_st = ene_st->alias_target_ene_st;
} }
// If the requesting player never hit this enemy, they are probably cheating; ignore the command. Also, each player bool should_give_shared_exp = ene_st->should_give_shared_exp();
// sends a 6xC8 if they ever hit the enemy; we only react to the first 6xC8 for each enemy (and give all relevant if (!ene_st->should_give_full_exp_for_client_id(c->lobby_client_id)) {
// players EXP then, if they deserve it). if (should_give_shared_exp) {
if (!ene_st->ever_hit_by_client_id(c->lobby_client_id)) { // This should be impossible because shared EXP should be given immediately upon the first 6xC8 command
l->log.warning_f("The requesting player did not hit this enemy; ignoring request"); throw std::logic_error("Full EXP was already given but shared EXP was not");
co_return; } else {
c->log.info_f("All relevant EXP has already been given for this player/enemy pair");
co_return;
}
} }
if (ene_st->server_flags & MapState::EnemyState::Flag::EXP_GIVEN) {
l->log.info_f("EXP already given for this enemy; ignoring request"); // Update kill counts on unsealable items, but only for the player who actually killed the enemy
co_return; if (ene_st->last_hit_by_client_id(c->lobby_client_id)) {
auto& inventory = c->character_file()->inventory;
for (size_t z = 0; z < inventory.num_items; z++) {
auto& item = inventory.items[z];
if ((item.flags & 0x08) && s->item_parameter_table(c->version())->is_unsealable_item(item.data)) {
size_t new_kill_count = item.data.get_kill_count() + 1;
item.data.set_kill_count(new_kill_count);
c->log.info_f("Item {:08X} kill count updated to {}", item.data.id, new_kill_count);
}
}
} }
ene_st->server_flags |= MapState::EnemyState::Flag::EXP_GIVEN;
uint8_t area = l->area_for_floor(c->version(), ene_st->super_ene->floor); uint8_t area = l->area_for_floor(c->version(), ene_st->super_ene->floor);
Episode episode = episode_for_area(area); Episode episode = episode_for_area(area);
auto type = ene_st->type(c->version(), area, l->difficulty, l->event); auto type = ene_st->type(c->version(), area, l->difficulty, l->event);
double base_exp = base_exp_for_enemy_type( double base_exp = base_exp_for_enemy_type(
s->battle_params, l->quest, type, episode, l->difficulty, ene_st->super_ene->floor, l->mode == GameMode::SOLO); s->battle_params, l->quest, type, episode, l->difficulty, ene_st->super_ene->floor, l->mode == GameMode::SOLO);
l->log.info_f("Base EXP for this enemy ({}) is {:g}", phosg::name_for_enum(type), base_exp);
// If this player killed the enemy, they get full EXP; if they tagged the enemy, they get 80% EXP; if auto EXP share
// is enabled and they are close enough to the monster, they get a smaller share; if none of these situations apply,
// they get no EXP. In Battle and Challenge modes, if a quest is loaded, EXP share is disabled.
bool is_battle = (l->mode == GameMode::BATTLE);
bool is_challenge = (l->mode == GameMode::CHALLENGE);
double share_mult = ((is_battle || is_challenge) && l->quest) ? 0.0f : l->exp_share_multiplier;
// In PSOBB, Sega decided to add a 30% EXP boost for Episode 2. They could have done something reasonable, like
// edit the BattleParamEntry files so the monsters would all give more EXP, but they did something far lazier
// instead: they just stuck an if statement in the client's EXP request function. We, unfortunately, have to do
// the same thing here.
double lobby_mult = is_challenge ? l->challenge_exp_multiplier : l->base_exp_multiplier;
double episode_mult = (episode == Episode::EP2) ? 1.3 : 1.0;
int32_t full_exp = std::max<int32_t>(0, base_exp * std::max<double>(1.0, share_mult) * lobby_mult * episode_mult);
int32_t tag_exp = std::max<int32_t>(0, base_exp * std::max<double>(0.8, share_mult) * lobby_mult * episode_mult);
int32_t shared_exp = std::max<int32_t>(0, base_exp * std::max<double>(0.0, share_mult) * lobby_mult * episode_mult);
l->log.info_f("Base EXP for this enemy ({}) is {:g} (share_mult={:g}, lobby_mult={:g}, episode_mult={:g}); full EXP is {}, tag EXP is {}, shared EXP is {}",
phosg::name_for_enum(type), base_exp, share_mult, lobby_mult, episode_mult, full_exp, tag_exp, shared_exp);
if (!should_give_shared_exp) {
full_exp = std::max<int32_t>(0, full_exp - shared_exp);
tag_exp = std::max<int32_t>(0, tag_exp - shared_exp);
shared_exp = 0;
l->log.info_f("Shared EXP has already been given; effective full EXP is {}, tag EXP is {}, shared EXP is {}",
full_exp, tag_exp, shared_exp);
}
for (size_t client_id = 0; client_id < 4; client_id++) { for (size_t client_id = 0; client_id < 4; client_id++) {
auto lc = l->clients[client_id]; auto lc = l->clients[client_id];
@@ -4164,61 +4201,37 @@ static asio::awaitable<void> on_enemy_exp_request_bb(std::shared_ptr<Client> c,
continue; continue;
} }
if (base_exp != 0.0) { int32_t exp_to_give = 0;
// If this player killed the enemy, they get full EXP; if they tagged the enemy, they get 80% EXP; if auto EXP bool last_hit = ene_st->last_hit_by_client_id(client_id);
// share is enabled and they are close enough to the monster, they get a smaller share; if none of these bool ever_hit = ene_st->ever_hit_by_client_id(client_id);
// situations apply, they get no EXP. In Battle and Challenge modes, if a quest is loaded, EXP share is disabled. if (lc->character_file()->disp.stats.level >= 199) {
float exp_share_multiplier = (((l->mode == GameMode::BATTLE) || (l->mode == GameMode::CHALLENGE)) && l->quest) l->log.info_f("Client in slot {} is level 200 and cannot receive EXP", client_id);
? 0.0f } else if ((lc == c) && (last_hit && cmd.is_killer)) {
: l->exp_share_multiplier; exp_to_give = full_exp;
double rate_factor; l->log.info_f("Client in slot {} killed this enemy; effective EXP is {}", client_id, exp_to_give);
if (lc->character_file()->disp.stats.level >= 199) { } else if ((lc == c) && (last_hit && !cmd.is_killer)) {
rate_factor = 0.0; // In certain cases we may think that a client deserves full EXP but they claim not to. This can happen if a
l->log.info_f("Client in slot {} is level 200 and cannot receive EXP", client_id); // player tags an enemy, but that enemy is then killed by another enemy (e.g. a Nano Dragon). So, we trust the
} else if (ene_st->last_hit_by_client_id(client_id)) { // client's is_killer flag, but only if it's false.
rate_factor = std::max<double>(1.0, exp_share_multiplier); exp_to_give = tag_exp;
l->log.info_f("Client in slot {} killed this enemy; EXP rate is {:g}", client_id, rate_factor); l->log.info_f("Client in slot {} last hit this enemy but did not kill it; effective EXP is {}",
} else if (ene_st->ever_hit_by_client_id(client_id)) { client_id, exp_to_give);
rate_factor = std::max<double>(0.8, exp_share_multiplier); } else if ((lc == c) && ever_hit) {
l->log.info_f("Client in slot {} tagged this enemy; EXP rate is {:g}", client_id, rate_factor); exp_to_give = tag_exp;
} else if (lc->floor == ene_st->super_ene->floor) { l->log.info_f("Client in slot {} tagged this enemy; effective EXP is {}", client_id, exp_to_give);
rate_factor = std::max<double>(0.0, exp_share_multiplier); } else if (lc->floor == ene_st->super_ene->floor) {
l->log.info_f("Client in slot {} shared this enemy; EXP rate is {:g}", client_id, rate_factor); exp_to_give = shared_exp;
} else { l->log.info_f("Client in slot {} shared this enemy or did not request this EXP; effective EXP is {}",
rate_factor = 0.0; client_id, exp_to_give);
l->log.info_f("Client in slot {} is not near this enemy; EXP rate is {:g}", client_id, rate_factor); } else {
} l->log.info_f("Client in slot {} is not near this enemy; effective EXP is {}", client_id, exp_to_give);
if (rate_factor > 0.0) {
// In PSOBB, Sega decided to add a 30% EXP boost for Episode 2. They could have done something reasonable, like
// edit the BattleParamEntry files so the monsters would all give more EXP, but they did something far lazier
// instead: they just stuck an if statement in the client's EXP request function. We, unfortunately, have to do
// the same thing here.
float episode_multiplier = ((episode == Episode::EP2) ? 1.3 : 1.0);
uint32_t player_exp = base_exp *
rate_factor *
l->base_exp_multiplier *
l->challenge_exp_multiplier *
episode_multiplier;
l->log.info_f(
"Client in slot {} receives {} EXP (base={:g}, factor={:g} base_mult={:g}, challenge={:g}, episode={:g})",
client_id, player_exp, base_exp, rate_factor, l->base_exp_multiplier, l->challenge_exp_multiplier, episode_multiplier);
if (lc->check_flag(Client::Flag::DEBUG_ENABLED)) {
send_text_message_fmt(lc, "$C5+{} E-{:03X} {}", player_exp, ene_st->e_id, phosg::name_for_enum(type));
}
add_player_exp(lc, player_exp, cmd.enemy_index | 0x1000);
}
} }
// Update kill counts on unsealable items, but only for the player who actually killed the enemy if (exp_to_give > 0) {
if (ene_st->last_hit_by_client_id(client_id)) { if (lc->check_flag(Client::Flag::DEBUG_ENABLED)) {
auto& inventory = lc->character_file()->inventory; send_text_message_fmt(lc, "$C5+{} E-{:03X} {}", exp_to_give, ene_st->e_id, phosg::name_for_enum(type));
for (size_t z = 0; z < inventory.num_items; z++) {
auto& item = inventory.items[z];
if ((item.flags & 0x08) && s->item_parameter_table(lc->version())->is_unsealable_item(item.data)) {
item.data.set_kill_count(item.data.get_kill_count() + 1);
}
} }
add_player_exp(lc, exp_to_give, cmd.enemy_index | 0x1000);
} }
} }
} }