implement level table parsers for v2 and v3
This commit is contained in:
+145
-28
@@ -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
@@ -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
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user