implement level table parsers for v2 and v3

This commit is contained in:
Martin Michelsen
2023-12-27 18:03:52 -08:00
parent 764fbf8841
commit b49408a88b
3 changed files with 242 additions and 91 deletions
+145 -28
View File
@@ -5,6 +5,7 @@
#include <phosg/Filesystem.hh> #include <phosg/Filesystem.hh>
#include "Compression.hh" #include "Compression.hh"
#include "PSOEncryption.hh"
using namespace std; 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<const string> 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) { if (compressed) {
this->data = make_shared<string>(prs_decompress(*data)); decompressed_data = prs_decompress(data);
r = StringReader(decompressed_data);
} else { } else {
this->data = data; r = StringReader(data);
} }
if (this->data->size() < sizeof(Table)) { const auto& offsets = r.pget<Offsets>(r.pget_u32l(r.size() - 0x10));
throw invalid_argument("level table size is incorrect"); const auto& level_deltas_offsets = r.pget<parray<le_uint32_t, 9>>(offsets.level_deltas);
const auto& base_stats_offsets = r.pget<parray<le_uint32_t, 9>>(offsets.base_stats);
for (size_t char_class = 0; char_class < 9; char_class++) {
const auto& src_level_deltas = r.pget<parray<LevelStatsDelta, 200>>(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<PlayerStats>(offsets.max_stats + char_class * sizeof(PlayerStats));
this->level_100_stats[char_class] = r.pget<Level100Entry>(offsets.level_100_stats + char_class * sizeof(Level100Entry));
this->base_stats[char_class] = r.pget<CharacterStats>(base_stats_offsets[char_class]);
} }
this->table = reinterpret_cast<const Table*>(this->data->data());
} }
const CharacterStats& LevelTable::base_stats_for_class(uint8_t char_class) const { const CharacterStats& LevelTableV2::base_stats_for_class(uint8_t char_class) const {
if (char_class >= 12) { return this->base_stats.at(char_class);
throw out_of_range("invalid character class");
}
return this->table->base_stats[char_class];
} }
const LevelTable::LevelStats& LevelTable::stats_delta_for_level( const LevelTableV2::Level100Entry& LevelTableV2::level_100_stats_for_class(uint8_t char_class) const {
uint8_t char_class, uint8_t level) const { return this->level_100_stats.at(char_class);
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];
} }
void LevelTable::LevelStats::apply(CharacterStats& ps) const { const PlayerStats& LevelTableV2::max_stats_for_class(uint8_t char_class) const {
ps.ata += this->ata; return this->max_stats.at(char_class);
ps.atp += this->atp; }
ps.dfp += this->dfp;
ps.evp += this->evp; const LevelStatsDelta& LevelTableV2::stats_delta_for_level(uint8_t char_class, uint8_t level) const {
ps.hp += this->hp; return this->level_deltas.at(char_class).at(level);
ps.mst += this->mst; }
ps.lck += this->lck;
LevelTableV3BE::LevelTableV3BE(const string& data, bool encrypted) {
StringReader r;
string decompressed_data;
if (encrypted) {
auto decrypted = decrypt_pr2_data<true>(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<parray<be_uint32_t, 12>>(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<parray<LevelStatsDeltaBE, 200>>(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<CharacterStats, 12> 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<Offsets>(r.pget_u32l(r.size() - 0x10));
const auto& level_deltas_offsets = r.pget<parray<le_uint32_t, 12>>(offsets.level_deltas);
const auto& base_stats_offsets = r.pget<parray<le_uint32_t, 12>>(offsets.base_stats);
for (size_t char_class = 0; char_class < 12; char_class++) {
const auto& src_level_deltas = r.pget<parray<LevelStatsDelta, 200>>(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<CharacterStats>(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);
} }
+96 -62
View File
@@ -9,13 +9,14 @@
class LevelTable; class LevelTable;
struct CharacterStats { struct CharacterStats {
le_uint16_t atp = 0; /* 00 */ le_uint16_t atp = 0;
le_uint16_t mst = 0; /* 02 */ le_uint16_t mst = 0;
le_uint16_t evp = 0; /* 04 */ le_uint16_t evp = 0;
le_uint16_t hp = 0; /* 06 */ le_uint16_t hp = 0;
le_uint16_t dfp = 0; /* 08 */ le_uint16_t dfp = 0;
le_uint16_t ata = 0; /* 0A */ le_uint16_t ata = 0;
le_uint16_t lck = 0; /* 0C */ le_uint16_t lck = 0;
/* 0E */
} __attribute__((packed)); } __attribute__((packed));
struct PlayerStats { struct PlayerStats {
@@ -32,66 +33,99 @@ struct PlayerStats {
void advance_to_level(uint8_t char_class, uint32_t level, std::shared_ptr<const LevelTable> level_table); void advance_to_level(uint8_t char_class, uint32_t level, std::shared_ptr<const LevelTable> level_table);
} __attribute__((packed)); } __attribute__((packed));
class LevelTable { // from PlyLevelTbl.prs template <bool IsBigEndian>
struct LevelStatsDeltaBase {
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::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<false> {
} __attribute__((packed));
struct LevelStatsDeltaBE : LevelStatsDeltaBase<true> {
} __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: public:
struct LevelStats { virtual ~LevelTable() = default;
uint8_t atp; virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const = 0;
uint8_t mst; virtual const LevelStatsDelta& stats_delta_for_level(uint8_t char_class, uint8_t level) const = 0;
uint8_t evp;
uint8_t hp;
uint8_t dfp;
uint8_t ata;
uint8_t lck;
uint8_t tp;
le_uint32_t experience;
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)); } __attribute__((packed));
struct Table { LevelTableV2(const std::string& data, bool compressed);
CharacterStats base_stats[12]; virtual ~LevelTableV2() = default;
le_uint32_t unknown[12];
LevelStats levels[12][200];
} __attribute__((packed));
LevelTable(std::shared_ptr<const std::string> data, bool compressed); 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 CharacterStats& base_stats_for_class(uint8_t char_class) const; const PlayerStats& max_stats_for_class(uint8_t char_class) const;
const LevelStats& stats_delta_for_level(uint8_t char_class, uint8_t level) const; virtual const LevelStatsDelta& stats_delta_for_level(uint8_t char_class, uint8_t level) const;
private: private:
// TODO: Currently we only support the BB version of this file. It'd be nice std::array<CharacterStats, 9> base_stats;
// to support non-BB versions, but their formats are very different: std::array<Level100Entry, 9> level_100_stats;
// std::array<PlayerStats, 9> max_stats;
// BB: std::array<std::array<LevelStatsDelta, 200>, 9> level_deltas;
// root: };
// u32 offset:
// u32[12] unknown class LevelTableV3BE : public LevelTable { // from PlyLevelTbl.cpt (GC)
// u32 offset: public:
// u32[12] offsets: LevelTableV3BE(const std::string& data, bool encrypted);
// LevelStats[200] level_stats virtual ~LevelTableV3BE() = default;
// u32 offset:
// CharacterStats[12] base_stats virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const;
// GC: virtual const LevelStatsDelta& stats_delta_for_level(uint8_t char_class, uint8_t level) const;
// root:
// u32 offset: private:
// u32[12] offsets: std::array<std::array<LevelStatsDelta, 200>, 12> level_deltas;
// LevelStats[200] level_stats };
// PC:
// root: class LevelTableV4 : public LevelTable { // from PlyLevelTbl.prs (BB)
// u32 offset: public:
// u32 offset[9]: LevelTableV4(const std::string& data, bool compressed);
// LevelStats[200] level_stats virtual ~LevelTableV4() = default;
// u32 offset:
// (0x18 bytes) virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const;
// u32 offset: virtual const LevelStatsDelta& stats_delta_for_level(uint8_t char_class, uint8_t level) const;
// PlayerStats[9] max_stats
// u32 offset: private:
// PlayerStats[9] level100_stats std::array<CharacterStats, 12> base_stats;
// u32 offset: std::array<std::array<LevelStatsDelta, 200>, 12> level_deltas;
// u32 offset[9]:
// CharacterStats level1_stats
// (11 more pointers)
std::shared_ptr<const std::string> data;
const Table* table;
}; };
+1 -1
View File
@@ -1099,7 +1099,7 @@ void ServerState::load_battle_params() {
void ServerState::load_level_table() { void ServerState::load_level_table() {
config_log.info("Loading level table"); config_log.info("Loading level table");
this->level_table = make_shared<LevelTable>(this->load_bb_file("PlyLevelTbl.prs"), true); this->level_table = make_shared<LevelTableV4>(*this->load_bb_file("PlyLevelTbl.prs"), true);
} }
void ServerState::load_word_select_table() { void ServerState::load_word_select_table() {