From d8230eb37a9b6eeb0a7667179b10f06f1f7dcb51 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Fri, 17 May 2024 20:32:52 -0700 Subject: [PATCH] load non-v4 level tables --- src/ChatCommands.cc | 5 +++-- src/Client.cc | 20 ++++++++++++++------ src/Client.hh | 1 + src/ItemCreator.cc | 6 ++---- src/ReceiveCommands.cc | 14 ++++++++------ src/ReceiveSubcommands.cc | 12 ++++++------ src/SaveFileFormats.cc | 35 +++++++++++++++++++++++++++++++++++ src/SaveFileFormats.hh | 1 + src/ServerShell.cc | 6 +++--- src/ServerState.cc | 38 ++++++++++++++++++++++++++++++++------ src/ServerState.hh | 7 +++++-- 11 files changed, 110 insertions(+), 35 deletions(-) diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index 7bd6e2f5..878caf00 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -1299,8 +1299,9 @@ static void server_command_edit(shared_ptr c, const std::string& args) { p->disp.stats.experience = stoul(tokens.at(1)); } else if (tokens.at(0) == "level" && cheats_allowed) { uint32_t level = stoul(tokens.at(1)) - 1; - s->level_table->reset_to_base(p->disp.stats, p->disp.visual.char_class); - s->level_table->advance_to_level(p->disp.stats, level, p->disp.visual.char_class); + auto level_table = s->level_table(c->version()); + level_table->reset_to_base(p->disp.stats, p->disp.visual.char_class); + level_table->advance_to_level(p->disp.stats, level, p->disp.visual.char_class); } else if (tokens.at(0) == "namecolor") { uint32_t new_color; sscanf(tokens.at(1).c_str(), "%8X", &new_color); diff --git a/src/Client.cc b/src/Client.cc index dd232c60..0a755e1c 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -775,12 +775,8 @@ void Client::load_all_files() { player_data_log.info("Loaded system data from %s", char_filename.c_str()); } - uint8_t lang = this->language(); - player_data_log.info("Overriding language fields in save files with %02hhX (%c)", - lang, char_for_language_code(lang)); - this->character_data->inventory.language = lang; - this->character_data->guild_card.language = lang; - this->system_data->base.language = lang; + this->update_character_data_after_load(this->character_data); + this->system_data->base.language = this->language(); } else { player_data_log.info("Character file is missing: %s", char_filename.c_str()); @@ -873,6 +869,7 @@ void Client::load_all_files() { } else { player_data_log.info("Loaded legacy player data from %s", nsc_filename.c_str()); } + this->update_character_data_after_load(this->character_data); } } @@ -892,6 +889,15 @@ void Client::load_all_files() { } } +void Client::update_character_data_after_load(shared_ptr charfile) { + charfile->import_tethealla_material_usage(this->require_server_state()->level_table(this->version())); + + uint8_t lang = this->language(); + player_data_log.info("Overriding language fields in save files with %02hhX (%c)", lang, char_for_language_code(lang)); + charfile->inventory.language = lang; + charfile->guild_card.language = lang; +} + void Client::save_all() { if (this->system_data) { this->save_system_file(); @@ -991,6 +997,7 @@ void Client::load_backup_character(uint32_t account_id, size_t index) { throw runtime_error("incorrect flag in character file header"); } this->character_data = make_shared(freadx(f.get())); + this->update_character_data_after_load(this->character_data); this->v1_v2_last_reported_disp.reset(); } @@ -1075,6 +1082,7 @@ void Client::use_character_bank(int8_t index) { throw runtime_error("incorrect flag in character file header"); } this->external_bank_character = make_shared(freadx(f.get())); + this->update_character_data_after_load(this->external_bank_character); this->external_bank_character_index = index; files_manager->set_character(filename, this->external_bank_character); player_data_log.info("Loaded character data from %s for external bank", filename.c_str()); diff --git a/src/Client.hh b/src/Client.hh index 652c6808..ada106d6 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -406,4 +406,5 @@ private: void save_and_clear_external_bank(); void load_all_files(); + void update_character_data_after_load(std::shared_ptr character_data); }; diff --git a/src/ItemCreator.cc b/src/ItemCreator.cc index 8c678f84..9e0b570e 100644 --- a/src/ItemCreator.cc +++ b/src/ItemCreator.cc @@ -1567,10 +1567,8 @@ void ItemCreator::generate_weapon_shop_item_grind(ItemData& item, size_t player_ ? this->weapon_random_set->get_favored_grind_range(table_index) : this->weapon_random_set->get_standard_grind_range(table_index); - const auto& weapon_def = this->item_parameter_table->get_weapon( - item.data1[1], item.data1[2]); - item.data1[3] = clamp( - this->rand_int(range->max + 1), range->min, weapon_def.max_grind); + const auto& weapon_def = this->item_parameter_table->get_weapon(item.data1[1], item.data1[2]); + item.data1[3] = clamp(this->rand_int(range->max + 1), range->min, weapon_def.max_grind); } void ItemCreator::generate_weapon_shop_item_special(ItemData& item, size_t player_level) { diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 586eb22e..99bced1a 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -1989,11 +1989,11 @@ static void on_quest_loaded(shared_ptr l) { lc->delete_overlay(); if (l->quest->battle_rules) { lc->use_default_bank(); - lc->create_battle_overlay(l->quest->battle_rules, s->level_table); + lc->create_battle_overlay(l->quest->battle_rules, s->level_table(lc->version())); lc->log.info("Created battle overlay"); } else if (l->quest->challenge_template_index >= 0) { lc->use_default_bank(); - lc->create_challenge_overlay(lc->version(), l->quest->challenge_template_index, s->level_table); + lc->create_challenge_overlay(lc->version(), l->quest->challenge_template_index, s->level_table(lc->version())); lc->log.info("Created challenge overlay"); l->assign_inventory_and_bank_item_ids(lc, true); } @@ -3216,17 +3216,19 @@ static void on_61_98(shared_ptr c, uint16_t command, uint32_t flag, stri c->language(), player->disp.visual, player->disp.name.decode(c->language()), - s->level_table); + s->level_table(c->version())); bb_player->disp.visual.version = 4; bb_player->disp.visual.name_color_checksum = 0x00000000; bb_player->inventory = player->inventory; // Before V3, player stats can't be correctly computed from other fields // because material usage isn't stored anywhere. For these versions, we // have to trust the stats field from the player's data. + auto level_table = s->level_table(c->version()); if (is_v1_or_v2(c->version())) { bb_player->disp.stats = player->disp.stats; + bb_player->import_tethealla_material_usage(level_table); } else { - s->level_table->advance_to_level(bb_player->disp.stats, player->disp.stats.level, bb_player->disp.visual.char_class); + level_table->advance_to_level(bb_player->disp.stats, player->disp.stats.level, bb_player->disp.visual.char_class); bb_player->disp.stats.char_stats.atp += bb_player->get_material_usage(PSOBBCharacterFile::MaterialType::POWER) * 2; bb_player->disp.stats.char_stats.mst += bb_player->get_material_usage(PSOBBCharacterFile::MaterialType::MIND) * 2; bb_player->disp.stats.char_stats.evp += bb_player->get_material_usage(PSOBBCharacterFile::MaterialType::EVADE) * 2; @@ -3641,7 +3643,7 @@ static void on_E5_BB(shared_ptr c, uint16_t, uint32_t, string& data) { } else { try { auto s = c->require_server_state(); - c->create_character_file(c->login->account->account_id, c->language(), cmd.preview, s->level_table); + c->create_character_file(c->login->account->account_id, c->language(), cmd.preview, s->level_table(c->version())); } catch (const exception& e) { send_message_box(c, string_printf("$C6New character could not be created:\n%s", e.what())); return; @@ -3761,7 +3763,7 @@ static void on_DF_BB(shared_ptr c, uint16_t command, uint32_t, string& d for (auto lc : l->clients) { if (lc) { lc->use_default_bank(); - lc->create_challenge_overlay(lc->version(), l->quest->challenge_template_index, s->level_table); + lc->create_challenge_overlay(lc->version(), l->quest->challenge_template_index, s->level_table(lc->version())); lc->log.info("Created challenge overlay"); l->assign_inventory_and_bank_item_ids(lc, true); } diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index cb9eaeb6..29fa90f7 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -3232,7 +3232,8 @@ static void on_level_up(shared_ptr c, uint8_t command, uint8_t flag, voi if (is_pre_v1(c->version())) { check_size_t(data, size); auto s = c->require_server_state(); - const auto& level_incrs = s->level_table->stats_delta_for_level(p->disp.visual.char_class, p->disp.stats.level + 1); + auto level_table = s->level_table(c->version()); + const auto& level_incrs = level_table->stats_delta_for_level(p->disp.visual.char_class, p->disp.stats.level + 1); p->disp.stats.char_stats.atp += level_incrs.atp; p->disp.stats.char_stats.mst += level_incrs.mst; p->disp.stats.char_stats.evp += level_incrs.evp; @@ -3265,8 +3266,7 @@ static void add_player_exp(shared_ptr c, uint32_t exp) { bool leveled_up = false; do { - const auto& level = s->level_table->stats_delta_for_level( - p->disp.visual.char_class, p->disp.stats.level + 1); + const auto& level = s->level_table(c->version())->stats_delta_for_level(p->disp.visual.char_class, p->disp.stats.level + 1); if (p->disp.stats.experience >= level.experience) { leveled_up = true; level.apply(p->disp.stats.char_stats); @@ -3775,7 +3775,7 @@ static void on_battle_restart_bb(shared_ptr c, uint8_t, uint8_t, void* d if (lc) { lc->delete_overlay(); lc->use_default_bank(); - lc->create_battle_overlay(new_rules, s->level_table); + lc->create_battle_overlay(new_rules, s->level_table(c->version())); } } l->load_maps(); @@ -3795,7 +3795,7 @@ static void on_battle_level_up_bb(shared_ptr c, uint8_t, uint8_t, void* auto lp = lc->character(); uint32_t target_level = lp->disp.stats.level + cmd.num_levels; uint32_t before_exp = lp->disp.stats.experience; - s->level_table->advance_to_level(lp->disp.stats, target_level, lp->disp.visual.char_class); + s->level_table(lc->version())->advance_to_level(lp->disp.stats, target_level, lp->disp.visual.char_class); send_give_experience(lc, lp->disp.stats.experience - before_exp); send_level_up(lc); } @@ -3839,7 +3839,7 @@ static void on_challenge_mode_retry_or_quit(shared_ptr c, uint8_t comman for (auto lc : l->clients) { if (lc) { lc->use_default_bank(); - lc->create_challenge_overlay(lc->version(), l->quest->challenge_template_index, s->level_table); + lc->create_challenge_overlay(lc->version(), l->quest->challenge_template_index, s->level_table(c->version())); lc->log.info("Created challenge overlay"); l->assign_inventory_and_bank_item_ids(lc, true); } diff --git a/src/SaveFileFormats.cc b/src/SaveFileFormats.cc index 96370c2f..a06179e7 100644 --- a/src/SaveFileFormats.cc +++ b/src/SaveFileFormats.cc @@ -4,6 +4,7 @@ #include #include +#include "LevelTable.hh" #include "PSOProtocol.hh" using namespace std; @@ -919,6 +920,40 @@ void PSOBBCharacterFile::clear_all_material_usage() { } } +void PSOBBCharacterFile::import_tethealla_material_usage(std::shared_ptr level_table) { + // Tethealla (Ephinea) doesn't store material counts anywhere in the file, + // so if the material counts in the inventory extension data are all zero, + // check the current stats against the expected stats for the character's + // current level and set the material counts if they make sense. + if (this->get_material_usage(PSOBBCharacterFile::MaterialType::POWER) | + this->get_material_usage(PSOBBCharacterFile::MaterialType::MIND) | + this->get_material_usage(PSOBBCharacterFile::MaterialType::EVADE) | + this->get_material_usage(PSOBBCharacterFile::MaterialType::DEF) | + this->get_material_usage(PSOBBCharacterFile::MaterialType::LUCK)) { + return; + } + + PlayerStats level_base_stats = this->disp.stats; + level_table->reset_to_base(level_base_stats, this->disp.visual.char_class); + level_table->advance_to_level(level_base_stats, this->disp.stats.level, this->disp.visual.char_class); + + uint64_t pow = (this->disp.stats.char_stats.atp - level_base_stats.char_stats.atp) / 2; + uint64_t mind = (this->disp.stats.char_stats.mst - level_base_stats.char_stats.mst) / 2; + uint64_t evade = (this->disp.stats.char_stats.evp - level_base_stats.char_stats.evp) / 2; + uint64_t def = (this->disp.stats.char_stats.dfp - level_base_stats.char_stats.dfp) / 2; + uint64_t luck = (this->disp.stats.char_stats.lck - level_base_stats.char_stats.lck) / 2; + + // We intentionally do not check any limits here. This is because on pre-v3, + // there are no limits, and we don't want to reject legitimate characters + // that have used more than 250 materials. + + this->set_material_usage(PSOBBCharacterFile::MaterialType::POWER, pow); + this->set_material_usage(PSOBBCharacterFile::MaterialType::MIND, mind); + this->set_material_usage(PSOBBCharacterFile::MaterialType::EVADE, evade); + this->set_material_usage(PSOBBCharacterFile::MaterialType::DEF, def); + this->set_material_usage(PSOBBCharacterFile::MaterialType::LUCK, luck); +} + static uint16_t crc16(const void* data, size_t size) { static const uint16_t table[0x100] = { // clang-format off diff --git a/src/SaveFileFormats.hh b/src/SaveFileFormats.hh index b591bfd3..20f3c657 100644 --- a/src/SaveFileFormats.hh +++ b/src/SaveFileFormats.hh @@ -674,6 +674,7 @@ struct PSOBBCharacterFile { uint8_t get_material_usage(MaterialType which) const; void set_material_usage(MaterialType which, uint8_t usage); void clear_all_material_usage(); + void import_tethealla_material_usage(std::shared_ptr level_table); } __packed_ws__(PSOBBCharacterFile, 0x2EA4); //////////////////////////////////////////////////////////////////////////////// diff --git a/src/ServerShell.cc b/src/ServerShell.cc index 3a20506e..6af6df6e 100644 --- a/src/ServerShell.cc +++ b/src/ServerShell.cc @@ -209,7 +209,7 @@ CommandDefinition c_reload( functions - recompile all client-side patches and functions\n\ item-definitions - reload item definitions files\n\ item-name-index - regenerate item name list\n\ - level-table - reload the level-up tables\n\ + level-tables - reload the player stats tables\n\ patch-files - reindex the PC and BB patch directories\n\ quests - reindex all quests (including Episode3 download quests)\n\ set-tables - reload set data tables\n\ @@ -246,8 +246,8 @@ CommandDefinition c_reload( args.s->load_set_data_tables(true); } else if (type == "battle-params") { args.s->load_battle_params(true); - } else if (type == "level-table") { - args.s->load_level_table(true); + } else if (type == "level-tables") { + args.s->load_level_tables(true); } else if (type == "text-index") { args.s->load_text_index(true); } else if (type == "word-select") { diff --git a/src/ServerState.cc b/src/ServerState.cc index b12834ea..c02fabf1 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -372,6 +372,28 @@ shared_ptr ServerState::set_data_table( return ret; } +shared_ptr ServerState::level_table(Version version) const { + switch (version) { + case Version::DC_NTE: + case Version::DC_V1_11_2000_PROTOTYPE: + case Version::DC_V1: + case Version::DC_V2: + case Version::PC_NTE: + case Version::PC_V2: + case Version::GC_NTE: // TODO: Does NTE use the v2 table, the v3 table, or neither? + return this->level_table_v1_v2; + case Version::GC_V3: + case Version::GC_EP3_NTE: + case Version::GC_EP3: + case Version::XB_V3: + return this->level_table_v3; + case Version::BB_V4: + return this->level_table_v4; + default: + throw logic_error("level table not available for version"); + } +} + shared_ptr ServerState::item_parameter_table(Version version) const { auto ret = this->item_parameter_tables.at(static_cast(version)); if (ret == nullptr) { @@ -1407,12 +1429,16 @@ void ServerState::load_battle_params(bool from_non_event_thread) { this->forward_or_call(from_non_event_thread, std::move(set)); } -void ServerState::load_level_table(bool from_non_event_thread) { - config_log.info("Loading level table"); - auto new_table = make_shared(*this->load_bb_file("PlyLevelTbl.prs"), true); +void ServerState::load_level_tables(bool from_non_event_thread) { + config_log.info("Loading level tables"); + auto new_table_v1_v2 = make_shared(load_file("system/level-tables/PlayerTable-pc-v2.prs"), true); + auto new_table_v3 = make_shared(load_file("system/level-tables/PlyLevelTbl-gc-v3.cpt"), true); + auto new_table_v4 = make_shared(*this->load_bb_file("PlyLevelTbl.prs"), true); - auto set = [s = this->shared_from_this(), new_table = std::move(new_table)]() { - s->level_table = std::move(new_table); + auto set = [s = this->shared_from_this(), new_table_v1_v2 = std::move(new_table_v1_v2), new_table_v3 = std::move(new_table_v3), new_table_v4 = std::move(new_table_v4)]() { + s->level_table_v1_v2 = std::move(new_table_v1_v2); + s->level_table_v3 = std::move(new_table_v3); + s->level_table_v4 = std::move(new_table_v4); }; this->forward_or_call(from_non_event_thread, std::move(set)); } @@ -1834,7 +1860,7 @@ void ServerState::load_all() { this->create_default_lobbies(); this->load_set_data_tables(false); this->load_battle_params(false); - this->load_level_table(false); + this->load_level_tables(false); this->load_text_index(false); this->load_word_select_table(false); this->load_item_definitions(false); diff --git a/src/ServerState.hh b/src/ServerState.hh index 172959e1..7ca68caa 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -155,7 +155,9 @@ struct ServerState : public std::enable_shared_from_this { std::shared_ptr quest_category_index; std::shared_ptr default_quest_index; std::shared_ptr ep3_download_quest_index; - std::shared_ptr level_table; + std::shared_ptr level_table_v1_v2; + std::shared_ptr level_table_v3; + std::shared_ptr level_table_v4; std::shared_ptr battle_params; std::shared_ptr bb_data_gsl; std::unordered_map> rare_item_sets; @@ -302,6 +304,7 @@ struct ServerState : public std::enable_shared_from_this { std::shared_ptr set_data_table(Version version, Episode episode, GameMode mode, uint8_t difficulty) const; + std::shared_ptr level_table(Version version) const; std::shared_ptr item_parameter_table(Version version) const; std::shared_ptr item_parameter_table_for_encode(Version version) const; std::shared_ptr item_stack_limits(Version version) const; @@ -364,7 +367,7 @@ struct ServerState : public std::enable_shared_from_this { void load_patch_indexes(bool from_non_event_thread); void clear_map_file_caches(); void load_battle_params(bool from_non_event_thread); - void load_level_table(bool from_non_event_thread); + void load_level_tables(bool from_non_event_thread); void load_text_index(bool from_non_event_thread); std::shared_ptr create_item_name_index_for_version( std::shared_ptr pmt,