From 598120c66135deaa55e553a10a8cf016b6ff0541 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sat, 15 Jun 2024 16:45:09 -0700 Subject: [PATCH] implement BB EXP share --- src/HTTPServer.cc | 1 + src/Lobby.cc | 1 + src/Lobby.hh | 1 + src/Map.cc | 6 +-- src/Map.hh | 22 ++++++--- src/ReceiveCommands.cc | 5 +- src/ReceiveSubcommands.cc | 98 ++++++++++++++++++++++---------------- src/SendCommands.cc | 2 +- src/ServerState.cc | 1 + src/ServerState.hh | 1 + system/config.example.json | 14 ++++++ tests/config.json | 1 + 12 files changed, 99 insertions(+), 54 deletions(-) diff --git a/src/HTTPServer.cc b/src/HTTPServer.cc index ff7dcb3d..f5bfa80c 100644 --- a/src/HTTPServer.cc +++ b/src/HTTPServer.cc @@ -579,6 +579,7 @@ JSON HTTPServer::generate_lobby_json_st(shared_ptr l, shared_ptrmode)); ret.emplace("Difficulty", name_for_difficulty(l->difficulty)); ret.emplace("BaseEXPMultiplier", l->base_exp_multiplier); + ret.emplace("EXPShareMultiplier", l->exp_share_multiplier); ret.emplace("AllowedDropModes", l->allowed_drop_modes); switch (l->drop_mode) { case Lobby::DropMode::DISABLED: diff --git a/src/Lobby.cc b/src/Lobby.cc index f04afce9..1d0c053b 100644 --- a/src/Lobby.cc +++ b/src/Lobby.cc @@ -149,6 +149,7 @@ Lobby::Lobby(shared_ptr s, uint32_t id, bool is_game) mode(GameMode::NORMAL), difficulty(0), base_exp_multiplier(1), + exp_share_multiplier(0.5), challenge_exp_multiplier(1.0f), random_seed(random_object()), drop_mode(DropMode::CLIENT), diff --git a/src/Lobby.hh b/src/Lobby.hh index 3c7cc5fd..c7f3dc02 100644 --- a/src/Lobby.hh +++ b/src/Lobby.hh @@ -123,6 +123,7 @@ struct Lobby : public std::enable_shared_from_this { GameMode mode; uint8_t difficulty; // 0-3 uint16_t base_exp_multiplier; + float exp_share_multiplier; float challenge_exp_multiplier; std::string password; std::string name; diff --git a/src/Map.cc b/src/Map.cc index 190b478c..92e5eccc 100644 --- a/src/Map.cc +++ b/src/Map.cc @@ -678,10 +678,10 @@ Map::Enemy::Enemy( wave_number(wave_number), type(type), floor(floor), - state_flags(0) {} + server_flags(0) {} string Map::Enemy::str() const { - return string_printf("[Map::Enemy E-%hX source %zX %s%s floor=%02hhX section=%04hX wave_number=%04hX flags=%02hhX]", + return string_printf("[Map::Enemy E-%hX source %zX %s%s floor=%02hhX section=%04hX wave_number=%04hX server_flags=%02hhX]", this->enemy_id, this->source_index, name_for_enum(this->type), @@ -689,7 +689,7 @@ string Map::Enemy::str() const { this->floor, this->section, this->wave_number, - this->state_flags); + this->server_flags); } string Map::Event::str() const { diff --git a/src/Map.hh b/src/Map.hh index 121d7735..1a496d14 100644 --- a/src/Map.hh +++ b/src/Map.hh @@ -233,11 +233,11 @@ struct Map { struct Enemy { enum Flag { - EXP_REQUESTED_BY_PLAYER0 = 0x01, - EXP_REQUESTED_BY_PLAYER1 = 0x02, - EXP_REQUESTED_BY_PLAYER2 = 0x04, - EXP_REQUESTED_BY_PLAYER3 = 0x08, - ITEM_DROPPED = 0x10, + LAST_HIT_MASK = 0x03, + EXP_GIVEN = 0x04, + ITEM_DROPPED = 0x08, + ALL_HITS_MASK_FIRST = 0x10, + ALL_HITS_MASK = 0xF0, }; size_t source_index; size_t set_index; @@ -248,7 +248,7 @@ struct Map { uint16_t wave_number; EnemyType type; uint8_t floor; - uint8_t state_flags; + uint8_t server_flags; Enemy( uint16_t enemy_id, @@ -260,6 +260,16 @@ struct Map { EnemyType type); std::string str() const; + + inline bool ever_hit_by_client_id(uint8_t client_id) const { + return this->server_flags & (Flag::ALL_HITS_MASK_FIRST << client_id); + } + inline bool last_hit_by_client_id(uint8_t client_id) const { + return (this->server_flags & Flag::LAST_HIT_MASK) == client_id; + } + inline void set_last_hit_by_client_id(uint8_t client_id) { + this->server_flags = (this->server_flags & (~Flag::LAST_HIT_MASK)) | (Flag::ALL_HITS_MASK_FIRST << client_id) | (client_id & 3); + } }; struct Event { diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 24e5aace..0d48ee66 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -4257,9 +4257,8 @@ shared_ptr create_game_generic( game->battle_player = battle_player; battle_player->set_lobby(game); } - if (game->base_version == Version::BB_V4) { - game->base_exp_multiplier = s->bb_global_exp_multiplier; - } + game->base_exp_multiplier = s->bb_global_exp_multiplier; + game->exp_share_multiplier = s->exp_share_multiplier; const unordered_map* quest_flag_rewrites; switch (game->base_version) { diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index a020b61c..269f4924 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -2625,11 +2625,11 @@ DropReconcileResult reconcile_drop_request_with_map( } } if (map_enemy) { - if (map_enemy->state_flags & Map::Enemy::Flag::ITEM_DROPPED) { + if (map_enemy->server_flags & Map::Enemy::Flag::ITEM_DROPPED) { log.info("Drop check has already occurred for E-%04hX; skipping it", map_enemy->enemy_id); res.should_drop = false; } else { - map_enemy->state_flags |= Map::Enemy::Flag::ITEM_DROPPED; + map_enemy->server_flags |= Map::Enemy::Flag::ITEM_DROPPED; } } } @@ -3190,6 +3190,7 @@ static void on_update_enemy_state(shared_ptr c, uint8_t command, uint8_t auto& enemy = l->map->enemies[cmd.enemy_index]; enemy.game_flags = is_big_endian(c->version()) ? bswap32(cmd.flags) : cmd.flags.load(); enemy.total_damage = cmd.total_damage; + enemy.set_last_hit_by_client_id(c->lobby_client_id); l->log.info("E-%hX updated to damage=%hu game_flags=%08" PRIX32, cmd.enemy_index.load(), enemy.total_damage, enemy.game_flags); } @@ -3397,10 +3398,6 @@ static void on_enemy_exp_request_bb(shared_ptr c, uint8_t, uint8_t, void auto s = c->require_server_state(); auto l = c->require_lobby(); - if (l->base_version != Version::BB_V4) { - throw runtime_error("BB-only command sent in non-BB game"); - } - const auto& cmd = check_size_t(data, size); if (!l->is_game()) { @@ -3421,66 +3418,85 @@ static void on_enemy_exp_request_bb(shared_ptr c, uint8_t, uint8_t, void string e_str = e.str(); c->log.info("EXP requested for E-%hX => %s", cmd.enemy_index.load(), e_str.c_str()); - uint8_t state_flag = Map::Enemy::Flag::EXP_REQUESTED_BY_PLAYER0 << c->lobby_client_id; - if (e.state_flags & state_flag) { + // If the requesting player never hit this enemy, they are probably cheating; + // ignore the command. Also, each player sends a 6xC8 if they ever hit the + // enemy; we only react to the first 6xC8 for each enemy (and give all + // relevant players EXP then, if they deserve it). + if (!e.ever_hit_by_client_id(c->lobby_client_id) || (e.server_flags & Map::Enemy::Flag::EXP_GIVEN)) { + return; + } + e.server_flags |= Map::Enemy::Flag::EXP_GIVEN; + + double base_exp = 0.0; + try { + const auto& bp_table = s->battle_params->get_table(l->mode == GameMode::SOLO, l->episode); + uint32_t bp_index = battle_param_index_for_enemy_type(l->episode, e.type); + base_exp = bp_table.stats[l->difficulty][bp_index].experience; + } catch (const exception& e) { if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { - send_text_message_printf(c, "$C5E-%hX __CHECKED__", cmd.enemy_index.load()); + send_text_message_printf(c, "$C5E-%hX __MISSING__\n%s", cmd.enemy_index.load(), e.what()); + } else { + send_text_message_printf(c, "$C4Unknown enemy killed:\n%s", e.what()); + } + } + + for (size_t client_id = 0; client_id < 4; client_id++) { + auto lc = l->clients[client_id]; + if (!lc) { + continue; } - } else { - e.state_flags |= state_flag; - - double experience = 0.0; - try { - const auto& bp_table = s->battle_params->get_table(l->mode == GameMode::SOLO, l->episode); - uint32_t bp_index = battle_param_index_for_enemy_type(l->episode, e.type); - experience = bp_table.stats[l->difficulty][bp_index].experience; - } catch (const exception& e) { - if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { - send_text_message_printf(c, "$C5E-%hX __MISSING__\n%s", cmd.enemy_index.load(), e.what()); + if (base_exp != 0.0) { + // 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. + double rate_factor; + if (e.last_hit_by_client_id(client_id)) { + rate_factor = max(1.0, l->exp_share_multiplier); + } else if (e.ever_hit_by_client_id(client_id)) { + rate_factor = max(0.8, l->exp_share_multiplier); + } else if (lc->floor == e.floor) { + rate_factor = l->exp_share_multiplier; } else { - send_text_message_printf(c, "$C4Unknown enemy killed:\n%s", e.what()); + rate_factor = 0.0; } - } - if (experience != 0.0) { - if (c->floor != e.floor) { - if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { - send_text_message_printf(c, "$C5E-%hX %s\n$C4FLOOR Y:%02" PRIX32 " E:%02hhX", - cmd.enemy_index.load(), name_for_enum(e.type), c->floor, e.floor); - } - } else { + 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 here. bool is_ep2 = (l->episode == Episode::EP2); - uint32_t player_exp = experience * - (cmd.is_killer ? 1.0 : 0.8) * + uint32_t player_exp = base_exp * + rate_factor * l->base_exp_multiplier * l->challenge_exp_multiplier * (is_ep2 ? 1.3 : 1.0); - if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { + if (lc->config.check_flag(Client::Flag::DEBUG_ENABLED)) { send_text_message_printf( - c, "$C5+%" PRIu32 " E-%hX %s", + lc, "$C5+%" PRIu32 " E-%hX %s", player_exp, cmd.enemy_index.load(), name_for_enum(e.type)); } - if (c->character()->disp.stats.level < 199) { - add_player_exp(c, player_exp); + if (lc->character()->disp.stats.level < 199) { + add_player_exp(lc, player_exp); } } } // Update kill counts on unsealable items - auto& inventory = c->character()->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)) { - item.data.set_kill_count(item.data.get_kill_count() + 1); + // TODO: Do tags count too, even if the player didn't actually strike the + // final blow? Here we assume tags do count. + if (e.ever_hit_by_client_id(client_id)) { + auto& inventory = lc->character()->inventory; + 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); + } } } } diff --git a/src/SendCommands.cc b/src/SendCommands.cc index a2cd1954..56f24212 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -2710,7 +2710,7 @@ void send_game_enemy_state(shared_ptr c) { const auto& enemy = l->map->enemies[z]; auto& entry = entries.emplace_back(); entry.flags = enemy.game_flags; - entry.item_drop_id = (enemy.state_flags & Map::Enemy::Flag::ITEM_DROPPED) ? 0xFFFF : (0xCA0 + z); + entry.item_drop_id = (enemy.server_flags & Map::Enemy::Flag::ITEM_DROPPED) ? 0xFFFF : (0xCA0 + z); entry.total_damage = enemy.total_damage; } diff --git a/src/ServerState.cc b/src/ServerState.cc index 6cd8b71a..1fbf8fe4 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -935,6 +935,7 @@ void ServerState::load_config_early() { } this->bb_global_exp_multiplier = this->config_json->get_int("BBGlobalEXPMultiplier", 1); + this->exp_share_multiplier = this->config_json->get_float("BBEXPShareMultiplier", 0.5); this->server_global_drop_rate_multiplier = this->config_json->get_float("ServerGlobalDropRateMultiplier", 1); set_log_levels_from_json(this->config_json->get("LogLevels", JSON::dict())); diff --git a/src/ServerState.hh b/src/ServerState.hh index 1804205f..3282b98b 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -200,6 +200,7 @@ struct ServerState : public std::enable_shared_from_this { QuestF960Result quest_F960_failure_results; std::vector secret_lottery_results; uint16_t bb_global_exp_multiplier = 1; + float exp_share_multiplier = 0.5; double server_global_drop_rate_multiplier = 1.0; std::shared_ptr ep3_tournament_index; diff --git a/system/config.example.json b/system/config.example.json index 68cdbde4..f422f5ab 100644 --- a/system/config.example.json +++ b/system/config.example.json @@ -827,6 +827,20 @@ // EXP multiplier for BB games. This must be an integer due to a client // limitation, and must be at least 1. "BBGlobalEXPMultiplier": 1, + // EXP share multiplier for BB games. The logic for EXP computation is: + // - If the player lands the killing blow on an enemy they get full EXP (or + // multiplied by this value, if this value is greater than 1). + // - If the player tags an enemy but doesn't kill it, they get full EXP + // multiplied by 80% (or multiplied by this value, if this value is greater + // than 0.8). This applies even if the player is on a different floor when + // the enemy dies. + // - If the player is on the same floor as an enemy when it is killed but + // the player never attacked it, they get full EXP multiplied by this value + // (50% by default). + // - If the player is not on the same floor as an enemy when it is killed and + // never tagged it, they get no EXP. + // To disable EXP share in BB games, set this to zero. + "BBEXPShareMultiplier": 0.5, // Drop rate multiplier for all server drop tables. This applies to server // drop modes in all game versions. If you want to scale the drop rates for // only some versions, use the --multiply option to the convert-rare-item-set diff --git a/tests/config.json b/tests/config.json index 8b2e230d..a0ff4923 100644 --- a/tests/config.json +++ b/tests/config.json @@ -312,6 +312,7 @@ "031000", ], "BBGlobalEXPMultiplier": 1, + "BBEXPShareMultiplier": 0.5, "ServerGlobalDropRateMultiplier": 1.0, "UseGameCreatorSectionID": false,