add level table JSON format
This commit is contained in:
+182
-92
@@ -5,14 +5,13 @@
|
||||
#include <phosg/Filesystem.hh>
|
||||
|
||||
#include "CommonFileFormats.hh"
|
||||
#include "Compression.hh"
|
||||
#include "PSOEncryption.hh"
|
||||
#include "StaticGameData.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
void LevelTable::reset_to_base(PlayerStats& stats, uint8_t char_class) const {
|
||||
stats.level = 0;
|
||||
stats.experience = 0;
|
||||
stats.exp = 0;
|
||||
stats.char_stats = this->base_stats_for_class(char_class);
|
||||
}
|
||||
|
||||
@@ -28,43 +27,111 @@ void LevelTable::advance_to_level(PlayerStats& stats, uint32_t level, uint8_t ch
|
||||
stats.char_stats.dfp += level_stats.dfp;
|
||||
stats.char_stats.ata += level_stats.ata;
|
||||
// Note: It is not a bug that lck is ignored here; the original code ignores it too.
|
||||
stats.experience = level_stats.experience;
|
||||
stats.exp = level_stats.exp;
|
||||
}
|
||||
}
|
||||
|
||||
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; // (5468) -> u32[9] -> LevelStatsDelta[200]
|
||||
le_uint32_t unknown_a1; // (548C) -> float[6]
|
||||
le_uint32_t max_stats; // (54A4) -> PlayerStats[9]
|
||||
le_uint32_t level_100_stats; // (55E8) -> PlayerStats[9]
|
||||
le_uint32_t base_stats; // (57AC) -> u32[9] -> CharacterStats
|
||||
le_uint32_t unknown_a2; // (57D0) -> (0x120 zero bytes)
|
||||
le_uint32_t attack_data; // (58F0) -> AttackData[9]
|
||||
le_uint32_t unknown_a4; // (5AA0) -> parray<parray<float, 5>, 9>
|
||||
le_uint32_t unknown_a5; // (5B54) -> float[9]
|
||||
le_uint32_t unknown_a6; // (5B78) -> (0x30 bytes)
|
||||
le_uint32_t unknown_a7; // (5BA8) -> (0x2D bytes)
|
||||
le_uint32_t unknown_a8; // (5E00) -> u32[3] -> float[0x2D]
|
||||
le_uint32_t unknown_a9; // (5DF4) -> (0x90 bytes)
|
||||
le_uint32_t unknown_a10; // (60D0) -> u32[3] -> (0x10-byte struct)[0x0C]
|
||||
le_uint32_t unknown_a11; // (616C) -> u32[3] -> (0x30-bytes)
|
||||
le_uint32_t unknown_a12; // (64FC) -> u32[3] -> (0x14-byte struct)[0x0F]
|
||||
} __packed_ws__(Offsets, 0x40);
|
||||
|
||||
phosg::StringReader r;
|
||||
string decompressed_data;
|
||||
if (compressed) {
|
||||
decompressed_data = prs_decompress(data);
|
||||
r = phosg::StringReader(decompressed_data);
|
||||
} else {
|
||||
r = phosg::StringReader(data);
|
||||
phosg::JSON LevelTable::json() const {
|
||||
auto base_stats_json = phosg::JSON::list();
|
||||
auto max_stats_json = phosg::JSON::list();
|
||||
auto level_deltas_json = phosg::JSON::list();
|
||||
for (size_t char_class = 0; char_class < this->num_char_classes(); char_class++) {
|
||||
base_stats_json.emplace_back(this->base_stats_for_class(char_class).json());
|
||||
max_stats_json.emplace_back(this->max_stats_for_class(char_class).json());
|
||||
auto this_class_level_deltas_json = phosg::JSON::list();
|
||||
for (size_t level = 0; level < 200; level++) {
|
||||
this_class_level_deltas_json.emplace_back(this->stats_delta_for_level(char_class, level).json());
|
||||
}
|
||||
level_deltas_json.emplace_back(std::move(this_class_level_deltas_json));
|
||||
}
|
||||
return phosg::JSON::dict({
|
||||
{"BaseStats", std::move(base_stats_json)},
|
||||
{"MaxStats", std::move(max_stats_json)},
|
||||
{"LevelDeltas", std::move(level_deltas_json)},
|
||||
});
|
||||
}
|
||||
|
||||
JSONLevelTable::JSONLevelTable(const phosg::JSON& json) {
|
||||
const auto& base_stats_json = json.at("BaseStats").as_list();
|
||||
const auto& max_stats_json = json.at("MaxStats").as_list();
|
||||
const auto& level_deltas_json = json.at("LevelDeltas").as_list();
|
||||
for (size_t char_class = 0; char_class < base_stats_json.size(); char_class++) {
|
||||
this->base_stats.emplace_back(CharacterStats::from_json(*base_stats_json.at(char_class)));
|
||||
this->max_stats.emplace_back(PlayerStats::from_json(*max_stats_json.at(char_class)));
|
||||
const auto& this_class_level_deltas_json = level_deltas_json.at(char_class)->as_list();
|
||||
auto& parsed_deltas = this->level_deltas.emplace_back();
|
||||
for (size_t level = 0; level < 200; level++) {
|
||||
parsed_deltas[level] = LevelStatsDelta::from_json(*this_class_level_deltas_json.at(level));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size_t JSONLevelTable::num_char_classes() const {
|
||||
return this->base_stats.size();
|
||||
}
|
||||
|
||||
const CharacterStats& JSONLevelTable::base_stats_for_class(uint8_t char_class) const {
|
||||
return this->base_stats.at(char_class);
|
||||
}
|
||||
|
||||
const PlayerStats& JSONLevelTable::max_stats_for_class(uint8_t char_class) const {
|
||||
return this->max_stats.at(char_class);
|
||||
}
|
||||
|
||||
const LevelStatsDelta& JSONLevelTable::stats_delta_for_level(uint8_t char_class, uint8_t level) const {
|
||||
return this->level_deltas.at(char_class).at(level);
|
||||
}
|
||||
|
||||
LevelTableV2::LevelTableV2(const string& data) {
|
||||
struct Root {
|
||||
// The overall format of this file on V2 has much more data than we actually use. This table is sorted by the
|
||||
// offset in the PlayerTable.prs file; note that the offset fields in this structure do not match that order.
|
||||
// ## OFFS WHAT -> TARGET
|
||||
// 0008 level_deltas[0] -> LevelStatsDelta[200] (by level) (the rest follow immediately)
|
||||
// 00 5468 level_deltas -> u32[9] (by char_class)
|
||||
// 04 548C hp_tp_factors -> HPTPFactors[3] (by char_class_class; [0] = hunter, [1] = ranger, [2] = force)
|
||||
// 08 54A4 max_stats -> PlayerStats[9] (by char_class)
|
||||
// 0C 55E8 level_100_stats -> PlayerStats[9] (by char_class)
|
||||
// 572C base_stats[0] -> CharacterStats
|
||||
// 10 57AC base_stats -> u32[9] (by char_class)
|
||||
// 14 57D0 resist_data -> ResistData[9] (by char_class)
|
||||
// 18 58F0 attack_data -> AttackData[9] (by char_class)
|
||||
// 1C 5AA0 unknown_a4 -> float[15][3] (by [???][attack_number])
|
||||
// 20 5B54 unknown_a5 -> float[3][3] (by [strike_number][attack_number])
|
||||
// 24 5B78 unknown_a6 -> float[3][3] (by [strike_number][attack_number]) (may be [4][3] in original code; there are 0xC zero bytes after)
|
||||
// 28 5BA8 unknown_a7 -> uint8_t[15][3] (same indexes as unknown_a4)
|
||||
// 5BD8 unknown_a9[0] -> UnknownA9[15] (index unknown; appears animation-related)
|
||||
// 30 5DF4 unknown_a9 -> u32[3] (by char_class_class)
|
||||
// 2C 5E00 area_sound_configs -> AreaSoundConfig[0x12] (by area)
|
||||
// 5E90 unknown_a10[0] -> (0x10-byte struct)[0x0C] (the rest follow immediately)
|
||||
// 34 60D0 unknown_a10 -> u32[3] (by char_class_class)
|
||||
// 60DC unknown_a11[0] -> WeaponReference[12] (the rest follow immediately)
|
||||
// 38 616C unknown_a11 -> u32[3] (by char_class_class)
|
||||
// 6178 unknown_a12[0] -> UnknownA12[15] (the rest follow immediately)
|
||||
// 3C 64FC unknown_a12 -> u32[3] (by char_class_class)
|
||||
|
||||
/* 00 / 5468 * */ le_uint32_t level_deltas;
|
||||
/* 04 / 548C * */ le_uint32_t hp_tp_factors;
|
||||
/* 08 / 54A4 * */ le_uint32_t max_stats;
|
||||
/* 0C / 55E8 * */ le_uint32_t level_100_stats;
|
||||
/* 10 / 57AC * */ le_uint32_t base_stats;
|
||||
/* 14 / 57D0 * */ le_uint32_t resist_data;
|
||||
/* 18 / 58F0 * */ le_uint32_t attack_data;
|
||||
/* 1C / 5AA0 * */ le_uint32_t unknown_a4;
|
||||
/* 20 / 5B54 * */ le_uint32_t unknown_a5;
|
||||
/* 24 / 5B78 * */ le_uint32_t unknown_a6;
|
||||
/* 28 / 5BA8 * */ le_uint32_t unknown_a7;
|
||||
/* 2C / 5E00 * */ le_uint32_t area_sound_configs;
|
||||
/* 30 / 5DF4 * */ le_uint32_t unknown_a9;
|
||||
/* 34 / 60D0 * */ le_uint32_t unknown_a10;
|
||||
/* 38 / 616C * */ le_uint32_t unknown_a11;
|
||||
/* 3C / 64FC * */ le_uint32_t unknown_a12;
|
||||
} __packed_ws__(Root, 0x40);
|
||||
|
||||
phosg::StringReader r(data);
|
||||
|
||||
const auto& footer = r.pget<RELFileFooter>(r.size() - sizeof(RELFileFooter));
|
||||
const auto& offsets = r.pget<Offsets>(footer.root_offset);
|
||||
const auto& offsets = r.pget<Root>(footer.root_offset);
|
||||
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++) {
|
||||
@@ -73,17 +140,16 @@ LevelTableV2::LevelTableV2(const string& data, bool compressed) {
|
||||
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<PlayerStats>(offsets.level_100_stats + char_class * sizeof(PlayerStats));
|
||||
this->base_stats[char_class] = r.pget<CharacterStats>(base_stats_offsets[char_class]);
|
||||
}
|
||||
}
|
||||
|
||||
const CharacterStats& LevelTableV2::base_stats_for_class(uint8_t char_class) const {
|
||||
return this->base_stats.at(char_class);
|
||||
size_t LevelTableV2::num_char_classes() const {
|
||||
return 9;
|
||||
}
|
||||
|
||||
const PlayerStats& LevelTableV2::level_100_stats_for_class(uint8_t char_class) const {
|
||||
return this->level_100_stats.at(char_class);
|
||||
const CharacterStats& LevelTableV2::base_stats_for_class(uint8_t char_class) const {
|
||||
return this->base_stats.at(char_class);
|
||||
}
|
||||
|
||||
const PlayerStats& LevelTableV2::max_stats_for_class(uint8_t char_class) const {
|
||||
@@ -94,46 +160,11 @@ const LevelStatsDelta& LevelTableV2::stats_delta_for_level(uint8_t char_class, u
|
||||
return this->level_deltas.at(char_class).at(level);
|
||||
}
|
||||
|
||||
LevelTableV3BE::LevelTableV3BE(const string& data, bool encrypted) {
|
||||
phosg::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 = phosg::StringReader(decompressed_data);
|
||||
} else {
|
||||
r = phosg::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& footer = r.pget<RELFileFooterBE>(r.size() - sizeof(RELFileFooterBE));
|
||||
const auto& offsets = r.pget<parray<be_uint32_t, 12>>(r.pget_u32b(footer.root_offset));
|
||||
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;
|
||||
}
|
||||
}
|
||||
size_t LevelTableV3::num_char_classes() const {
|
||||
return 12;
|
||||
}
|
||||
|
||||
const CharacterStats& LevelTableV3BE::base_stats_for_class(uint8_t char_class) const {
|
||||
const CharacterStats& LevelTableV3::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},
|
||||
@@ -168,31 +199,86 @@ static const array<PlayerStats, 12> max_stats_v3_v4 = {
|
||||
PlayerStats{{0x0474, 0x0407, 0x0384, 0x02CF, 0x0241, 0x06C2, 0x0064}, 0x0064, 0.0f, 0.0f, 0, 0, 0},
|
||||
};
|
||||
|
||||
const PlayerStats& LevelTableV3BE::max_stats_for_class(uint8_t char_class) const {
|
||||
const PlayerStats& LevelTableV3::max_stats_for_class(uint8_t char_class) const {
|
||||
return max_stats_v3_v4.at(char_class);
|
||||
}
|
||||
|
||||
const LevelStatsDelta& LevelTableV3BE::stats_delta_for_level(uint8_t char_class, uint8_t level) const {
|
||||
const LevelStatsDelta& LevelTableV3::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]
|
||||
} __packed_ws__(Offsets, 8);
|
||||
template <bool BE>
|
||||
void parse_level_deltas_t(std::array<std::array<LevelStatsDelta, 200>, 12>& deltas, const string& data) {
|
||||
// The V3 format is very simple:
|
||||
// root:
|
||||
// u32 offset:
|
||||
// u32[12] offsets:
|
||||
// LevelStatsDeltaBE[200] level_deltas
|
||||
phosg::StringReader r(data);
|
||||
const auto& footer = r.pget<RELFileFooterT<BE>>(r.size() - sizeof(RELFileFooterT<BE>));
|
||||
const auto& offsets = r.pget<parray<U32T<BE>, 12>>(r.pget<U32T<BE>>(footer.root_offset));
|
||||
for (size_t char_class = 0; char_class < 12; char_class++) {
|
||||
const auto& src_deltas = r.pget<parray<LevelStatsDeltaT<BE>, 200>>(offsets[char_class]);
|
||||
for (size_t level = 0; level < 200; level++) {
|
||||
deltas[char_class][level] = src_deltas[level];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
phosg::StringReader r;
|
||||
string decompressed_data;
|
||||
if (compressed) {
|
||||
decompressed_data = prs_decompress(data);
|
||||
r = phosg::StringReader(decompressed_data);
|
||||
} else {
|
||||
r = phosg::StringReader(data);
|
||||
LevelTableGC::LevelTableGC(const string& data) {
|
||||
parse_level_deltas_t<true>(this->level_deltas, data);
|
||||
}
|
||||
|
||||
LevelTableXB::LevelTableXB(const string& data) {
|
||||
parse_level_deltas_t<false>(this->level_deltas, data);
|
||||
}
|
||||
|
||||
struct RootV4 {
|
||||
le_uint32_t base_stats; // -> u32[12] -> CharacterStats
|
||||
le_uint32_t level_deltas; // -> u32[12] -> LevelStatsDelta[200]
|
||||
} __packed_ws__(RootV4, 8);
|
||||
|
||||
std::string LevelTable::serialize_binary_v4() const {
|
||||
RELFileWriter<false> rel;
|
||||
RootV4 root;
|
||||
|
||||
{
|
||||
std::vector<uint32_t> offsets;
|
||||
for (size_t char_class = 0; char_class < this->num_char_classes(); char_class++) {
|
||||
offsets.emplace_back(rel.put<CharacterStats>(this->base_stats_for_class(char_class)));
|
||||
}
|
||||
root.base_stats = rel.w.size();
|
||||
for (uint32_t offset : offsets) {
|
||||
rel.write_offset(offset);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::vector<uint32_t> offsets;
|
||||
for (size_t char_class = 0; char_class < this->num_char_classes(); char_class++) {
|
||||
offsets.emplace_back(rel.w.size());
|
||||
for (size_t level = 0; level < 200; level++) {
|
||||
rel.put<LevelStatsDelta>(this->stats_delta_for_level(char_class, level));
|
||||
}
|
||||
}
|
||||
root.level_deltas = rel.w.size();
|
||||
for (uint32_t offset : offsets) {
|
||||
rel.write_offset(offset);
|
||||
}
|
||||
}
|
||||
|
||||
size_t root_offset = rel.put<RootV4>(root);
|
||||
rel.relocations.emplace(root_offset);
|
||||
rel.relocations.emplace(root_offset + 4);
|
||||
|
||||
return rel.finalize(root_offset);
|
||||
}
|
||||
|
||||
LevelTableV4::LevelTableV4(const string& data) {
|
||||
|
||||
phosg::StringReader r(data);
|
||||
const auto& footer = r.pget<RELFileFooter>(r.size() - sizeof(RELFileFooter));
|
||||
const auto& offsets = r.pget<Offsets>(footer.root_offset);
|
||||
const auto& offsets = r.pget<RootV4>(footer.root_offset);
|
||||
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++) {
|
||||
@@ -204,6 +290,10 @@ LevelTableV4::LevelTableV4(const string& data, bool compressed) {
|
||||
}
|
||||
}
|
||||
|
||||
size_t LevelTableV4::num_char_classes() const {
|
||||
return 12;
|
||||
}
|
||||
|
||||
const CharacterStats& LevelTableV4::base_stats_for_class(uint8_t char_class) const {
|
||||
return this->base_stats.at(char_class);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user