add level table JSON format

This commit is contained in:
Martin Michelsen
2026-05-09 13:36:33 -07:00
parent 9915422ae6
commit 7ce3ce5b65
46 changed files with 7462 additions and 398 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ void BattleParamsIndex::Table::print(FILE* stream, Episode episode) const {
phosg::fwrite_fmt(stream,
"{:5} {:5} {:5} {:5} {:5} {:5} {:5} {:5} {:5} {:5} {}",
e.char_stats.atp, e.char_stats.mst, e.char_stats.evp, e.char_stats.hp, e.char_stats.dfp, e.char_stats.ata,
e.char_stats.lck, e.esp, e.experience, e.meseta, names_str);
e.char_stats.lck, e.esp, e.exp, e.meseta, names_str);
fputc('\n', stream);
}
}
+2 -2
View File
@@ -540,7 +540,7 @@ static asio::awaitable<void> server_command_bbchar_savechar(const Args& a, bool
bb_player->disp.stats.char_stats.dfp += bb_player->get_material_usage(PSOBBCharacterFile::MaterialType::DEF) * 2;
bb_player->disp.stats.char_stats.lck += bb_player->get_material_usage(PSOBBCharacterFile::MaterialType::LUCK) * 2;
bb_player->disp.stats.char_stats.hp += bb_player->get_material_usage(PSOBBCharacterFile::MaterialType::HP) * 2;
bb_player->disp.stats.experience = ch.character->disp.stats.experience;
bb_player->disp.stats.exp = ch.character->disp.stats.exp;
bb_player->disp.stats.meseta = ch.character->disp.stats.meseta;
}
bb_player->disp.technique_levels_v1 = ch.character->disp.technique_levels_v1;
@@ -915,7 +915,7 @@ ChatCommandDefinition cc_edit(
} else if (tokens.at(0) == "meseta" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
p->disp.stats.meseta = stoul(tokens.at(1));
} else if (tokens.at(0) == "exp" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
p->disp.stats.experience = stoul(tokens.at(1));
p->disp.stats.exp = stoul(tokens.at(1));
} else if (tokens.at(0) == "level" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
p->disp.stats.level = stoul(tokens.at(1)) - 1;
p->recompute_stats(s->level_table(a.c->version()), true);
+1 -1
View File
@@ -703,7 +703,7 @@ void Client::create_challenge_overlay(
overlay->disp.visual.char_class, overlay->disp.stats.level);
overlay->disp.stats.esp = 40;
overlay->disp.stats.attack_range = 10.0;
overlay->disp.stats.experience = stats_delta.experience;
overlay->disp.stats.exp = stats_delta.exp;
overlay->disp.stats.meseta = 0;
overlay->clear_all_material_usage();
for (size_t z = 0; z < 0x13; z++) {
+3 -1
View File
@@ -4269,7 +4269,9 @@ struct G_Attack_6x43_6x44_6x45 {
le_uint16_t unknown_a2 = 0;
} __packed_ws__(G_Attack_6x43_6x44_6x45, 8);
// 6x46: Attack finished (sent after each of 43, 44, and 45) (protected on GC NTE/V3/V4)
// 6x46: Set attack strike targets (sent after each of 43, 44, and 45) (protected on GC NTE/V3/V4)
// This command sets the targets of each strike of an attack (e.g. each pair of mechgun bullets, or each swing of a
// pair of daggers). For multi-strike attacks, this is sent multiple times.
// The number of targets is not bounds-checked during byteswapping on GC clients. The client only expects up to 10
// entries here, so if the number of targets is too large, the client will byteswap the function's return address on
// the stack, and it will crash.
+76
View File
@@ -197,3 +197,79 @@ size_t get_rel_array_count(const std::set<uint32_t>& offsets, size_t start_offse
}
return (*it - start_offset) / sizeof(T);
}
template <bool BE>
class RELFileWriter {
public:
RELFileWriter() = default;
~RELFileWriter() = default;
template <typename T>
uint32_t put(const T& obj) {
uint32_t ret = this->w.size();
this->w.put<T>(obj);
return ret;
}
uint32_t write(const void* data, size_t size) {
uint32_t ret = this->w.size();
this->w.write(data, size);
return ret;
}
uint32_t write(const std::string& data) {
uint32_t ret = this->w.size();
this->w.write(data);
return ret;
}
uint32_t write_offset(uint32_t value) {
uint32_t ret = this->w.size();
this->relocations.emplace(ret);
this->w.put<U32T<BE>>(value);
return ret;
}
uint32_t write_ref(const ArrayRefT<BE>& ref) {
uint32_t ret = this->w.size();
this->w.put<ArrayRefT<BE>>(ref);
this->relocations.emplace(ret + offsetof(ArrayRefT<BE>, offset));
return ret;
}
void align(size_t alignment) {
while (this->w.size() & (alignment - 1)) {
this->w.put_u8(0);
}
}
std::string finalize(uint32_t root_offset) {
RELFileFooterT<BE> footer;
footer.root_offset = root_offset;
this->align(0x20);
footer.relocations_offset = this->w.size();
footer.num_relocations = this->relocations.size();
footer.unused1[0] = 1;
uint32_t last_offset = 0;
for (uint32_t reloc_offset : this->relocations) {
if (reloc_offset & 3) {
throw std::logic_error("Relocation is not 4-byte aligned");
}
size_t reloc_value = (reloc_offset - last_offset) >> 2;
if (reloc_value > 0xFFFF) {
throw std::runtime_error("Relocation offset is too far away from previous");
}
this->w.put<U16T<BE>>(reloc_value);
last_offset = reloc_offset;
}
align(0x20);
this->w.put<RELFileFooterT<BE>>(footer);
return std::move(this->w.str());
}
phosg::StringWriter w;
std::set<uint32_t> relocations;
};
+1 -1
View File
@@ -152,7 +152,7 @@ HTTPServer::HTTPServer(shared_ptr<ServerState> state)
client_json.emplace("DFP", p->disp.stats.char_stats.dfp.load());
client_json.emplace("ATA", p->disp.stats.char_stats.ata.load());
client_json.emplace("LCK", p->disp.stats.char_stats.lck.load());
client_json.emplace("EXP", p->disp.stats.experience.load());
client_json.emplace("EXP", p->disp.stats.exp.load());
client_json.emplace("Meseta", p->disp.stats.meseta.load());
auto tech_levels_json = phosg::JSON::dict();
for (size_t z = 0; z < 0x13; z++) {
+98 -148
View File
@@ -2913,224 +2913,204 @@ public:
}
static std::string serialize(const ItemParameterTable& pmt) {
set<uint32_t> relocations;
RELFileWriter<BE> rel;
RootT root;
phosg::StringWriter w;
if constexpr (!std::is_same_v<HeaderT, EmptyHeader>) {
w.put<HeaderT>(HeaderT());
rel.template put<HeaderT>(HeaderT());
}
auto align = [&w](size_t alignment) -> void {
while (w.size() & (alignment - 1)) {
w.put_u8(0);
}
};
auto write_ref = [&w, &relocations](const ArrayRefT<BE>& ref) -> void {
w.put<ArrayRefT<BE>>(ref);
relocations.emplace(w.size() - 4);
};
if constexpr (requires { root.entry_count; }) {
root.entry_count = 0x13;
}
align(4);
ArrayRefT<BE> shields_ref{pmt.num_armors_or_shields_in_class(2) - HasImplicitPlaceholders, w.size()};
rel.align(4);
ArrayRefT<BE> shields_ref{pmt.num_armors_or_shields_in_class(2) - HasImplicitPlaceholders, rel.w.size()};
for (size_t data1_2 = 0; data1_2 < (shields_ref.count + HasImplicitPlaceholders); data1_2++) {
w.put<ArmorOrShieldT>(pmt.get_armor_or_shield(2, data1_2));
rel.template put<ArmorOrShieldT>(pmt.get_armor_or_shield(2, data1_2));
}
if constexpr (requires { root.shield_stat_boost_index_table; }) {
root.shield_stat_boost_index_table = w.size();
w.write(pmt.get_shield_stat_boost_index_table());
root.shield_stat_boost_index_table = rel.write(pmt.get_shield_stat_boost_index_table());
}
align(4);
ArrayRefT<BE> armors_ref{pmt.num_armors_or_shields_in_class(1) - HasImplicitPlaceholders, w.size()};
rel.align(4);
ArrayRefT<BE> armors_ref{pmt.num_armors_or_shields_in_class(1) - HasImplicitPlaceholders, rel.w.size()};
for (size_t data1_2 = 0; data1_2 < (armors_ref.count + HasImplicitPlaceholders); data1_2++) {
w.put<ArmorOrShieldT>(pmt.get_armor_or_shield(1, data1_2));
rel.template put<ArmorOrShieldT>(pmt.get_armor_or_shield(1, data1_2));
}
if constexpr (requires { root.armor_stat_boost_index_table; }) {
root.armor_stat_boost_index_table = w.size();
w.write(pmt.get_armor_stat_boost_index_table());
root.armor_stat_boost_index_table = rel.write(pmt.get_armor_stat_boost_index_table());
}
align(4);
ArrayRefT<BE> units_ref{pmt.num_units() - HasImplicitPlaceholders, w.size()};
rel.align(4);
ArrayRefT<BE> units_ref{pmt.num_units() - HasImplicitPlaceholders, rel.w.size()};
for (size_t data1_2 = 0; data1_2 < (units_ref.count + HasImplicitPlaceholders); data1_2++) {
w.put<UnitT>(pmt.get_unit(data1_2));
rel.template put<UnitT>(pmt.get_unit(data1_2));
}
align(4);
ArrayRefT<BE> mags_ref{pmt.num_mags() - HasImplicitPlaceholders, w.size()};
rel.align(4);
ArrayRefT<BE> mags_ref{pmt.num_mags() - HasImplicitPlaceholders, rel.w.size()};
for (size_t data1_2 = 0; data1_2 < (mags_ref.count + HasImplicitPlaceholders); data1_2++) {
w.put<MagT>(pmt.get_mag(data1_2));
rel.template put<MagT>(pmt.get_mag(data1_2));
}
align(4);
rel.align(4);
std::vector<ArrayRefT<BE>> tool_refs;
for (size_t data1_1 = 0; data1_1 < pmt.num_tool_classes(); data1_1++) {
auto& ref = tool_refs.emplace_back(ArrayRefT<BE>{pmt.num_tools_in_class(data1_1), w.size()});
auto& ref = tool_refs.emplace_back(ArrayRefT<BE>{pmt.num_tools_in_class(data1_1), rel.w.size()});
for (size_t data1_2 = 0; data1_2 < ref.count; data1_2++) {
w.put<ToolT>(pmt.get_tool(data1_1, data1_2));
rel.template put<ToolT>(pmt.get_tool(data1_1, data1_2));
}
}
align(4);
rel.align(4);
std::vector<ArrayRefT<BE>> weapon_refs;
for (size_t data1_1 = 0; data1_1 < pmt.num_weapon_classes(); data1_1++) {
auto& ref = weapon_refs.emplace_back(ArrayRefT<BE>{pmt.num_weapons_in_class(data1_1), w.size()});
auto& ref = weapon_refs.emplace_back(ArrayRefT<BE>{pmt.num_weapons_in_class(data1_1), rel.w.size()});
for (size_t data1_2 = 0; data1_2 < ref.count; data1_2++) {
w.put<WeaponT>(pmt.get_weapon(data1_1, data1_2));
rel.template put<WeaponT>(pmt.get_weapon(data1_1, data1_2));
}
}
if constexpr (requires { root.weapon_stat_boost_index_table; }) {
root.weapon_stat_boost_index_table = w.size();
w.write(pmt.get_weapon_stat_boost_index_table());
root.weapon_stat_boost_index_table = rel.write(pmt.get_weapon_stat_boost_index_table());
}
align(4);
root.photon_color_table = w.size();
rel.align(4);
root.photon_color_table = rel.w.size();
for (size_t z = 0; z < pmt.num_photon_colors(); z++) {
w.put<PhotonColorEntryT<BE>>(pmt.get_photon_color(z));
rel.template put<PhotonColorEntryT<BE>>(pmt.get_photon_color(z));
}
align(4);
root.weapon_range_table = w.size();
rel.align(4);
root.weapon_range_table = rel.w.size();
for (size_t z = 0; z < pmt.num_weapon_ranges(); z++) {
w.put<WeaponRangeT<BE>>(pmt.get_weapon_range(z));
rel.template put<WeaponRangeT<BE>>(pmt.get_weapon_range(z));
}
root.weapon_kind_table = w.size();
root.weapon_kind_table = rel.w.size();
for (size_t z = 0; z < pmt.num_weapon_classes(); z++) {
w.put_u8(pmt.get_weapon_kind(z));
rel.w.put_u8(pmt.get_weapon_kind(z));
}
if constexpr (requires { root.weapon_integral_sale_divisor_table; }) {
root.weapon_integral_sale_divisor_table = w.size();
root.weapon_integral_sale_divisor_table = rel.w.size();
for (size_t z = 0; z < pmt.num_weapon_classes(); z++) {
w.put_u8(pmt.get_sale_divisor(0, z));
rel.w.put_u8(pmt.get_sale_divisor(0, z));
}
} else {
align(4);
root.weapon_sale_divisor_table = w.size();
rel.align(4);
root.weapon_sale_divisor_table = rel.w.size();
for (size_t z = 0; z < pmt.num_weapon_sale_divisors(); z++) {
w.put<F32T<BE>>(pmt.get_sale_divisor(0, z));
rel.template put<F32T<BE>>(pmt.get_sale_divisor(0, z));
}
}
if constexpr (requires { root.non_weapon_integral_sale_divisor_table; }) {
root.non_weapon_integral_sale_divisor_table = w.size();
NonWeaponSaleDivisorsDCProtos sds;
sds.armor_divisor = pmt.get_sale_divisor(1, 1);
sds.shield_divisor = pmt.get_sale_divisor(1, 2);
sds.unit_divisor = pmt.get_sale_divisor(1, 3);
w.put<NonWeaponSaleDivisorsDCProtos>(sds);
root.non_weapon_integral_sale_divisor_table = rel.template put<NonWeaponSaleDivisorsDCProtos>(sds);
} else {
align(4);
root.non_weapon_sale_divisor_table = w.size();
rel.align(4);
NonWeaponSaleDivisorsT<BE> sds;
sds.armor_divisor = pmt.get_sale_divisor(1, 1);
sds.shield_divisor = pmt.get_sale_divisor(1, 2);
sds.unit_divisor = pmt.get_sale_divisor(1, 3);
sds.mag_divisor = pmt.get_sale_divisor(2, 0);
w.put<NonWeaponSaleDivisorsT<BE>>(sds);
root.non_weapon_sale_divisor_table = rel.template put<NonWeaponSaleDivisorsT<BE>>(sds);
}
MagFeedResultsListOffsetsT<BE> mag_feed_result_offsets;
for (size_t table_index = 0; table_index < 8; table_index++) {
mag_feed_result_offsets[table_index] = w.size();
mag_feed_result_offsets[table_index] = rel.w.size();
for (size_t item_id = 0; item_id < 11; item_id++) {
w.put<MagFeedResult>(pmt.get_mag_feed_result(table_index, item_id));
rel.template put<MagFeedResult>(pmt.get_mag_feed_result(table_index, item_id));
}
}
root.star_value_table = w.size();
w.write(pmt.get_star_value_table());
root.star_value_table = rel.write(pmt.get_star_value_table());
if constexpr (requires { root.unknown_a1; }) {
align(2);
root.unknown_a1 = w.size();
w.write(pmt.get_unknown_a1());
rel.align(2);
root.unknown_a1 = rel.write(pmt.get_unknown_a1());
}
align(2);
root.special_table = w.size();
rel.align(2);
root.special_table = rel.w.size();
for (size_t z = 0; z < pmt.num_specials(); z++) {
w.put<SpecialT<BE>>(pmt.get_special(z));
rel.template put<SpecialT<BE>>(pmt.get_special(z));
}
align(4);
root.weapon_effect_table = w.size();
rel.align(4);
root.weapon_effect_table = rel.w.size();
for (size_t z = 0; z < pmt.num_weapon_effects(); z++) {
w.put<WeaponEffectT<BE>>(pmt.get_weapon_effect(z));
rel.template put<WeaponEffectT<BE>>(pmt.get_weapon_effect(z));
}
align(4);
rel.align(4);
if constexpr (requires { root.shield_effect_table; }) {
root.shield_effect_table = w.size();
root.shield_effect_table = rel.w.size();
for (size_t z = 0; z < pmt.num_shield_effects(); z++) {
w.put<ShieldEffectT<BE>>(pmt.get_shield_effect(z));
rel.template put<ShieldEffectT<BE>>(pmt.get_shield_effect(z));
}
}
align(4);
rel.align(4);
if constexpr (requires { root.sound_remap_table; }) {
std::vector<SoundRemapTableOffsetsT<BE>> remap_refs;
const auto& remaps = pmt.get_all_sound_remaps();
for (const auto& remap : remaps) {
auto& remap_ref = remap_refs.emplace_back();
remap_ref.sound_id = remap.sound_id;
remap_ref.remaps_for_rt_index_table = w.size();
remap_ref.remaps_for_rt_index_table = rel.w.size();
for (uint32_t remap_sound_id : remap.by_rt_index) {
w.put<U32T<BE>>(remap_sound_id);
rel.template put<U32T<BE>>(remap_sound_id);
}
remap_ref.remaps_for_char_class_table = w.size();
remap_ref.remaps_for_char_class_table = rel.w.size();
for (uint32_t remap_sound_id : remap.by_char_class) {
w.put<U32T<BE>>(remap_sound_id);
rel.template put<U32T<BE>>(remap_sound_id);
}
}
ArrayRefT<BE> remap_vec{remaps.size(), w.size()};
ArrayRefT<BE> remap_vec{remaps.size(), rel.w.size()};
for (const auto& remap_ref : remap_refs) {
w.put<SoundRemapTableOffsetsT<BE>>(remap_ref);
relocations.emplace(w.size() - 8);
relocations.emplace(w.size() - 4);
uint32_t offset = rel.template put<SoundRemapTableOffsetsT<BE>>(remap_ref);
rel.relocations.emplace(offset + 4);
rel.relocations.emplace(offset + 8);
}
root.sound_remap_table = w.size();
write_ref(remap_vec);
root.sound_remap_table = rel.write_ref(remap_vec);
}
align(4);
root.stat_boost_table = w.size();
rel.align(4);
root.stat_boost_table = rel.w.size();
for (size_t z = 0; z < pmt.num_stat_boosts(); z++) {
w.put<StatBoostT<BE>>(pmt.get_stat_boost(z));
rel.template put<StatBoostT<BE>>(pmt.get_stat_boost(z));
}
if constexpr (requires { root.max_tech_level_table; }) {
root.max_tech_level_table = w.size();
MaxTechniqueLevels max_tech_levels;
for (size_t tech_num = 0; tech_num < 0x13; tech_num++) {
for (size_t char_class = 0; char_class < 0x0C; char_class++) {
max_tech_levels[tech_num][char_class] = pmt.get_max_tech_level(char_class, tech_num);
}
}
w.put<MaxTechniqueLevels>(max_tech_levels);
root.max_tech_level_table = rel.template put<MaxTechniqueLevels>(max_tech_levels);
}
ArrayRefT<BE> combination_table_ref;
if constexpr (requires { root.combination_table; }) {
combination_table_ref.offset = w.size();
combination_table_ref.offset = rel.w.size();
combination_table_ref.count = pmt.num_item_combinations();
for (size_t z = 0; z < combination_table_ref.count; z++) {
w.put<ItemCombination>(pmt.get_item_combination(z));
rel.template put<ItemCombination>(pmt.get_item_combination(z));
}
}
if constexpr (requires { root.tech_boost_table; }) {
align(4);
root.tech_boost_table = w.size();
rel.align(4);
root.tech_boost_table = rel.w.size();
for (size_t z = 0; z < pmt.num_tech_boosts(); z++) {
w.put<TechBoostT<BE>>(pmt.get_tech_boost(z));
rel.template put<TechBoostT<BE>>(pmt.get_tech_boost(z));
}
}
@@ -3138,8 +3118,8 @@ public:
if constexpr (requires { root.unwrap_table; }) {
for (size_t event = 0; event < pmt.num_events(); event++) {
auto [event_items, num_items] = pmt.get_event_items(event);
unwrap_table_refs.emplace_back(ArrayRefT<BE>{num_items, w.size()});
w.write(event_items, sizeof(EventItem) * num_items);
unwrap_table_refs.emplace_back(ArrayRefT<BE>{num_items, rel.w.size()});
rel.write(event_items, sizeof(EventItem) * num_items);
}
}
@@ -3147,95 +3127,65 @@ public:
if constexpr (requires { root.unsealable_table; }) {
const auto& items = pmt.all_unsealable_items();
unsealable_table_ref.count = items.size();
unsealable_table_ref.offset = w.size();
unsealable_table_ref.offset = rel.w.size();
for (const auto& item : items) {
UnsealableItem encoded;
u32_to_item_code(encoded.item, item);
w.put<UnsealableItem>(encoded);
rel.template put<UnsealableItem>(encoded);
}
}
ArrayRefT<BE> ranged_specials_ref;
if constexpr (requires { root.ranged_special_table; }) {
ranged_specials_ref.count = pmt.num_ranged_specials();
ranged_specials_ref.offset = w.size();
ranged_specials_ref.offset = rel.w.size();
for (size_t z = 0; z < ranged_specials_ref.count; z++) {
w.put<RangedSpecial>(pmt.get_ranged_special(z));
rel.template put<RangedSpecial>(pmt.get_ranged_special(z));
}
}
align(4);
root.armor_table = w.size();
write_ref(armors_ref);
write_ref(shields_ref);
root.unit_table = w.size();
write_ref(units_ref);
root.mag_table = w.size();
write_ref(mags_ref);
root.tool_table = w.size();
rel.align(4);
root.armor_table = rel.write_ref(armors_ref);
rel.write_ref(shields_ref);
root.unit_table = rel.write_ref(units_ref);
root.mag_table = rel.write_ref(mags_ref);
root.tool_table = rel.w.size();
for (const auto& ref : tool_refs) {
write_ref(ref);
rel.write_ref(ref);
}
root.weapon_table = w.size();
root.weapon_table = rel.w.size();
for (const auto& ref : weapon_refs) {
write_ref(ref);
rel.write_ref(ref);
}
if constexpr (requires { root.combination_table; }) {
root.combination_table = w.size();
write_ref(combination_table_ref);
root.combination_table = rel.write_ref(combination_table_ref);
}
if constexpr (requires { root.unwrap_table; }) {
ArrayRefT<BE> event_ref{unwrap_table_refs.size(), w.size()};
ArrayRefT<BE> event_ref{unwrap_table_refs.size(), rel.w.size()};
for (const auto& ref : unwrap_table_refs) {
write_ref(ref);
rel.write_ref(ref);
}
root.unwrap_table = w.size();
write_ref(event_ref);
root.unwrap_table = rel.write_ref(event_ref);
}
if constexpr (requires { root.unsealable_table; }) {
root.unsealable_table = w.size();
write_ref(unsealable_table_ref);
root.unsealable_table = rel.write_ref(unsealable_table_ref);
}
if constexpr (requires { root.ranged_special_table; }) {
root.ranged_special_table = w.size();
write_ref(ranged_specials_ref);
root.ranged_special_table = rel.write_ref(ranged_specials_ref);
}
root.mag_feed_table = w.size();
w.put<MagFeedResultsListOffsetsT<BE>>(mag_feed_result_offsets);
root.mag_feed_table = rel.template put<MagFeedResultsListOffsetsT<BE>>(mag_feed_result_offsets);
for (size_t z = 1; z <= 8; z++) {
relocations.emplace(w.size() - (z * 4));
rel.relocations.emplace(rel.w.size() - (z * 4));
}
RELFileFooterT<BE> footer;
footer.root_offset = w.size();
w.put<RootT>(root);
uint32_t root_offset = rel.template put<RootT>(root);
constexpr size_t root_field_count = (sizeof(RootT) / 4) - ((requires { root.entry_count; }) ? 1 : 0);
for (size_t z = 1; z <= root_field_count; z++) {
relocations.emplace(w.size() - (z * 4));
rel.relocations.emplace(rel.w.size() - (z * 4));
}
align(0x20);
footer.relocations_offset = w.size();
footer.num_relocations = relocations.size();
footer.unused1[0] = 1;
uint32_t last_offset = 0;
for (uint32_t reloc_offset : relocations) {
if (reloc_offset & 3) {
throw logic_error("Relocation is not 4-byte aligned");
}
size_t reloc_value = (reloc_offset - last_offset) >> 2;
if (reloc_value > 0xFFFF) {
throw runtime_error("Relocation offset is too far away from previous");
}
w.put<U16T<BE>>(reloc_value);
last_offset = reloc_offset;
}
align(0x20);
w.put<RELFileFooterT<BE>>(footer);
return std::move(w.str());
return rel.finalize(root_offset);
}
protected:
+182 -92
View File
@@ -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);
}
+168 -41
View File
@@ -21,17 +21,34 @@ struct CharacterStatsT {
/* 0A */ U16T<BE> ata = 0;
/* 0C */ U16T<BE> lck = 0;
/* 0E */
static CharacterStatsT<BE> from_json(const phosg::JSON& json) {
return CharacterStatsT<BE>{
json.at("ATP").as_int(),
json.at("MST").as_int(),
json.at("EVP").as_int(),
json.at("HP").as_int(),
json.at("DFP").as_int(),
json.at("ATA").as_int(),
json.at("LCK").as_int()};
}
phosg::JSON json() const {
return phosg::JSON::dict({{"ATP", this->atp.load()},
{"MST", this->mst.load()},
{"EVP", this->evp.load()},
{"HP", this->hp.load()},
{"DFP", this->dfp.load()},
{"ATA", this->ata.load()},
{"LCK", this->lck.load()}});
}
operator CharacterStatsT<!BE>() const {
CharacterStatsT<!BE> ret;
ret.atp = this->atp;
ret.mst = this->mst;
ret.evp = this->evp;
ret.hp = this->hp;
ret.dfp = this->dfp;
ret.ata = this->ata;
ret.lck = this->lck;
return ret;
return CharacterStatsT<!BE>{
this->atp.load(),
this->mst.load(),
this->evp.load(),
this->hp.load(),
this->dfp.load(),
this->ata.load(),
this->lck.load()};
}
} __packed_ws_be__(CharacterStatsT, 0x0E);
using CharacterStats = CharacterStatsT<false>;
@@ -44,37 +61,82 @@ struct PlayerStatsT {
/* 10 */ F32T<BE> attack_range = 0.0;
/* 14 */ F32T<BE> knockback_range = 0.0;
/* 18 */ U32T<BE> level = 0; // Qedit specifies this as tech level when used for enemies
/* 1C */ U32T<BE> experience = 0;
/* 1C */ U32T<BE> exp = 0;
/* 20 */ U32T<BE> meseta = 0; // Qedit specifies this as TP when used for enemies
/* 24 */
operator PlayerStatsT<!BE>() const {
PlayerStatsT<!BE> ret;
ret.char_stats = this->char_stats;
ret.esp = this->esp;
ret.attack_range = this->attack_range;
ret.knockback_range = this->knockback_range;
ret.level = this->level;
ret.experience = this->experience;
ret.meseta = this->meseta;
static PlayerStatsT<BE> from_json(const phosg::JSON& json) {
return PlayerStatsT<BE>{
CharacterStatsT<BE>::from_json(json),
json.at("ESP").as_int(),
json.at("AttackRange").as_float(),
json.at("KnockbackRange").as_float(),
json.at("Level").as_int(),
json.at("EXP").as_int(),
json.at("Meseta").as_int()};
}
phosg::JSON json() const {
auto ret = this->char_stats.json();
ret.emplace("ESP", this->esp.load());
ret.emplace("AttackRange", this->attack_range.load());
ret.emplace("KnockbackRange", this->knockback_range.load());
ret.emplace("Level", this->level.load());
ret.emplace("EXP", this->exp.load());
ret.emplace("Meseta", this->meseta.load());
return ret;
}
operator PlayerStatsT<!BE>() const {
return PlayerStatsT<!BE>{
this->char_stats,
this->esp.load(),
this->attack_range.load(),
this->knockback_range.load(),
this->level.load(),
this->exp.load(),
this->meseta.load()};
}
} __packed_ws_be__(PlayerStatsT, 0x24);
using PlayerStats = PlayerStatsT<false>;
using PlayerStatsBE = PlayerStatsT<true>;
template <bool BE>
struct LevelStatsDeltaT {
/* 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<BE> experience;
/* 00 */ uint8_t atp = 0;
/* 01 */ uint8_t mst = 0;
/* 02 */ uint8_t evp = 0;
/* 03 */ uint8_t hp = 0;
/* 04 */ uint8_t dfp = 0;
/* 05 */ uint8_t ata = 0;
/* 06 */ uint8_t lck = 0;
/* 07 */ uint8_t tp = 0;
/* 08 */ U32T<BE> exp = 0;
/* 0C */
static LevelStatsDeltaT<BE> from_json(const phosg::JSON& json) {
return LevelStatsDeltaT<BE>{
static_cast<uint8_t>(json.at("ATP").as_int()),
static_cast<uint8_t>(json.at("MST").as_int()),
static_cast<uint8_t>(json.at("EVP").as_int()),
static_cast<uint8_t>(json.at("HP").as_int()),
static_cast<uint8_t>(json.at("DFP").as_int()),
static_cast<uint8_t>(json.at("ATA").as_int()),
static_cast<uint8_t>(json.at("LCK").as_int()),
static_cast<uint8_t>(json.at("TP").as_int()),
static_cast<uint32_t>(json.at("EXP").as_int())};
}
phosg::JSON json() const {
return phosg::JSON::dict({{"ATP", this->atp},
{"MST", this->mst},
{"EVP", this->evp},
{"HP", this->hp},
{"DFP", this->dfp},
{"ATA", this->ata},
{"LCK", this->lck},
{"TP", this->tp},
{"EXP", this->exp.load()}});
}
operator LevelStatsDeltaT<!BE>() const {
return LevelStatsDeltaT<!BE>{
this->atp, this->mst, this->evp, this->hp, this->dfp, this->ata, this->lck, this->tp, this->exp.load()};
}
void apply(CharacterStats& ps) const {
ps.ata += this->ata;
@@ -95,6 +157,8 @@ class LevelTable {
// Offsets structures inside the subclasses' constructor implementations for more details on the file formats.
public:
virtual ~LevelTable() = default;
virtual size_t num_char_classes() const = 0;
virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const = 0;
virtual const PlayerStats& max_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;
@@ -102,50 +166,113 @@ public:
void reset_to_base(PlayerStats& stats, uint8_t char_class) const;
void advance_to_level(PlayerStats& stats, uint32_t level, uint8_t char_class) const;
std::string serialize_binary_v4() const;
phosg::JSON json() const;
protected:
LevelTable() = default;
};
class LevelTableV2 : public LevelTable { // from PlayerTable.prs (PC)
class JSONLevelTable : public LevelTable {
public:
LevelTableV2(const std::string& data, bool compressed);
virtual ~LevelTableV2() = default;
JSONLevelTable(const phosg::JSON& json);
virtual ~JSONLevelTable() = default;
virtual size_t num_char_classes() const;
virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const;
const PlayerStats& level_100_stats_for_class(uint8_t char_class) const;
virtual 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:
std::vector<CharacterStats> base_stats;
std::vector<PlayerStats> max_stats;
std::vector<std::array<LevelStatsDelta, 200>> level_deltas;
};
class LevelTableV2 : public LevelTable { // from PlayerTable.prs (PC)
public:
struct HPTPFactors {
le_float hp_factor;
le_float tp_factor;
} __packed_ws__(HPTPFactors, 8);
struct UnknownA9 {
le_float unknown_a1;
le_float unknown_a2;
le_float unknown_a3;
} __packed_ws__(UnknownA9, 0x0C);
struct AreaSoundConfig {
le_uint16_t step_sound;
le_uint16_t grass_step_sound;
le_uint16_t water_step_sound;
parray<uint8_t, 2> unused;
} __packed_ws__(AreaSoundConfig, 8);
struct WeaponReference {
le_uint16_t data1_1;
le_uint16_t data1_2;
} __packed_ws__(WeaponReference, 4);
struct UnknownA12 {
le_float unknown_a1;
le_float unknown_a2;
le_float unknown_a3;
le_float unknown_a4;
le_uint32_t unknown_a5;
} __packed_ws__(UnknownA12, 0x14);
explicit LevelTableV2(const std::string& data);
virtual ~LevelTableV2() = default;
virtual size_t num_char_classes() const;
virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const;
virtual 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;
protected:
std::array<CharacterStats, 9> base_stats;
std::array<PlayerStats, 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)
class LevelTableV3 : public LevelTable { // from PlyLevelTbl.cpt (GC/XB)
public:
LevelTableV3BE(const std::string& data, bool encrypted);
virtual ~LevelTableV3BE() = default;
virtual ~LevelTableV3() = default;
virtual size_t num_char_classes() const;
virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const;
virtual 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:
protected:
LevelTableV3() = default;
std::array<std::array<LevelStatsDelta, 200>, 12> level_deltas;
};
class LevelTableGC : public LevelTableV3 {
public:
explicit LevelTableGC(const std::string& data);
virtual ~LevelTableGC() = default;
};
class LevelTableXB : public LevelTableV3 {
public:
explicit LevelTableXB(const std::string& data);
virtual ~LevelTableXB() = default;
};
class LevelTableV4 : public LevelTable { // from PlyLevelTbl.prs (BB)
public:
LevelTableV4(const std::string& data, bool compressed);
explicit LevelTableV4(const std::string& data);
virtual ~LevelTableV4() = default;
virtual size_t num_char_classes() const;
virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const;
virtual 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:
protected:
std::array<CharacterStats, 12> base_stats;
std::array<std::array<LevelStatsDelta, 200>, 12> level_deltas;
};
+44 -3
View File
@@ -2429,6 +2429,50 @@ Action a_encode_item_parameter_table(
write_output_data(args, data.data(), data.size(), nullptr);
});
Action a_decode_level_table(
"decode-level-table", nullptr,
+[](phosg::Arguments& args) {
auto input_data = read_input_data(args);
std::shared_ptr<LevelTable> table;
bool decompressed = args.get<bool>("decompressed");
switch (get_cli_version(args)) {
case Version::PC_V2:
table = std::make_shared<LevelTableV2>(decompressed ? input_data : prs_decompress(input_data));
break;
case Version::GC_V3:
table = std::make_shared<LevelTableGC>(
decompressed ? input_data : decrypt_and_decompress_pr2_data<true>(input_data));
break;
case Version::XB_V3:
table = std::make_shared<LevelTableXB>(
decompressed ? input_data : decrypt_and_decompress_pr2_data<false>(input_data));
break;
case Version::BB_V4:
table = std::make_shared<LevelTableV4>(decompressed ? input_data : prs_decompress(input_data));
break;
default:
throw std::runtime_error("This version does not have a level table");
}
auto json = table->json();
uint32_t serialize_options = phosg::JSON::SerializeOption::FORMAT | phosg::JSON::SerializeOption::SORT_DICT_KEYS;
if (args.get<bool>("hex")) {
serialize_options |= phosg::JSON::SerializeOption::HEX_INTEGERS;
}
string json_data = json.serialize(serialize_options);
write_output_data(args, json_data.data(), json_data.size(), nullptr);
});
Action a_encode_level_table(
"encode-level-table-v4", nullptr,
+[](phosg::Arguments& args) {
JSONLevelTable table(phosg::JSON::parse(read_input_data(args)));
string data = table.serialize_binary_v4();
if (!args.get<bool>("decompressed")) {
data = prs_compress_optimal(data);
}
write_output_data(args, data.data(), data.size(), nullptr);
});
Action a_find_rel_sectionr(
"find-rel-sections", nullptr,
+[](phosg::Arguments& args) {
@@ -2616,7 +2660,6 @@ Action a_print_level_stats(
vector<PlayerStats> level_1_v1_v2;
vector<PlayerStats> level_100_v1_v2;
vector<PlayerStats> level_100_limit_v1_v2;
vector<PlayerStats> level_200_v1_v2;
vector<PlayerStats> level_200_limit_v1_v2;
vector<PlayerStats> level_1_v3;
@@ -2628,7 +2671,6 @@ Action a_print_level_stats(
for (size_t z = 0; z < 12; z++) {
if (z < 9) {
level_1_v1_v2.emplace_back().char_stats = s->level_table_v1_v2->base_stats_for_class(z);
level_100_limit_v1_v2.emplace_back(s->level_table_v1_v2->level_100_stats_for_class(z));
level_200_limit_v1_v2.emplace_back(s->level_table_v1_v2->max_stats_for_class(z));
s->level_table_v1_v2->advance_to_level(level_100_v1_v2.emplace_back(level_1_v1_v2.back()), 99, z);
s->level_table_v1_v2->advance_to_level(level_200_v1_v2.emplace_back(level_1_v1_v2.back()), 199, z);
@@ -2682,7 +2724,6 @@ Action a_print_level_stats(
print_stats_set(level_1_v1_v2, "v1/v2 Lv.1 ");
print_stats_set(level_100_v1_v2, "v1/v2 Lv.100");
print_stats_set(level_100_limit_v1_v2, "v1 limit ");
print_stats_set(level_200_v1_v2, "v2 Lv.200 ");
print_stats_set(level_200_limit_v1_v2, "v2 limit ");
print_stats_set(level_1_v3, "v3 Lv.1 ");
+1 -1
View File
@@ -334,7 +334,7 @@ using PlayerDispDataDCPCV3 = PlayerDispDataDCPCV3T<false>;
using PlayerDispDataDCPCV3BE = PlayerDispDataDCPCV3T<true>;
struct PlayerDispDataBBPreview {
/* 00 */ le_uint32_t experience = 0;
/* 00 */ le_uint32_t exp = 0;
/* 04 */ le_uint32_t level = 0;
// The name field in this structure is used for the player's Guild Card number, apparently (possibly because it's a
// char array and this is BB)
+1 -1
View File
@@ -3070,7 +3070,7 @@ std::string disassemble_quest_script(
l->lines.emplace_back(std::format(" {:04X} attack_range {:08X} /* {:g} */", l->offset + offsetof(PlayerStats, attack_range), stats.attack_range.load_raw(), stats.attack_range));
l->lines.emplace_back(std::format(" {:04X} knockback_range {:08X} /* {:g} */", l->offset + offsetof(PlayerStats, knockback_range), stats.knockback_range.load_raw(), stats.knockback_range));
l->lines.emplace_back(std::format(" {:04X} level {:08X} /* level {} */", l->offset + offsetof(PlayerStats, level), stats.level, stats.level + 1));
l->lines.emplace_back(std::format(" {:04X} experience {:08X} /* {} */", l->offset + offsetof(PlayerStats, experience), stats.experience, stats.experience));
l->lines.emplace_back(std::format(" {:04X} exp {:08X} /* {} */", l->offset + offsetof(PlayerStats, exp), stats.exp, stats.exp));
l->lines.emplace_back(std::format(" {:04X} meseta {:08X} /* {} */", l->offset + offsetof(PlayerStats, meseta), stats.meseta, stats.meseta));
});
};
+5 -5
View File
@@ -3947,7 +3947,7 @@ static void add_player_exp(shared_ptr<Client> c, uint32_t exp, uint16_t from_ene
auto s = c->require_server_state();
auto p = c->character_file();
p->disp.stats.experience += exp;
p->disp.stats.exp += exp;
if (c->version() == Version::BB_V4) {
send_give_experience(c, exp, from_enemy_id);
}
@@ -3955,7 +3955,7 @@ static void add_player_exp(shared_ptr<Client> c, uint32_t exp, uint16_t from_ene
bool leveled_up = false;
do {
const auto& level = s->level_table(c->version())->stats_delta_for_level(p->disp.visual.char_class, p->disp.stats.level + 1);
if (p->disp.stats.experience >= level.experience) {
if (p->disp.stats.exp >= level.exp) {
leveled_up = true;
level.apply(p->disp.stats.char_stats);
p->disp.stats.level++;
@@ -4017,7 +4017,7 @@ static uint32_t base_exp_for_enemy_type(
const auto& bp_table = bp_index->get_table(is_solo, episode);
const auto& bp_stats_indexes = type_definition_for_enemy(enemy_type).bp_stats_indexes;
if (!bp_stats_indexes.empty()) {
return bp_table.stats_for_index(difficulty, bp_stats_indexes.back()).experience;
return bp_table.stats_for_index(difficulty, bp_stats_indexes.back()).exp;
}
} catch (const out_of_range&) {
}
@@ -4700,8 +4700,8 @@ static asio::awaitable<void> on_battle_level_up_bb(shared_ptr<Client> c, Subcomm
auto s = c->require_server_state();
auto lp = lc->character_file();
uint32_t target_level = min<uint32_t>(lp->disp.stats.level + cmd.num_levels, 199);
uint32_t before_exp = lp->disp.stats.experience;
int32_t exp_delta = lp->disp.stats.experience - before_exp;
uint32_t before_exp = lp->disp.stats.exp;
int32_t exp_delta = lp->disp.stats.exp - before_exp;
if (exp_delta > 0) {
s->level_table(lc->version())->advance_to_level(lp->disp.stats, target_level, lp->disp.visual.char_class);
if (lc->version() == Version::BB_V4) {
+3 -3
View File
@@ -370,7 +370,7 @@ PSOBBBaseSystemFile::PSOBBBaseSystemFile() {
PlayerDispDataBBPreview PSOBBCharacterFile::to_preview() const {
PlayerDispDataBBPreview pre;
pre.level = this->disp.stats.level;
pre.experience = this->disp.stats.experience;
pre.exp = this->disp.stats.exp;
pre.visual = this->disp.visual;
pre.name = this->disp.name;
pre.play_time_seconds = this->play_time_seconds;
@@ -1429,11 +1429,11 @@ void PSOBBCharacterFile::import_tethealla_material_usage(std::shared_ptr<const L
void PSOBBCharacterFile::recompute_stats(std::shared_ptr<const LevelTable> level_table, bool reset_exp) {
uint32_t level = this->disp.stats.level;
uint32_t exp = this->disp.stats.experience;
uint32_t exp = this->disp.stats.exp;
level_table->reset_to_base(this->disp.stats, this->disp.visual.char_class);
level_table->advance_to_level(this->disp.stats, level, this->disp.visual.char_class);
if (!reset_exp) {
this->disp.stats.experience = exp;
this->disp.stats.exp = exp;
}
this->disp.stats.char_stats.atp += (this->get_material_usage(MaterialType::POWER) * 2);
+4 -4
View File
@@ -1888,9 +1888,9 @@ void ServerState::load_battle_params() {
void ServerState::load_level_tables() {
config_log.info_f("Loading level tables");
this->level_table_v1_v2 = make_shared<LevelTableV2>(phosg::load_file("system/level-tables/PlayerTable-pc-v2.prs"), true);
this->level_table_v3 = make_shared<LevelTableV3BE>(phosg::load_file("system/level-tables/PlyLevelTbl-gc-v3.cpt"), true);
this->level_table_v4 = make_shared<LevelTableV4>(*this->load_bb_file("PlyLevelTbl.prs"), true);
this->level_table_v1_v2 = make_shared<JSONLevelTable>(phosg::JSON::parse(phosg::load_file("system/level-tables/level-table-v1-v2.json")));
this->level_table_v3 = make_shared<JSONLevelTable>(phosg::JSON::parse(phosg::load_file("system/level-tables/level-table-v3.json")));
this->level_table_v4 = make_shared<JSONLevelTable>(phosg::JSON::parse(phosg::load_file("system/level-tables/level-table-v4.json")));
}
void ServerState::load_text_index() {
@@ -2248,7 +2248,7 @@ void ServerState::generate_bb_stream_file() {
add_file("BattleParamEntry_lab_on.dat");
add_file("BattleParamEntry_ep4.dat");
add_file("BattleParamEntry_ep4_on.dat");
add_file("PlyLevelTbl.prs");
add_file("PlyLevelTbl.prs", prs_compress_optimal(this->level_table_v4->serialize_binary_v4()));
add_file("ItemMagEdit.prs");
auto pmt = this->item_parameter_table(Version::BB_V4);
add_file("ItemPMT.prs", prs_compress_optimal(pmt->serialize_binary(Version::BB_V4)));
+1 -1
View File
@@ -204,7 +204,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
std::shared_ptr<const G_SetEXResultValues_Ep3_6xB4x4B> ep3_tournament_final_round_ex_values;
std::shared_ptr<const QuestCategoryIndex> quest_category_index;
std::shared_ptr<const QuestIndex> quest_index;
std::shared_ptr<const LevelTableV2> level_table_v1_v2;
std::shared_ptr<const LevelTable> level_table_v1_v2;
std::shared_ptr<const LevelTable> level_table_v3;
std::shared_ptr<const LevelTable> level_table_v4;
std::shared_ptr<const BattleParamsIndex> battle_params;
+12 -12
View File
@@ -293,18 +293,18 @@ uint8_t npc_for_name(const string& name, Version version) {
const char* name_for_char_class(uint8_t cls) {
static const array<const char*, 12> names = {
/* 00 */ "HUmar", // 0
/* 01 */ "HUnewearl", // 0
/* 02 */ "HUcast", // 1
/* 03 */ "RAmar", // 0
/* 04 */ "RAcast", // 2
/* 05 */ "RAcaseal", // 1
/* 06 */ "FOmarl", // 0
/* 07 */ "FOnewm", // 0
/* 08 */ "FOnewearl", // 0
/* 09 */ "HUcaseal", // 1
/* 0A */ "FOmar", // 0
/* 0B */ "RAmarl"}; // 0
/* 00 */ "HUmar",
/* 01 */ "HUnewearl",
/* 02 */ "HUcast",
/* 03 */ "RAmar",
/* 04 */ "RAcast",
/* 05 */ "RAcaseal",
/* 06 */ "FOmarl",
/* 07 */ "FOnewm",
/* 08 */ "FOnewearl",
/* 09 */ "HUcaseal",
/* 0A */ "FOmar",
/* 0B */ "RAmarl"};
try {
return names.at(cls);
} catch (const out_of_range&) {