implement BB EXP share
This commit is contained in:
@@ -579,6 +579,7 @@ JSON HTTPServer::generate_lobby_json_st(shared_ptr<const Lobby> l, shared_ptr<co
|
|||||||
ret.emplace("Mode", name_for_mode(l->mode));
|
ret.emplace("Mode", name_for_mode(l->mode));
|
||||||
ret.emplace("Difficulty", name_for_difficulty(l->difficulty));
|
ret.emplace("Difficulty", name_for_difficulty(l->difficulty));
|
||||||
ret.emplace("BaseEXPMultiplier", l->base_exp_multiplier);
|
ret.emplace("BaseEXPMultiplier", l->base_exp_multiplier);
|
||||||
|
ret.emplace("EXPShareMultiplier", l->exp_share_multiplier);
|
||||||
ret.emplace("AllowedDropModes", l->allowed_drop_modes);
|
ret.emplace("AllowedDropModes", l->allowed_drop_modes);
|
||||||
switch (l->drop_mode) {
|
switch (l->drop_mode) {
|
||||||
case Lobby::DropMode::DISABLED:
|
case Lobby::DropMode::DISABLED:
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ Lobby::Lobby(shared_ptr<ServerState> s, uint32_t id, bool is_game)
|
|||||||
mode(GameMode::NORMAL),
|
mode(GameMode::NORMAL),
|
||||||
difficulty(0),
|
difficulty(0),
|
||||||
base_exp_multiplier(1),
|
base_exp_multiplier(1),
|
||||||
|
exp_share_multiplier(0.5),
|
||||||
challenge_exp_multiplier(1.0f),
|
challenge_exp_multiplier(1.0f),
|
||||||
random_seed(random_object<uint32_t>()),
|
random_seed(random_object<uint32_t>()),
|
||||||
drop_mode(DropMode::CLIENT),
|
drop_mode(DropMode::CLIENT),
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
|||||||
GameMode mode;
|
GameMode mode;
|
||||||
uint8_t difficulty; // 0-3
|
uint8_t difficulty; // 0-3
|
||||||
uint16_t base_exp_multiplier;
|
uint16_t base_exp_multiplier;
|
||||||
|
float exp_share_multiplier;
|
||||||
float challenge_exp_multiplier;
|
float challenge_exp_multiplier;
|
||||||
std::string password;
|
std::string password;
|
||||||
std::string name;
|
std::string name;
|
||||||
|
|||||||
+3
-3
@@ -678,10 +678,10 @@ Map::Enemy::Enemy(
|
|||||||
wave_number(wave_number),
|
wave_number(wave_number),
|
||||||
type(type),
|
type(type),
|
||||||
floor(floor),
|
floor(floor),
|
||||||
state_flags(0) {}
|
server_flags(0) {}
|
||||||
|
|
||||||
string Map::Enemy::str() const {
|
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->enemy_id,
|
||||||
this->source_index,
|
this->source_index,
|
||||||
name_for_enum(this->type),
|
name_for_enum(this->type),
|
||||||
@@ -689,7 +689,7 @@ string Map::Enemy::str() const {
|
|||||||
this->floor,
|
this->floor,
|
||||||
this->section,
|
this->section,
|
||||||
this->wave_number,
|
this->wave_number,
|
||||||
this->state_flags);
|
this->server_flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
string Map::Event::str() const {
|
string Map::Event::str() const {
|
||||||
|
|||||||
+16
-6
@@ -233,11 +233,11 @@ struct Map {
|
|||||||
|
|
||||||
struct Enemy {
|
struct Enemy {
|
||||||
enum Flag {
|
enum Flag {
|
||||||
EXP_REQUESTED_BY_PLAYER0 = 0x01,
|
LAST_HIT_MASK = 0x03,
|
||||||
EXP_REQUESTED_BY_PLAYER1 = 0x02,
|
EXP_GIVEN = 0x04,
|
||||||
EXP_REQUESTED_BY_PLAYER2 = 0x04,
|
ITEM_DROPPED = 0x08,
|
||||||
EXP_REQUESTED_BY_PLAYER3 = 0x08,
|
ALL_HITS_MASK_FIRST = 0x10,
|
||||||
ITEM_DROPPED = 0x10,
|
ALL_HITS_MASK = 0xF0,
|
||||||
};
|
};
|
||||||
size_t source_index;
|
size_t source_index;
|
||||||
size_t set_index;
|
size_t set_index;
|
||||||
@@ -248,7 +248,7 @@ struct Map {
|
|||||||
uint16_t wave_number;
|
uint16_t wave_number;
|
||||||
EnemyType type;
|
EnemyType type;
|
||||||
uint8_t floor;
|
uint8_t floor;
|
||||||
uint8_t state_flags;
|
uint8_t server_flags;
|
||||||
|
|
||||||
Enemy(
|
Enemy(
|
||||||
uint16_t enemy_id,
|
uint16_t enemy_id,
|
||||||
@@ -260,6 +260,16 @@ struct Map {
|
|||||||
EnemyType type);
|
EnemyType type);
|
||||||
|
|
||||||
std::string str() const;
|
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 {
|
struct Event {
|
||||||
|
|||||||
@@ -4257,9 +4257,8 @@ shared_ptr<Lobby> create_game_generic(
|
|||||||
game->battle_player = battle_player;
|
game->battle_player = battle_player;
|
||||||
battle_player->set_lobby(game);
|
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<uint16_t, IntegralExpression>* quest_flag_rewrites;
|
const unordered_map<uint16_t, IntegralExpression>* quest_flag_rewrites;
|
||||||
switch (game->base_version) {
|
switch (game->base_version) {
|
||||||
|
|||||||
+45
-29
@@ -2625,11 +2625,11 @@ DropReconcileResult reconcile_drop_request_with_map(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (map_enemy) {
|
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);
|
log.info("Drop check has already occurred for E-%04hX; skipping it", map_enemy->enemy_id);
|
||||||
res.should_drop = false;
|
res.should_drop = false;
|
||||||
} else {
|
} 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<Client> c, uint8_t command, uint8_t
|
|||||||
auto& enemy = l->map->enemies[cmd.enemy_index];
|
auto& enemy = l->map->enemies[cmd.enemy_index];
|
||||||
enemy.game_flags = is_big_endian(c->version()) ? bswap32(cmd.flags) : cmd.flags.load();
|
enemy.game_flags = is_big_endian(c->version()) ? bswap32(cmd.flags) : cmd.flags.load();
|
||||||
enemy.total_damage = cmd.total_damage;
|
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);
|
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<Client> c, uint8_t, uint8_t, void
|
|||||||
auto s = c->require_server_state();
|
auto s = c->require_server_state();
|
||||||
auto l = c->require_lobby();
|
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<G_EnemyEXPRequest_BB_6xC8>(data, size);
|
const auto& cmd = check_size_t<G_EnemyEXPRequest_BB_6xC8>(data, size);
|
||||||
|
|
||||||
if (!l->is_game()) {
|
if (!l->is_game()) {
|
||||||
@@ -3421,20 +3418,20 @@ static void on_enemy_exp_request_bb(shared_ptr<Client> c, uint8_t, uint8_t, void
|
|||||||
string e_str = e.str();
|
string e_str = e.str();
|
||||||
c->log.info("EXP requested for E-%hX => %s", cmd.enemy_index.load(), e_str.c_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 the requesting player never hit this enemy, they are probably cheating;
|
||||||
if (e.state_flags & state_flag) {
|
// ignore the command. Also, each player sends a 6xC8 if they ever hit the
|
||||||
if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
// enemy; we only react to the first 6xC8 for each enemy (and give all
|
||||||
send_text_message_printf(c, "$C5E-%hX __CHECKED__", cmd.enemy_index.load());
|
// 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;
|
||||||
|
|
||||||
} else {
|
double base_exp = 0.0;
|
||||||
e.state_flags |= state_flag;
|
|
||||||
|
|
||||||
double experience = 0.0;
|
|
||||||
try {
|
try {
|
||||||
const auto& bp_table = s->battle_params->get_table(l->mode == GameMode::SOLO, l->episode);
|
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);
|
uint32_t bp_index = battle_param_index_for_enemy_type(l->episode, e.type);
|
||||||
experience = bp_table.stats[l->difficulty][bp_index].experience;
|
base_exp = bp_table.stats[l->difficulty][bp_index].experience;
|
||||||
} catch (const exception& e) {
|
} catch (const exception& e) {
|
||||||
if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
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());
|
send_text_message_printf(c, "$C5E-%hX __MISSING__\n%s", cmd.enemy_index.load(), e.what());
|
||||||
@@ -3443,48 +3440,67 @@ static void on_enemy_exp_request_bb(shared_ptr<Client> c, uint8_t, uint8_t, void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (experience != 0.0) {
|
for (size_t client_id = 0; client_id < 4; client_id++) {
|
||||||
if (c->floor != e.floor) {
|
auto lc = l->clients[client_id];
|
||||||
if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
if (!lc) {
|
||||||
send_text_message_printf(c, "$C5E-%hX %s\n$C4FLOOR Y:%02" PRIX32 " E:%02hhX",
|
continue;
|
||||||
cmd.enemy_index.load(), name_for_enum(e.type), c->floor, e.floor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<double>(1.0, l->exp_share_multiplier);
|
||||||
|
} else if (e.ever_hit_by_client_id(client_id)) {
|
||||||
|
rate_factor = max<double>(0.8, l->exp_share_multiplier);
|
||||||
|
} else if (lc->floor == e.floor) {
|
||||||
|
rate_factor = l->exp_share_multiplier;
|
||||||
} else {
|
} else {
|
||||||
|
rate_factor = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rate_factor > 0.0) {
|
||||||
// In PSOBB, Sega decided to add a 30% EXP boost for Episode 2. They could
|
// 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
|
// have done something reasonable, like edit the BattleParamEntry files so
|
||||||
// the monsters would all give more EXP, but they did something far lazier
|
// 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
|
// instead: they just stuck an if statement in the client's EXP request
|
||||||
// function. We, unfortunately, have to do the same here.
|
// function. We, unfortunately, have to do the same here.
|
||||||
bool is_ep2 = (l->episode == Episode::EP2);
|
bool is_ep2 = (l->episode == Episode::EP2);
|
||||||
uint32_t player_exp = experience *
|
uint32_t player_exp = base_exp *
|
||||||
(cmd.is_killer ? 1.0 : 0.8) *
|
rate_factor *
|
||||||
l->base_exp_multiplier *
|
l->base_exp_multiplier *
|
||||||
l->challenge_exp_multiplier *
|
l->challenge_exp_multiplier *
|
||||||
(is_ep2 ? 1.3 : 1.0);
|
(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(
|
send_text_message_printf(
|
||||||
c, "$C5+%" PRIu32 " E-%hX %s",
|
lc, "$C5+%" PRIu32 " E-%hX %s",
|
||||||
player_exp,
|
player_exp,
|
||||||
cmd.enemy_index.load(),
|
cmd.enemy_index.load(),
|
||||||
name_for_enum(e.type));
|
name_for_enum(e.type));
|
||||||
}
|
}
|
||||||
if (c->character()->disp.stats.level < 199) {
|
if (lc->character()->disp.stats.level < 199) {
|
||||||
add_player_exp(c, player_exp);
|
add_player_exp(lc, player_exp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update kill counts on unsealable items
|
// Update kill counts on unsealable items
|
||||||
auto& inventory = c->character()->inventory;
|
// 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++) {
|
for (size_t z = 0; z < inventory.num_items; z++) {
|
||||||
auto& item = inventory.items[z];
|
auto& item = inventory.items[z];
|
||||||
if ((item.flags & 0x08) &&
|
if ((item.flags & 0x08) && s->item_parameter_table(lc->version())->is_unsealable_item(item.data)) {
|
||||||
s->item_parameter_table(c->version())->is_unsealable_item(item.data)) {
|
|
||||||
item.data.set_kill_count(item.data.get_kill_count() + 1);
|
item.data.set_kill_count(item.data.get_kill_count() + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void on_adjust_player_meseta_bb(shared_ptr<Client> c, uint8_t, uint8_t, void* data, size_t size) {
|
void on_adjust_player_meseta_bb(shared_ptr<Client> c, uint8_t, uint8_t, void* data, size_t size) {
|
||||||
const auto& cmd = check_size_t<G_AdjustPlayerMeseta_BB_6xC9>(data, size);
|
const auto& cmd = check_size_t<G_AdjustPlayerMeseta_BB_6xC9>(data, size);
|
||||||
|
|||||||
+1
-1
@@ -2710,7 +2710,7 @@ void send_game_enemy_state(shared_ptr<Client> c) {
|
|||||||
const auto& enemy = l->map->enemies[z];
|
const auto& enemy = l->map->enemies[z];
|
||||||
auto& entry = entries.emplace_back();
|
auto& entry = entries.emplace_back();
|
||||||
entry.flags = enemy.game_flags;
|
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;
|
entry.total_damage = enemy.total_damage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -935,6 +935,7 @@ void ServerState::load_config_early() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this->bb_global_exp_multiplier = this->config_json->get_int("BBGlobalEXPMultiplier", 1);
|
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);
|
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()));
|
set_log_levels_from_json(this->config_json->get("LogLevels", JSON::dict()));
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
|||||||
QuestF960Result quest_F960_failure_results;
|
QuestF960Result quest_F960_failure_results;
|
||||||
std::vector<ItemData> secret_lottery_results;
|
std::vector<ItemData> secret_lottery_results;
|
||||||
uint16_t bb_global_exp_multiplier = 1;
|
uint16_t bb_global_exp_multiplier = 1;
|
||||||
|
float exp_share_multiplier = 0.5;
|
||||||
double server_global_drop_rate_multiplier = 1.0;
|
double server_global_drop_rate_multiplier = 1.0;
|
||||||
|
|
||||||
std::shared_ptr<Episode3::TournamentIndex> ep3_tournament_index;
|
std::shared_ptr<Episode3::TournamentIndex> ep3_tournament_index;
|
||||||
|
|||||||
@@ -827,6 +827,20 @@
|
|||||||
// EXP multiplier for BB games. This must be an integer due to a client
|
// EXP multiplier for BB games. This must be an integer due to a client
|
||||||
// limitation, and must be at least 1.
|
// limitation, and must be at least 1.
|
||||||
"BBGlobalEXPMultiplier": 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 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
|
// 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
|
// only some versions, use the --multiply option to the convert-rare-item-set
|
||||||
|
|||||||
@@ -312,6 +312,7 @@
|
|||||||
"031000",
|
"031000",
|
||||||
],
|
],
|
||||||
"BBGlobalEXPMultiplier": 1,
|
"BBGlobalEXPMultiplier": 1,
|
||||||
|
"BBEXPShareMultiplier": 0.5,
|
||||||
"ServerGlobalDropRateMultiplier": 1.0,
|
"ServerGlobalDropRateMultiplier": 1.0,
|
||||||
"UseGameCreatorSectionID": false,
|
"UseGameCreatorSectionID": false,
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user