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 "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<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) {
this->data = make_shared<string>(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<Offsets>(r.pget_u32l(r.size() - 0x10));
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 {
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<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;
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<const LevelTable> level_table);
} __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:
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<const std::string> 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<const std::string> data;
const Table* table;
std::array<CharacterStats, 9> base_stats;
std::array<Level100Entry, 9> level_100_stats;
std::array<PlayerStats, 9> max_stats;
std::array<std::array<LevelStatsDelta, 200>, 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<std::array<LevelStatsDelta, 200>, 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<CharacterStats, 12> base_stats;
std::array<std::array<LevelStatsDelta, 200>, 12> level_deltas;
};
+1 -1
View File
@@ -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<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() {