From b49408a88b6d050ba5cca0e25aaccd7281fb7520 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Wed, 27 Dec 2023 18:03:52 -0800 Subject: [PATCH] implement level table parsers for v2 and v3 --- src/LevelTable.cc | 173 +++++++++++++++++++++++++++++++++++++-------- src/LevelTable.hh | 158 +++++++++++++++++++++++++---------------- src/ServerState.cc | 2 +- 3 files changed, 242 insertions(+), 91 deletions(-) diff --git a/src/LevelTable.cc b/src/LevelTable.cc index 80e8fdd0..82e73bbc 100644 --- a/src/LevelTable.cc +++ b/src/LevelTable.cc @@ -5,6 +5,7 @@ #include #include "Compression.hh" +#include "PSOEncryption.hh" using namespace std; @@ -31,43 +32,159 @@ void PlayerStats::advance_to_level(uint8_t char_class, uint32_t level, shared_pt } } -LevelTable::LevelTable(shared_ptr data, bool compressed) { +LevelTableV2::LevelTableV2(const string& data, bool compressed) { + struct Offsets { + // TODO: The overall format of this file on V2 has much more data than we + // actually use. What's known of the structure so far: + le_uint32_t level_deltas; // -> u32[9] -> LevelStatsDelta[200] + le_uint32_t unknown_a1; // -> float[6] + le_uint32_t max_stats; // -> PlayerStats[9] + le_uint32_t level_100_stats; // -> Level100Entry[9] + le_uint32_t base_stats; // -> u32[9] -> CharacterStats + le_uint32_t unknown_a2; // -> (0x120 zero bytes) + le_uint32_t attack_data; // -> AttackData[9] + le_uint32_t unknown_a4; // -> (0x14-byte struct)[9] + le_uint32_t unknown_a5; // -> float[9] + le_uint32_t unknown_a6; // -> (0x30 bytes) + le_uint32_t unknown_a7; // -> (0x2D bytes) + le_uint32_t unknown_a8; // -> u32[3] -> float[0x2D] + le_uint32_t unknown_a9; // -> (0x90 bytes) + le_uint32_t unknown_a10; // -> u32[3] -> (0x10-byte struct)[0x0C] + le_uint32_t unknown_a11; // -> u32[3] -> (0x30-bytes) + le_uint32_t unknown_a12; // -> u32[3] -> (0x14-byte struct)[0x0F] + } __attribute__((packed)); + + StringReader r; + string decompressed_data; if (compressed) { - this->data = make_shared(prs_decompress(*data)); + decompressed_data = prs_decompress(data); + r = StringReader(decompressed_data); } else { - this->data = data; + r = StringReader(data); } - if (this->data->size() < sizeof(Table)) { - throw invalid_argument("level table size is incorrect"); + const auto& offsets = r.pget(r.pget_u32l(r.size() - 0x10)); + const auto& level_deltas_offsets = r.pget>(offsets.level_deltas); + const auto& base_stats_offsets = r.pget>(offsets.base_stats); + for (size_t char_class = 0; char_class < 9; char_class++) { + const auto& src_level_deltas = r.pget>(level_deltas_offsets[char_class]); + for (size_t level = 0; level < 200; level++) { + this->level_deltas[char_class][level] = src_level_deltas[level]; + } + this->max_stats[char_class] = r.pget(offsets.max_stats + char_class * sizeof(PlayerStats)); + this->level_100_stats[char_class] = r.pget(offsets.level_100_stats + char_class * sizeof(Level100Entry)); + this->base_stats[char_class] = r.pget(base_stats_offsets[char_class]); } - this->table = reinterpret_cast(this->data->data()); } -const CharacterStats& LevelTable::base_stats_for_class(uint8_t char_class) const { - if (char_class >= 12) { - throw out_of_range("invalid character class"); - } - return this->table->base_stats[char_class]; +const CharacterStats& LevelTableV2::base_stats_for_class(uint8_t char_class) const { + return this->base_stats.at(char_class); } -const LevelTable::LevelStats& LevelTable::stats_delta_for_level( - uint8_t char_class, uint8_t level) const { - if (char_class >= 12) { - throw invalid_argument("invalid character class"); - } - if (level >= 200) { - throw invalid_argument("invalid character level"); - } - return this->table->levels[char_class][level]; +const LevelTableV2::Level100Entry& LevelTableV2::level_100_stats_for_class(uint8_t char_class) const { + return this->level_100_stats.at(char_class); } -void LevelTable::LevelStats::apply(CharacterStats& ps) const { - ps.ata += this->ata; - ps.atp += this->atp; - ps.dfp += this->dfp; - ps.evp += this->evp; - ps.hp += this->hp; - ps.mst += this->mst; - ps.lck += this->lck; +const PlayerStats& LevelTableV2::max_stats_for_class(uint8_t char_class) const { + return this->max_stats.at(char_class); +} + +const LevelStatsDelta& LevelTableV2::stats_delta_for_level(uint8_t char_class, uint8_t level) const { + return this->level_deltas.at(char_class).at(level); +} + +LevelTableV3BE::LevelTableV3BE(const string& data, bool encrypted) { + StringReader r; + string decompressed_data; + if (encrypted) { + auto decrypted = decrypt_pr2_data(data); + decompressed_data = prs_decompress(decrypted.compressed_data); + if (decompressed_data.size() != decrypted.decompressed_size) { + throw runtime_error("decompressed data size does not match expected size"); + } + r = StringReader(decompressed_data); + } else { + r = StringReader(data); + } + + // The GC format is very simple (but everything is big-endian): + // root: + // u32 offset: + // u32[12] offsets: + // LevelStatsDeltaBE[200] level_deltas + const auto& offsets = r.pget>(r.pget_u32b(r.pget_u32b(r.size() - 0x10))); + for (size_t char_class = 0; char_class < 12; char_class++) { + const auto& src_deltas = r.pget>(offsets[char_class]); + for (size_t level = 0; level < 200; level++) { + const auto& src_delta = src_deltas[level]; + auto& dest_delta = this->level_deltas[char_class][level]; + dest_delta.atp = src_delta.atp; + dest_delta.mst = src_delta.mst; + dest_delta.evp = src_delta.evp; + dest_delta.hp = src_delta.hp; + dest_delta.dfp = src_delta.dfp; + dest_delta.ata = src_delta.ata; + dest_delta.lck = src_delta.lck; + dest_delta.tp = src_delta.tp; + dest_delta.experience = src_delta.experience.load(); + } + } +} + +const CharacterStats& LevelTableV3BE::base_stats_for_class(uint8_t char_class) const { + static const array data = { + // ATP MST EVP HP DFP ATA LCK + CharacterStats{0x0023, 0x001D, 0x002D, 0x0014, 0x0011, 0x001E, 0x000A}, + CharacterStats{0x001E, 0x0028, 0x003C, 0x0013, 0x0016, 0x0019, 0x000A}, + CharacterStats{0x0023, 0x0000, 0x0023, 0x0016, 0x0012, 0x0023, 0x000A}, + CharacterStats{0x0012, 0x0014, 0x0024, 0x0010, 0x000D, 0x0028, 0x000A}, + CharacterStats{0x0019, 0x0000, 0x001F, 0x0012, 0x0012, 0x002D, 0x000A}, + CharacterStats{0x0014, 0x0000, 0x001F, 0x0011, 0x0017, 0x002D, 0x000A}, + CharacterStats{0x000D, 0x0035, 0x0023, 0x0014, 0x000A, 0x000F, 0x000A}, + CharacterStats{0x000D, 0x003C, 0x0032, 0x0013, 0x0007, 0x000C, 0x000A}, + CharacterStats{0x000A, 0x003A, 0x0035, 0x0013, 0x000D, 0x000A, 0x000A}, + CharacterStats{0x0023, 0x0000, 0x0023, 0x0016, 0x0012, 0x0023, 0x000A}, + CharacterStats{0x000D, 0x0035, 0x0023, 0x0014, 0x000A, 0x000F, 0x000A}, + CharacterStats{0x0012, 0x0014, 0x0024, 0x0010, 0x000D, 0x0028, 0x000A}, + }; + return data.at(char_class); +} + +const LevelStatsDelta& LevelTableV3BE::stats_delta_for_level(uint8_t char_class, uint8_t level) const { + return this->level_deltas.at(char_class).at(level); +} + +LevelTableV4::LevelTableV4(const string& data, bool compressed) { + struct Offsets { + le_uint32_t base_stats; // -> u32[12] -> CharacterStats + le_uint32_t level_deltas; // -> u32[12] -> LevelStatsDelta[200] + } __attribute__((packed)); + + StringReader r; + string decompressed_data; + if (compressed) { + decompressed_data = prs_decompress(data); + r = StringReader(decompressed_data); + } else { + r = StringReader(data); + } + + const auto& offsets = r.pget(r.pget_u32l(r.size() - 0x10)); + const auto& level_deltas_offsets = r.pget>(offsets.level_deltas); + const auto& base_stats_offsets = r.pget>(offsets.base_stats); + for (size_t char_class = 0; char_class < 12; char_class++) { + const auto& src_level_deltas = r.pget>(level_deltas_offsets[char_class]); + for (size_t level = 0; level < 200; level++) { + this->level_deltas[char_class][level] = src_level_deltas[level]; + } + this->base_stats[char_class] = r.pget(base_stats_offsets[char_class]); + } +} + +const CharacterStats& LevelTableV4::base_stats_for_class(uint8_t char_class) const { + return this->base_stats.at(char_class); +} + +const LevelStatsDelta& LevelTableV4::stats_delta_for_level(uint8_t char_class, uint8_t level) const { + return this->level_deltas.at(char_class).at(level); } diff --git a/src/LevelTable.hh b/src/LevelTable.hh index 8b1bfd5a..3be121b4 100644 --- a/src/LevelTable.hh +++ b/src/LevelTable.hh @@ -9,13 +9,14 @@ class LevelTable; struct CharacterStats { - le_uint16_t atp = 0; - le_uint16_t mst = 0; - le_uint16_t evp = 0; - le_uint16_t hp = 0; - le_uint16_t dfp = 0; - le_uint16_t ata = 0; - le_uint16_t lck = 0; + /* 00 */ le_uint16_t atp = 0; + /* 02 */ le_uint16_t mst = 0; + /* 04 */ le_uint16_t evp = 0; + /* 06 */ le_uint16_t hp = 0; + /* 08 */ le_uint16_t dfp = 0; + /* 0A */ le_uint16_t ata = 0; + /* 0C */ le_uint16_t lck = 0; + /* 0E */ } __attribute__((packed)); struct PlayerStats { @@ -32,66 +33,99 @@ struct PlayerStats { void advance_to_level(uint8_t char_class, uint32_t level, std::shared_ptr level_table); } __attribute__((packed)); -class LevelTable { // from PlyLevelTbl.prs +template +struct LevelStatsDeltaBase { + using U32T = typename std::conditional::type; + + /* 00 */ uint8_t atp; + /* 01 */ uint8_t mst; + /* 02 */ uint8_t evp; + /* 03 */ uint8_t hp; + /* 04 */ uint8_t dfp; + /* 05 */ uint8_t ata; + /* 06 */ uint8_t lck; + /* 07 */ uint8_t tp; + /* 08 */ U32T experience; + /* 0C */ + + void apply(CharacterStats& ps) const { + ps.ata += this->ata; + ps.atp += this->atp; + ps.dfp += this->dfp; + ps.evp += this->evp; + ps.hp += this->hp; + ps.mst += this->mst; + ps.lck += this->lck; + } +} __attribute__((packed)); + +struct LevelStatsDelta : LevelStatsDeltaBase { +} __attribute__((packed)); +struct LevelStatsDeltaBE : LevelStatsDeltaBase { +} __attribute__((packed)); + +class LevelTable { + // This is the base class for all the LevelTable implementations. The public + // interface here only defines functions that the server needs to handle + // requests, but some subclasses implement more functionality. See the + // comments and Offsets structures inside the subclasses' constructor + // implementations for more details on the file formats. public: - struct LevelStats { - uint8_t atp; - uint8_t mst; - uint8_t evp; - uint8_t hp; - uint8_t dfp; - uint8_t ata; - uint8_t lck; - uint8_t tp; - le_uint32_t experience; + virtual ~LevelTable() = default; + virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const = 0; + virtual const LevelStatsDelta& stats_delta_for_level(uint8_t char_class, uint8_t level) const = 0; - void apply(CharacterStats& ps) const; +protected: + LevelTable() = default; +}; + +class LevelTableV2 : public LevelTable { // from PlayerTable.prs (PC) +public: + struct Level100Entry { + /* 00 */ CharacterStats char_stats; + /* 0E */ le_uint16_t unknown_a1 = 0; + /* 10 */ le_float height = 0.0; + /* 14 */ le_float unknown_a3 = 0.0; + /* 18 */ le_uint32_t level = 0; + /* 1C */ } __attribute__((packed)); - struct Table { - CharacterStats base_stats[12]; - le_uint32_t unknown[12]; - LevelStats levels[12][200]; - } __attribute__((packed)); + LevelTableV2(const std::string& data, bool compressed); + virtual ~LevelTableV2() = default; - LevelTable(std::shared_ptr data, bool compressed); - - const CharacterStats& base_stats_for_class(uint8_t char_class) const; - const LevelStats& stats_delta_for_level(uint8_t char_class, uint8_t level) const; + virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const; + const Level100Entry& level_100_stats_for_class(uint8_t char_class) const; + const PlayerStats& max_stats_for_class(uint8_t char_class) const; + virtual const LevelStatsDelta& stats_delta_for_level(uint8_t char_class, uint8_t level) const; private: - // TODO: Currently we only support the BB version of this file. It'd be nice - // to support non-BB versions, but their formats are very different: - // - // BB: - // root: - // u32 offset: - // u32[12] unknown - // u32 offset: - // u32[12] offsets: - // LevelStats[200] level_stats - // u32 offset: - // CharacterStats[12] base_stats - // GC: - // root: - // u32 offset: - // u32[12] offsets: - // LevelStats[200] level_stats - // PC: - // root: - // u32 offset: - // u32 offset[9]: - // LevelStats[200] level_stats - // u32 offset: - // (0x18 bytes) - // u32 offset: - // PlayerStats[9] max_stats - // u32 offset: - // PlayerStats[9] level100_stats - // u32 offset: - // u32 offset[9]: - // CharacterStats level1_stats - // (11 more pointers) - std::shared_ptr data; - const Table* table; + std::array base_stats; + std::array level_100_stats; + std::array max_stats; + std::array, 9> level_deltas; +}; + +class LevelTableV3BE : public LevelTable { // from PlyLevelTbl.cpt (GC) +public: + LevelTableV3BE(const std::string& data, bool encrypted); + virtual ~LevelTableV3BE() = default; + + virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const; + virtual const LevelStatsDelta& stats_delta_for_level(uint8_t char_class, uint8_t level) const; + +private: + std::array, 12> level_deltas; +}; + +class LevelTableV4 : public LevelTable { // from PlyLevelTbl.prs (BB) +public: + LevelTableV4(const std::string& data, bool compressed); + virtual ~LevelTableV4() = default; + + virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const; + virtual const LevelStatsDelta& stats_delta_for_level(uint8_t char_class, uint8_t level) const; + +private: + std::array base_stats; + std::array, 12> level_deltas; }; diff --git a/src/ServerState.cc b/src/ServerState.cc index 54ff645e..934621cb 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -1099,7 +1099,7 @@ void ServerState::load_battle_params() { void ServerState::load_level_table() { config_log.info("Loading level table"); - this->level_table = make_shared(this->load_bb_file("PlyLevelTbl.prs"), true); + this->level_table = make_shared(*this->load_bb_file("PlyLevelTbl.prs"), true); } void ServerState::load_word_select_table() {