#include "CommonItemSet.hh" #include "AFSArchive.hh" #include "EnemyType.hh" #include "GSLArchive.hh" #include "StaticGameData.hh" #include "Types.hh" template phosg::JSON to_json(const parray& v) { auto ret = phosg::JSON::list(); for (size_t z = 0; z < Count; z++) { ret.emplace_back(v[z]); } return ret; } template void from_json_into(const phosg::JSON& json, parray& ret) { if (json.size() != Count) { throw std::runtime_error("incorrect array length"); } for (size_t z = 0; z < Count; z++) { ret[z] = json.at(z).as_int(); } } template phosg::JSON to_json(const parray, Count>& v) { auto ret = phosg::JSON::list(); for (size_t z = 0; z < Count; z++) { ret.emplace_back(to_json(v[z])); } return ret; } template void from_json_into(const phosg::JSON& json, parray, Count>& ret) { if (json.size() != Count) { throw std::runtime_error("incorrect array length"); } for (size_t z = 0; z < Count; z++) { from_json_into(json.at(z), ret[z]); } } template phosg::JSON to_json(const CommonItemSet::Table::Range& v) { return (v.min == v.max) ? phosg::JSON(v.min) : phosg::JSON::list({v.min, v.max}); } template void from_json_into(const phosg::JSON& json, CommonItemSet::Table::Range& ret) { if (json.is_int()) { IntT v = json.as_int(); ret.min = v; ret.max = v; } else { const auto& l = json.as_list(); if (l.size() != 2) { throw std::runtime_error("incorrect range list length"); } ret.min = l.at(0)->as_int(); ret.max = l.at(1)->as_int(); } } template phosg::JSON to_json(const parray, Count1>& v) { auto ret = phosg::JSON::list(); for (size_t z = 0; z < Count1; z++) { ret.emplace_back(to_json(v[z])); } return ret; } template void from_json_into(const phosg::JSON& json, parray, Count1>& ret) { if (json.size() != Count1) { throw std::runtime_error("incorrect array length"); } for (size_t z = 0; z < Count1; z++) { from_json_into(json.at(z), ret[z]); } } template void from_json_into(const phosg::JSON& json, parray, Count2>, Count1>& ret) { if (json.size() != Count1) { throw std::runtime_error("incorrect array length"); } for (size_t z = 0; z < Count1; z++) { from_json_into(json.at(z), ret[z]); } } CommonItemSet::Table::Table(std::shared_ptr prev_table, const phosg::JSON& json, Episode episode) : episode(episode) { auto parse_field = [&](const std::string& key, T& field, const T* prev_field) { if (json.count(key)) { from_json_into(json.at(key), field); } else if (prev_field) { field = *prev_field; } }; parse_field("BaseWeaponTypeProbTable", this->base_weapon_type_prob_table, prev_table ? &prev_table->base_weapon_type_prob_table : nullptr); parse_field("SubtypeBaseTable", this->subtype_base_table, prev_table ? &prev_table->subtype_base_table : nullptr); parse_field("SubtypeAreaLengthTable", this->subtype_area_length_table, prev_table ? &prev_table->subtype_area_length_table : nullptr); parse_field("GrindProbTable", this->grind_prob_table, prev_table ? &prev_table->grind_prob_table : nullptr); parse_field("ArmorShieldTypeIndexProbTable", this->armor_shield_type_index_prob_table, prev_table ? &prev_table->armor_shield_type_index_prob_table : nullptr); parse_field("ArmorSlotCountProbTable", this->armor_slot_count_prob_table, prev_table ? &prev_table->armor_slot_count_prob_table : nullptr); parse_field("BoxMesetaRanges", this->box_meseta_ranges, prev_table ? &prev_table->box_meseta_ranges : nullptr); if (json.count("HasRareBonusValueProbTable")) { this->has_rare_bonus_value_prob_table = json.at("HasRareBonusValueProbTable").as_bool(); } else if (prev_table) { this->has_rare_bonus_value_prob_table = prev_table->has_rare_bonus_value_prob_table; } parse_field("BonusValueProbTable", this->bonus_value_prob_table, prev_table ? &prev_table->bonus_value_prob_table : nullptr); parse_field("NonRareBonusProbSpec", this->nonrare_bonus_prob_spec, prev_table ? &prev_table->nonrare_bonus_prob_spec : nullptr); parse_field("BonusTypeProbTable", this->bonus_type_prob_table, prev_table ? &prev_table->bonus_type_prob_table : nullptr); parse_field("SpecialMult", this->special_mult, prev_table ? &prev_table->special_mult : nullptr); parse_field("SpecialPercent", this->special_percent, prev_table ? &prev_table->special_percent : nullptr); parse_field("ToolClassProbTable", this->tool_class_prob_table, prev_table ? &prev_table->tool_class_prob_table : nullptr); parse_field("TechniqueIndexProbTable", this->technique_index_prob_table, prev_table ? &prev_table->technique_index_prob_table : nullptr); parse_field("TechniqueLevelRanges", this->technique_level_ranges, prev_table ? &prev_table->technique_level_ranges : nullptr); if (json.count("ArmorOrShieldTypeBias")) { this->armor_or_shield_type_bias = json.at("ArmorOrShieldTypeBias").as_int(); } else if (prev_table) { this->armor_or_shield_type_bias = prev_table->armor_or_shield_type_bias; } parse_field("UnitMaxStarsTable", this->unit_max_stars_table, prev_table ? &prev_table->unit_max_stars_table : nullptr); parse_field("BoxItemClassProbTable", this->box_item_class_prob_table, prev_table ? &prev_table->box_item_class_prob_table : nullptr); if (json.count("EnemyMesetaRanges")) { const auto& dict = json.at("EnemyMesetaRanges").as_dict(); for (auto enemy_type : phosg::EnumRange()) { try { from_json_into(*dict.at(phosg::name_for_enum(enemy_type)), this->enemy_type_meseta_ranges[enemy_type]); } catch (const std::out_of_range&) { } } } else { this->enemy_type_meseta_ranges = prev_table->enemy_type_meseta_ranges; } if (json.count("EnemyTypeDropProbs")) { const auto& dict = json.at("EnemyTypeDropProbs").as_dict(); for (auto enemy_type : phosg::EnumRange()) { try { this->enemy_type_drop_probs[enemy_type] = dict.at(phosg::name_for_enum(enemy_type))->as_int(); } catch (const std::out_of_range&) { } } } else { this->enemy_type_drop_probs = prev_table->enemy_type_drop_probs; } if (json.count("EnemyItemClasses")) { const auto& dict = json.at("EnemyItemClasses").as_dict(); for (auto enemy_type : phosg::EnumRange()) { try { this->enemy_type_item_classes[enemy_type] = dict.at(phosg::name_for_enum(enemy_type))->as_int(); } catch (const std::out_of_range&) { } } } else { this->enemy_type_item_classes = prev_table->enemy_type_item_classes; } } static const char* name_for_common_item_class(uint8_t item_class) { switch (item_class) { case 0x00: return "WEAPON"; case 0x01: return "ARMOR"; case 0x02: return "SHIELD"; case 0x03: return "UNIT"; case 0x04: return "TOOL"; case 0x05: return "MESETA"; case 0x06: return "NOTHING"; default: return "UNKNOWN"; } } void CommonItemSet::Table::print(FILE* stream) const { const auto& meseta_ranges = this->enemy_type_meseta_ranges; const auto& drop_probs = this->enemy_type_drop_probs; const auto& item_classes = this->enemy_type_item_classes; phosg::fwrite_fmt(stream, "Enemy tables:\n"); phosg::fwrite_fmt(stream, " ##:ENEMY $LOW $HIGH DAR% ITEM\n"); for (auto enemy_type : phosg::EnumRange()) { const auto& def = type_definition_for_enemy(enemy_type); try { const auto& meseta_range = meseta_ranges.at(enemy_type); const auto& drop_prob = drop_probs.at(enemy_type); const auto& item_class = item_classes.at(enemy_type); phosg::fwrite_fmt(stream, " {:02X}:{:<23} {:5} {:5} {:3}% {:02X}:{:<7}\n", def.rt_index, phosg::name_for_enum(enemy_type), meseta_range.min, meseta_range.max, drop_prob, item_class, name_for_common_item_class(item_class)); } catch (const std::out_of_range&) { phosg::fwrite_fmt(stream, " {:02X}:{:<23} ----- ----- ---- --:-------\n", def.rt_index, phosg::name_for_enum(enemy_type)); } } static const std::array base_weapon_type_names = { "SABER", "SWORD", "DAGGER", "PARTISAN", "SLICER", "HANDGUN", "RIFLE", "MECHGUN", "SHOT", "CANE", "ROD", "WAND"}; phosg::fwrite_fmt(stream, "Base weapon config:\n"); phosg::fwrite_fmt(stream, " TYPE PROB [SB AL] FLOORS\n"); for (size_t z = 0; z < 12; z++) { uint8_t floor_to_class[10]; if (this->subtype_base_table[z] < 0) { size_t start_floor = std::min(-this->subtype_area_length_table[z], 10); for (size_t x = 0; x < start_floor; x++) { floor_to_class[x] = 0xFF; } for (size_t x = start_floor; x < 10; x++) { floor_to_class[x] = (x - start_floor) / this->subtype_area_length_table[z]; } } else { for (size_t x = 0; x < 10; x++) { floor_to_class[x] = this->subtype_base_table[z] + (x / this->subtype_area_length_table[z]); } } phosg::fwrite_fmt(stream, " {:02X}:{:<8} {:3}% [{:02X} {:02X}] {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X}\n", z, base_weapon_type_names[z], this->base_weapon_type_prob_table[z], this->subtype_base_table[z], this->subtype_area_length_table[z], floor_to_class[0], floor_to_class[1], floor_to_class[2], floor_to_class[3], floor_to_class[4], floor_to_class[5], floor_to_class[6], floor_to_class[7], floor_to_class[8], floor_to_class[9]); } phosg::fwrite_fmt(stream, "Box configuration:\n"); phosg::fwrite_fmt(stream, " AR $LOW $HIGH WEP% ARM% SHD% UNI% TL% MST% NO%\n"); for (size_t z = 0; z < 10; z++) { phosg::fwrite_fmt(stream, " {:02X} {:5} {:5} {:3}% {:3}% {:3}% {:3}% {:3}% {:3}% {:3}%\n", z, this->box_meseta_ranges[z].min, this->box_meseta_ranges[z].max, this->box_item_class_prob_table[0][z], this->box_item_class_prob_table[1][z], this->box_item_class_prob_table[2][z], this->box_item_class_prob_table[3][z], this->box_item_class_prob_table[4][z], this->box_item_class_prob_table[5][z], this->box_item_class_prob_table[6][z]); } phosg::fwrite_fmt(stream, "Weapon drops:\n"); phosg::fwrite_fmt(stream, " Grinds:\n"); phosg::fwrite_fmt(stream, " GD AR0% AR1% AR2% AR3%\n"); for (size_t z = 0; z < 9; z++) { phosg::fwrite_fmt(stream, " +{} {:3}% {:3}% {:3}% {:3}%\n", z, this->grind_prob_table[z][0], this->grind_prob_table[z][1], this->grind_prob_table[z][2], this->grind_prob_table[z][3]); } phosg::fwrite_fmt(stream, " Bonus value table:\n"); phosg::fwrite_fmt(stream, " ID"); for (int8_t v = -10; v <= 100; v += 5) { phosg::fwrite_fmt(stream, " {:5}%", v); } fputc('\n', stream); for (size_t z = 0; z < (this->has_rare_bonus_value_prob_table ? 6 : 5); z++) { phosg::fwrite_fmt(stream, " {:02X}", z); for (size_t x = 0; x < 0x17; x++) { phosg::fwrite_fmt(stream, " {:5}#", this->bonus_value_prob_table[x][z]); } fputc('\n', stream); } phosg::fwrite_fmt(stream, " Area config tables:\n"); phosg::fwrite_fmt(stream, " AR BONUS SP NO% NTV% AB% MAC% DRK% HIT% SM SPC%\n"); for (size_t z = 0; z < 10; z++) { phosg::fwrite_fmt(stream, " {:02X} {:02X} {:02X} {:02X} {:3}% {:3}% {:3}% {:3}% {:3}% {:3}% {:02X} {:3}%\n", z, this->nonrare_bonus_prob_spec[0][z], this->nonrare_bonus_prob_spec[1][z], this->nonrare_bonus_prob_spec[2][z], this->bonus_type_prob_table[0][z], this->bonus_type_prob_table[1][z], this->bonus_type_prob_table[2][z], this->bonus_type_prob_table[3][z], this->bonus_type_prob_table[4][z], this->bonus_type_prob_table[5][z], this->special_mult[z], this->special_percent[z]); } phosg::fwrite_fmt(stream, "Tool class table:\n"); phosg::fwrite_fmt(stream, " CS A1 A2 A3 A4 A5 A6 A7 A8 A9 A10\n"); for (size_t tool_class = 0; tool_class < this->tool_class_prob_table.size(); tool_class++) { phosg::fwrite_fmt(stream, " {:02X}", tool_class); for (size_t area_norm = 0; area_norm < 10; area_norm++) { phosg::fwrite_fmt(stream, " {:5}", this->tool_class_prob_table[tool_class][area_norm]); } fputc('\n', stream); } static const std::array technique_names = { "FOIE ", "GIFOIE ", "RAFOIE ", "BARTA ", "GIBARTA ", "RABARTA ", "ZONDE ", "GIZONDE ", "RAZONDE ", "GRANTS ", "DEBAND ", "JELLEN ", "ZALURE ", "SHIFTA ", "RYUKER ", "RESTA ", "ANTI ", "REVERSER", "MEGID ", }; phosg::fwrite_fmt(stream, "Technique table:\n"); phosg::fwrite_fmt(stream, " TECH A1 A2 A3 A4 A5 A6 A7 A8 A9 A10\n"); for (size_t tech_num = 0; tech_num < this->technique_index_prob_table.size(); tech_num++) { phosg::fwrite_fmt(stream, " {:02X}:{}", tech_num, technique_names[tech_num]); for (size_t area_norm = 0; area_norm < 10; area_norm++) { uint16_t prob = this->technique_index_prob_table[tech_num][area_norm]; if (prob) { const auto& level_range = this->technique_level_ranges[tech_num][area_norm]; size_t min_level = level_range.min + 1; size_t max_level = level_range.max + 1; phosg::fwrite_fmt(stream, " {:5}[{:2}-{:2}]", prob, min_level, max_level); } else { phosg::fwrite_fmt(stream, " 0[-----]"); } } fputc('\n', stream); } phosg::fwrite_fmt(stream, "Armor/shield type bias: {}\n", this->armor_or_shield_type_bias); phosg::fwrite_fmt(stream, "Armor/shield type index table:\n"); phosg::fwrite_fmt(stream, " TY PROB\n"); for (size_t z = 0; z < 5; z++) { phosg::fwrite_fmt(stream, " {:02X} {:3}%\n", z, this->armor_shield_type_index_prob_table[z]); } phosg::fwrite_fmt(stream, "Armor/shield slot count table:\n"); phosg::fwrite_fmt(stream, " #S PROB\n"); for (size_t z = 0; z < 5; z++) { phosg::fwrite_fmt(stream, " {:02X} {:3}%\n", z, this->armor_slot_count_prob_table[z]); } phosg::fwrite_fmt(stream, "Unit maximum stars table:\n"); phosg::fwrite_fmt(stream, " AR #*\n"); for (size_t z = 0; z < 10; z++) { phosg::fwrite_fmt(stream, " {:02X} {:3}\n", z, this->unit_max_stars_table[z]); } } void CommonItemSet::Table::print_diff(FILE* stream, const Table& other) const { if (this->episode != other.episode) { phosg::fwrite_fmt(stream, "> Episode: {} -> {}\n", name_for_episode(this->episode), name_for_episode(other.episode)); } if (this->base_weapon_type_prob_table != other.base_weapon_type_prob_table) { phosg::fwrite_fmt(stream, "> base_weapon_type_prob_table: {} -> {}\n", phosg::format_data_string(&this->base_weapon_type_prob_table, sizeof(this->base_weapon_type_prob_table)), phosg::format_data_string(&other.base_weapon_type_prob_table, sizeof(other.base_weapon_type_prob_table))); } if (this->subtype_base_table != other.subtype_base_table) { phosg::fwrite_fmt(stream, "> subtype_base_table: {} -> {}\n", phosg::format_data_string(&this->subtype_base_table, sizeof(this->subtype_base_table)), phosg::format_data_string(&other.subtype_base_table, sizeof(other.subtype_base_table))); } if (this->subtype_area_length_table != other.subtype_area_length_table) { phosg::fwrite_fmt(stream, "> subtype_area_length_table: {} -> {}\n", phosg::format_data_string(&this->subtype_area_length_table, sizeof(this->subtype_area_length_table)), phosg::format_data_string(&other.subtype_area_length_table, sizeof(other.subtype_area_length_table))); } if (this->grind_prob_table != other.grind_prob_table) { phosg::fwrite_fmt(stream, "> grind_prob_table: {} -> {}\n", phosg::format_data_string(&this->grind_prob_table, sizeof(this->grind_prob_table)), phosg::format_data_string(&other.grind_prob_table, sizeof(other.grind_prob_table))); } if (this->armor_shield_type_index_prob_table != other.armor_shield_type_index_prob_table) { phosg::fwrite_fmt(stream, "> armor_shield_type_index_prob_table: {} -> {}\n", phosg::format_data_string(&this->armor_shield_type_index_prob_table, sizeof(this->armor_shield_type_index_prob_table)), phosg::format_data_string(&other.armor_shield_type_index_prob_table, sizeof(other.armor_shield_type_index_prob_table))); } if (this->armor_slot_count_prob_table != other.armor_slot_count_prob_table) { phosg::fwrite_fmt(stream, "> armor_slot_count_prob_table: {} -> {}\n", phosg::format_data_string(&this->armor_slot_count_prob_table, sizeof(this->armor_slot_count_prob_table)), phosg::format_data_string(&other.armor_slot_count_prob_table, sizeof(other.armor_slot_count_prob_table))); } auto format_enemy_range_table = [&](const std::unordered_map>& table) -> std::string { std::string ret = ""; for (auto enemy_type : phosg::EnumRange()) { try { const auto& range = table.at(enemy_type); if (!ret.empty()) { ret += ","; } ret += std::format("{}=[{},{}]", phosg::name_for_enum(enemy_type), range.min, range.max); } catch (const std::out_of_range&) { } } return ret; }; auto format_enemy_u8_table = [&](const std::unordered_map& table) -> std::string { std::string ret = ""; for (auto enemy_type : phosg::EnumRange()) { try { uint8_t value = table.at(enemy_type); if (!ret.empty()) { ret += ","; } ret += std::format("{}={}", phosg::name_for_enum(enemy_type), value); } catch (const std::out_of_range&) { } } return ret; }; if (this->enemy_type_meseta_ranges != other.enemy_type_meseta_ranges) { phosg::fwrite_fmt(stream, "> enemy_type_meseta_ranges: {} -> {}\n", format_enemy_range_table(this->enemy_type_meseta_ranges), format_enemy_range_table(other.enemy_type_meseta_ranges)); } if (this->enemy_type_drop_probs != other.enemy_type_drop_probs) { phosg::fwrite_fmt(stream, "> enemy_type_drop_probs: {} -> {}\n", format_enemy_u8_table(this->enemy_type_drop_probs), format_enemy_u8_table(other.enemy_type_drop_probs)); } if (this->enemy_type_item_classes != other.enemy_type_item_classes) { phosg::fwrite_fmt(stream, "> enemy_type_item_classes: {} -> {}\n", format_enemy_u8_table(this->enemy_type_item_classes), format_enemy_u8_table(other.enemy_type_item_classes)); } if (this->box_meseta_ranges != other.box_meseta_ranges) { phosg::fwrite_fmt(stream, "> box_meseta_ranges: {} -> {}\n", phosg::format_data_string(&this->box_meseta_ranges, sizeof(this->box_meseta_ranges)), phosg::format_data_string(&other.box_meseta_ranges, sizeof(other.box_meseta_ranges))); } if (this->has_rare_bonus_value_prob_table != other.has_rare_bonus_value_prob_table) { phosg::fwrite_fmt(stream, "> Has rare bonus value prob table: {} -> {}\n", this->has_rare_bonus_value_prob_table ? "true" : "false", other.has_rare_bonus_value_prob_table ? "true" : "false"); } if (this->bonus_value_prob_table != other.bonus_value_prob_table) { phosg::fwrite_fmt(stream, "> bonus_value_prob_table: {} -> {}\n", phosg::format_data_string(&this->bonus_value_prob_table, sizeof(this->bonus_value_prob_table)), phosg::format_data_string(&other.bonus_value_prob_table, sizeof(other.bonus_value_prob_table))); } if (this->nonrare_bonus_prob_spec != other.nonrare_bonus_prob_spec) { phosg::fwrite_fmt(stream, "> nonrare_bonus_prob_spec: {} -> {}\n", phosg::format_data_string(&this->nonrare_bonus_prob_spec, sizeof(this->nonrare_bonus_prob_spec)), phosg::format_data_string(&other.nonrare_bonus_prob_spec, sizeof(other.nonrare_bonus_prob_spec))); } if (this->bonus_type_prob_table != other.bonus_type_prob_table) { phosg::fwrite_fmt(stream, "> bonus_type_prob_table: {} -> {}\n", phosg::format_data_string(&this->bonus_type_prob_table, sizeof(this->bonus_type_prob_table)), phosg::format_data_string(&other.bonus_type_prob_table, sizeof(other.bonus_type_prob_table))); } if (this->special_mult != other.special_mult) { phosg::fwrite_fmt(stream, "> special_mult: {} -> {}\n", phosg::format_data_string(&this->special_mult, sizeof(this->special_mult)), phosg::format_data_string(&other.special_mult, sizeof(other.special_mult))); } if (this->special_percent != other.special_percent) { phosg::fwrite_fmt(stream, "> special_percent: {} -> {}\n", phosg::format_data_string(&this->special_percent, sizeof(this->special_percent)), phosg::format_data_string(&other.special_percent, sizeof(other.special_percent))); } if (this->tool_class_prob_table != other.tool_class_prob_table) { phosg::fwrite_fmt(stream, "> tool_class_prob_table: {} -> {}\n", phosg::format_data_string(&this->tool_class_prob_table, sizeof(this->tool_class_prob_table)), phosg::format_data_string(&other.tool_class_prob_table, sizeof(other.tool_class_prob_table))); } if (this->technique_index_prob_table != other.technique_index_prob_table) { phosg::fwrite_fmt(stream, "> technique_index_prob_table: {} -> {}\n", phosg::format_data_string(&this->technique_index_prob_table, sizeof(this->technique_index_prob_table)), phosg::format_data_string(&other.technique_index_prob_table, sizeof(other.technique_index_prob_table))); } if (this->technique_level_ranges != other.technique_level_ranges) { phosg::fwrite_fmt(stream, "> technique_level_ranges: {} -> {}\n", phosg::format_data_string(&this->technique_level_ranges, sizeof(this->technique_level_ranges)), phosg::format_data_string(&other.technique_level_ranges, sizeof(other.technique_level_ranges))); } if (this->armor_or_shield_type_bias != other.armor_or_shield_type_bias) { phosg::fwrite_fmt(stream, "> Armor/shield type bias: {} -> {}\n", this->armor_or_shield_type_bias ? "true" : "false", other.armor_or_shield_type_bias ? "true" : "false"); } if (this->unit_max_stars_table != other.unit_max_stars_table) { phosg::fwrite_fmt(stream, "> unit_max_stars_table: {} -> {}\n", phosg::format_data_string(&this->unit_max_stars_table, sizeof(this->unit_max_stars_table)), phosg::format_data_string(&other.unit_max_stars_table, sizeof(other.unit_max_stars_table))); } if (this->box_item_class_prob_table != other.box_item_class_prob_table) { phosg::fwrite_fmt(stream, "> box_item_class_prob_table: {} -> {}\n", phosg::format_data_string(&this->box_item_class_prob_table, sizeof(this->box_item_class_prob_table)), phosg::format_data_string(&other.box_item_class_prob_table, sizeof(other.box_item_class_prob_table))); } } phosg::JSON CommonItemSet::Table::json(std::shared_ptr prev_table) const { auto ret = phosg::JSON::dict(); if (!prev_table || (this->base_weapon_type_prob_table != prev_table->base_weapon_type_prob_table)) { ret.emplace("BaseWeaponTypeProbTable", to_json(this->base_weapon_type_prob_table)); } if (!prev_table || (this->subtype_base_table != prev_table->subtype_base_table)) { ret.emplace("SubtypeBaseTable", to_json(this->subtype_base_table)); } if (!prev_table || (this->subtype_area_length_table != prev_table->subtype_area_length_table)) { ret.emplace("SubtypeAreaLengthTable", to_json(this->subtype_area_length_table)); } if (!prev_table || (this->grind_prob_table != prev_table->grind_prob_table)) { ret.emplace("GrindProbTable", to_json(this->grind_prob_table)); } if (!prev_table || (this->armor_shield_type_index_prob_table != prev_table->armor_shield_type_index_prob_table)) { ret.emplace("ArmorShieldTypeIndexProbTable", to_json(this->armor_shield_type_index_prob_table)); } if (!prev_table || (this->armor_slot_count_prob_table != prev_table->armor_slot_count_prob_table)) { ret.emplace("ArmorSlotCountProbTable", to_json(this->armor_slot_count_prob_table)); } bool needs_enemy_type_meseta_ranges = (!prev_table || (this->enemy_type_meseta_ranges != prev_table->enemy_type_meseta_ranges)); bool needs_enemy_type_drop_probs = (!prev_table || (this->enemy_type_drop_probs != prev_table->enemy_type_drop_probs)); bool needs_enemy_type_item_classes = (!prev_table || (this->enemy_type_item_classes != prev_table->enemy_type_item_classes)); if (needs_enemy_type_meseta_ranges || needs_enemy_type_drop_probs || needs_enemy_type_item_classes) { phosg::JSON enemy_type_meseta_ranges_json = phosg::JSON::dict(); phosg::JSON enemy_type_drop_probs_json = phosg::JSON::dict(); phosg::JSON enemy_type_item_classes_json = phosg::JSON::dict(); for (auto enemy_type : phosg::EnumRange()) { auto name = phosg::name_for_enum(enemy_type); if (needs_enemy_type_meseta_ranges) { try { enemy_type_meseta_ranges_json.emplace(name, to_json(this->enemy_type_meseta_ranges.at(enemy_type))); } catch (const std::out_of_range&) { } } if (needs_enemy_type_drop_probs) { try { enemy_type_drop_probs_json.emplace(name, this->enemy_type_drop_probs.at(enemy_type)); } catch (const std::out_of_range&) { } } if (needs_enemy_type_item_classes) { try { enemy_type_item_classes_json.emplace(name, this->enemy_type_item_classes.at(enemy_type)); } catch (const std::out_of_range&) { } } } if (needs_enemy_type_meseta_ranges) { ret.emplace("EnemyMesetaRanges", std::move(enemy_type_meseta_ranges_json)); } if (needs_enemy_type_drop_probs) { ret.emplace("EnemyTypeDropProbs", std::move(enemy_type_drop_probs_json)); } if (needs_enemy_type_item_classes) { ret.emplace("EnemyItemClasses", std::move(enemy_type_item_classes_json)); } } if (!prev_table || (this->box_meseta_ranges != prev_table->box_meseta_ranges)) { ret.emplace("BoxMesetaRanges", to_json(this->box_meseta_ranges)); } if (!prev_table || (this->has_rare_bonus_value_prob_table != prev_table->has_rare_bonus_value_prob_table)) { ret.emplace("HasRareBonusValueProbTable", this->has_rare_bonus_value_prob_table); } if (!prev_table || (this->bonus_value_prob_table != prev_table->bonus_value_prob_table)) { ret.emplace("BonusValueProbTable", to_json(this->bonus_value_prob_table)); } if (!prev_table || (this->nonrare_bonus_prob_spec != prev_table->nonrare_bonus_prob_spec)) { ret.emplace("NonRareBonusProbSpec", to_json(this->nonrare_bonus_prob_spec)); } if (!prev_table || (this->bonus_type_prob_table != prev_table->bonus_type_prob_table)) { ret.emplace("BonusTypeProbTable", to_json(this->bonus_type_prob_table)); } if (!prev_table || (this->special_mult != prev_table->special_mult)) { ret.emplace("SpecialMult", to_json(this->special_mult)); } if (!prev_table || (this->special_percent != prev_table->special_percent)) { ret.emplace("SpecialPercent", to_json(this->special_percent)); } if (!prev_table || (this->tool_class_prob_table != prev_table->tool_class_prob_table)) { ret.emplace("ToolClassProbTable", to_json(this->tool_class_prob_table)); } if (!prev_table || (this->technique_index_prob_table != prev_table->technique_index_prob_table)) { ret.emplace("TechniqueIndexProbTable", to_json(this->technique_index_prob_table)); } if (!prev_table || (this->technique_level_ranges != prev_table->technique_level_ranges)) { ret.emplace("TechniqueLevelRanges", to_json(this->technique_level_ranges)); } if (!prev_table || (this->armor_or_shield_type_bias != prev_table->armor_or_shield_type_bias)) { ret.emplace("ArmorOrShieldTypeBias", this->armor_or_shield_type_bias); } if (!prev_table || (this->unit_max_stars_table != prev_table->unit_max_stars_table)) { ret.emplace("UnitMaxStarsTable", to_json(this->unit_max_stars_table)); } if (!prev_table || (this->box_item_class_prob_table != prev_table->box_item_class_prob_table)) { ret.emplace("BoxItemClassProbTable", to_json(this->box_item_class_prob_table)); } return ret; } phosg::JSON CommonItemSet::json() const { auto ret = phosg::JSON::dict(); for (const auto& episode : ALL_EPISODES_V4) { for (const auto& mode : ALL_GAME_MODES_V4) { for (const auto& difficulty : ALL_DIFFICULTIES_V234) { for (uint8_t section_id = 0; section_id < 10; section_id++) { auto json_key = this->json_key_for_table(episode, mode, difficulty, section_id); try { auto prev_table = this->get_prev_table(episode, mode, difficulty, section_id); auto table = this->get_table(episode, mode, difficulty, section_id); ret.emplace(json_key, table->json(prev_table)); } catch (const std::runtime_error&) { } } } } } return ret; } void CommonItemSet::print(FILE* stream) const { for (const auto& episode : ALL_EPISODES_V4) { for (const auto& mode : ALL_GAME_MODES_V4) { for (Difficulty difficulty : ALL_DIFFICULTIES_V234) { for (uint8_t section_id = 0; section_id < 10; section_id++) { try { auto table = this->get_table(episode, mode, difficulty, section_id); phosg::fwrite_fmt(stream, "============ {} {} {} {}\n", name_for_episode(episode), name_for_mode(mode), name_for_difficulty(difficulty), name_for_section_id(section_id)); table->print(stream); } catch (const std::runtime_error&) { } } } } } } void CommonItemSet::print_diff(FILE* stream, const CommonItemSet& other) const { for (const auto& episode : ALL_EPISODES_V4) { for (const auto& mode : ALL_GAME_MODES_V4) { for (const auto& difficulty : ALL_DIFFICULTIES_V234) { for (uint8_t section_id = 0; section_id < 10; section_id++) { std::shared_ptr this_table; std::shared_ptr other_table; try { this_table = this->get_table(episode, mode, difficulty, section_id); } catch (const std::runtime_error&) { } try { other_table = other.get_table(episode, mode, difficulty, section_id); } catch (const std::runtime_error&) { } if (!this_table && !other_table) { continue; } else if (!this_table) { phosg::fwrite_fmt(stream, "> Table present in other but not this: {} {} {} {}\n", name_for_episode(episode), name_for_mode(mode), name_for_difficulty(difficulty), name_for_section_id(section_id)); } else if (!other_table) { phosg::fwrite_fmt(stream, "> Table present in this but not other: {} {} {} {}\n", name_for_episode(episode), name_for_mode(mode), name_for_difficulty(difficulty), name_for_section_id(section_id)); } else if (*this_table != *other_table) { phosg::fwrite_fmt(stream, "> Tables do not match: {} {} {} {}\n", name_for_episode(episode), name_for_mode(mode), name_for_difficulty(difficulty), name_for_section_id(section_id)); this_table->print_diff(stream, *other_table); } } } } } } CommonItemSet::Table::Table(const phosg::StringReader& r, bool is_big_endian, bool is_v3, Episode episode) : episode(episode) { if (is_big_endian) { this->parse_itempt_t(r, is_v3); } else { this->parse_itempt_t(r, is_v3); } } template void CommonItemSet::Table::parse_itempt_t(const phosg::StringReader& r, bool is_v3) { const auto& offsets = r.pget>(r.pget>(r.size() - 0x10)); this->base_weapon_type_prob_table = r.pget>(offsets.base_weapon_type_prob_table_offset); this->subtype_base_table = r.pget>(offsets.subtype_base_table_offset); this->subtype_area_length_table = r.pget>(offsets.subtype_area_length_table_offset); this->grind_prob_table = r.pget, 9>>(offsets.grind_prob_table_offset); this->armor_shield_type_index_prob_table = r.pget>(offsets.armor_shield_type_index_prob_table_offset); this->armor_slot_count_prob_table = r.pget>(offsets.armor_slot_count_prob_table_offset); const auto& enemy_rt_index_meseta_ranges = r.pget>, NUM_RT_INDEXES_V3>>( offsets.enemy_rt_index_meseta_ranges_offset); const auto& enemy_rt_index_drop_probs = r.pget>( offsets.enemy_rt_index_drop_probs_offset); const auto& enemy_rt_index_item_classes = r.pget>( offsets.enemy_rt_index_item_classes_offset); for (auto enemy_type : phosg::EnumRange()) { const auto& def = type_definition_for_enemy(enemy_type); if (def.valid_in_episode(this->episode) && (def.rt_index < enemy_rt_index_meseta_ranges.size())) { const auto& meseta_range = enemy_rt_index_meseta_ranges[def.rt_index]; if (meseta_range.max > 0) { this->enemy_type_meseta_ranges.emplace(enemy_type, Range{meseta_range.min, meseta_range.max}); } if (enemy_rt_index_drop_probs[def.rt_index] > 0) { this->enemy_type_drop_probs.emplace(enemy_type, enemy_rt_index_drop_probs[def.rt_index]); } if (enemy_rt_index_item_classes[def.rt_index] != 0xFF) { this->enemy_type_item_classes.emplace(enemy_type, enemy_rt_index_item_classes[def.rt_index]); } } } { const auto& data = r.pget>, 0x0A>>(offsets.box_meseta_ranges_offset); for (size_t z = 0; z < data.size(); z++) { this->box_meseta_ranges[z] = Range{data[z].min, data[z].max}; } } this->has_rare_bonus_value_prob_table = is_v3; if (!this->has_rare_bonus_value_prob_table) { // V2 const auto& data = r.pget, 0x17>>(offsets.bonus_value_prob_table_offset); for (size_t z = 0; z < data.size(); z++) { for (size_t x = 0; x < data[z].size(); x++) { this->bonus_value_prob_table[z][x] = data[z][x]; } } } else { // V3 const auto& data = r.pget, 6>, 0x17>>(offsets.bonus_value_prob_table_offset); for (size_t z = 0; z < data.size(); z++) { for (size_t x = 0; x < data[z].size(); x++) { this->bonus_value_prob_table[z][x] = data[z][x]; } } } this->nonrare_bonus_prob_spec = r.pget, 3>>(offsets.nonrare_bonus_prob_spec_offset); this->bonus_type_prob_table = r.pget, 6>>(offsets.bonus_type_prob_table_offset); this->special_mult = r.pget>(offsets.special_mult_offset); this->special_percent = r.pget>(offsets.special_percent_offset); { const auto& data = r.pget, 0x0A>, 0x1C>>(offsets.tool_class_prob_table_offset); for (size_t z = 0; z < data.size(); z++) { for (size_t x = 0; x < data[z].size(); x++) { this->tool_class_prob_table[z][x] = data[z][x]; } } } this->technique_index_prob_table = r.pget, 0x13>>(offsets.technique_index_prob_table_offset); this->technique_level_ranges = r.pget, 0x0A>, 0x13>>(offsets.technique_level_ranges_offset); this->armor_or_shield_type_bias = offsets.armor_or_shield_type_bias; this->unit_max_stars_table = r.pget>(offsets.unit_max_stars_offset); this->box_item_class_prob_table = r.pget, 7>>(offsets.box_item_class_prob_table_offset); } uint16_t CommonItemSet::key_for_table(Episode episode, GameMode mode, Difficulty difficulty, uint8_t secid) { // Bits: -----EEEMMDDSSSS return (((static_cast(episode) << 8) & 0x0700) | ((static_cast(mode) << 6) & 0x00C0) | ((static_cast(difficulty) << 4) & 0x0030) | (static_cast(secid) & 0x000F)); } std::string CommonItemSet::json_key_for_table( Episode episode, GameMode mode, Difficulty difficulty, uint8_t section_id) { return std::format("{}:{}:{}:{}", abbreviation_for_episode(episode), name_for_mode(mode), token_name_for_difficulty(difficulty), name_for_section_id(section_id)); } std::shared_ptr CommonItemSet::get_table( Episode episode, GameMode mode, Difficulty difficulty, uint8_t section_id) const { try { return this->tables.at(this->key_for_table(episode, mode, difficulty, section_id)); } catch (const std::out_of_range&) { throw std::runtime_error(std::format("common item table not available for episode={}, mode={}, difficulty={}, secid={}", name_for_episode(episode), name_for_mode(mode), name_for_difficulty(difficulty), section_id)); } } std::shared_ptr CommonItemSet::get_prev_table( Episode episode, GameMode mode, Difficulty difficulty, uint8_t section_id) const { if (section_id != 0) { // All section IDs are based on the previous, except Viridia return this->get_table(episode, mode, difficulty, section_id - 1); } else if (difficulty != Difficulty::NORMAL) { // All Viridia tables are based on the previous difficulty, except Normal auto prev_difficulty = static_cast(static_cast(difficulty) - 1); return this->get_table(episode, mode, prev_difficulty, 0); } else if (mode != GameMode::NORMAL) { // All Normal Viridia tables are based on the Normal game mode, except Normal itself return this->get_table(episode, GameMode::NORMAL, Difficulty::NORMAL, 0); } else { // There's no previous table return nullptr; } } AFSV2CommonItemSet::AFSV2CommonItemSet( std::shared_ptr pt_afs_data, std::shared_ptr ct_afs_data) { // Each AFS file has 40 entries (30 on v1); the first 10 are for Normal, then Hard, etc. { AFSArchive pt_afs(pt_afs_data); bool include_ultimate; if (pt_afs.num_entries() >= 40) { include_ultimate = true; } else if (pt_afs.num_entries() >= 30) { include_ultimate = false; } else { throw std::runtime_error(std::format("PT AFS file has unexpected entry count ({})", pt_afs.num_entries())); } for (Difficulty difficulty : ALL_DIFFICULTIES_V234) { if ((difficulty == Difficulty::ULTIMATE) && !include_ultimate) { continue; } for (size_t section_id = 0; section_id < 10; section_id++) { auto entry = pt_afs.get(static_cast(difficulty) * 10 + section_id); phosg::StringReader r(entry.first, entry.second); auto table = std::make_shared(r, false, false, Episode::EP1); this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::NORMAL, difficulty, section_id), table); this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::BATTLE, difficulty, section_id), table); this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::SOLO, difficulty, section_id), table); } } } // ItemCT AFS files also have 40 entries, but only the 0th, 10th, 20th, and 30th are used (section_id is ignored) if (ct_afs_data) { AFSArchive ct_afs(ct_afs_data); bool include_ultimate; if (ct_afs.num_entries() >= 40) { include_ultimate = true; } else if (ct_afs.num_entries() >= 30) { include_ultimate = false; } else { throw std::runtime_error(std::format("CT AFS file has unexpected entry count ({})", ct_afs.num_entries())); } for (Difficulty difficulty : ALL_DIFFICULTIES_V234) { if ((difficulty == Difficulty::ULTIMATE) && !include_ultimate) { continue; } auto r = ct_afs.get_reader(static_cast(difficulty) * 10); auto table = std::make_shared
(r, false, false, Episode::EP1); for (size_t section_id = 0; section_id < 10; section_id++) { this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::CHALLENGE, difficulty, section_id), table); } } } } GSLV3V4CommonItemSet::GSLV3V4CommonItemSet(std::shared_ptr gsl_data, bool is_big_endian) { GSLArchive gsl(gsl_data, is_big_endian); auto filename_for_table = +[](Episode episode, Difficulty difficulty, uint8_t section_id, bool is_challenge) -> std::string { const char* episode_token = ""; switch (episode) { case Episode::EP1: episode_token = ""; break; case Episode::EP2: episode_token = "l"; break; case Episode::EP4: episode_token = "s"; break; default: throw std::runtime_error("invalid episode"); } return std::format( "ItemPT{}{}{}{}.rel", is_challenge ? "c" : "", episode_token, static_cast(tolower(abbreviation_for_difficulty(difficulty))), section_id); }; for (Episode episode : ALL_EPISODES_V3) { for (Difficulty difficulty : ALL_DIFFICULTIES_V234) { for (size_t section_id = 0; section_id < 10; section_id++) { phosg::StringReader r; try { r = gsl.get_reader(filename_for_table(episode, difficulty, section_id, false)); } catch (const std::exception&) { // Fall back to Episode 1 if Episode 4 data is missing if (episode == Episode::EP4) { auto ep1_table = this->tables.at(this->key_for_table(Episode::EP1, GameMode::NORMAL, difficulty, section_id)); this->tables.emplace(this->key_for_table(episode, GameMode::NORMAL, difficulty, section_id), ep1_table); this->tables.emplace(this->key_for_table(episode, GameMode::BATTLE, difficulty, section_id), ep1_table); this->tables.emplace(this->key_for_table(episode, GameMode::SOLO, difficulty, section_id), ep1_table); continue; } else { throw; } } auto table = std::make_shared
(r, is_big_endian, true, episode); this->tables.emplace(this->key_for_table(episode, GameMode::NORMAL, difficulty, section_id), table); this->tables.emplace(this->key_for_table(episode, GameMode::BATTLE, difficulty, section_id), table); this->tables.emplace(this->key_for_table(episode, GameMode::SOLO, difficulty, section_id), table); } } for (Difficulty difficulty : ALL_DIFFICULTIES_V234) { try { auto r = gsl.get_reader(filename_for_table(episode, difficulty, 0, true)); auto table = std::make_shared
(r, is_big_endian, true, episode); for (size_t section_id = 0; section_id < 10; section_id++) { this->tables.emplace(this->key_for_table(episode, GameMode::CHALLENGE, difficulty, section_id), table); } } catch (const std::out_of_range&) { // GC NTE doesn't have Ep2 challenge; just skip adding the table } } } } JSONCommonItemSet::JSONCommonItemSet(const phosg::JSON& json) { for (const auto& episode : ALL_EPISODES_V4) { for (const auto& mode : ALL_GAME_MODES_V4) { for (const auto& difficulty : ALL_DIFFICULTIES_V234) { for (uint8_t section_id = 0; section_id < 10; section_id++) { try { auto prev_table = this->get_prev_table(episode, mode, difficulty, section_id); auto json_key = this->json_key_for_table(episode, mode, difficulty, section_id); auto key = this->key_for_table(episode, mode, difficulty, section_id); this->tables.emplace(key, std::make_shared
(prev_table, json.at(json_key), episode)); } catch (const std::runtime_error&) { } catch (const std::out_of_range&) { } } } } } } RELFileSet::RELFileSet(std::shared_ptr data) : data(data), r(*this->data) {} ArmorRandomSet::ArmorRandomSet(std::shared_ptr data) : RELFileSet(data) { // For some reason the footer tables are doubly indirect in this file uint32_t specs_offset_offset = this->r.pget_u32b(data->size() - 0x10); uint32_t specs_offset = this->r.pget_u32b(specs_offset_offset); this->tables = &this->r.pget>(specs_offset); } std::pair ArmorRandomSet::get_armor_table(size_t index) const { return this->get_table(this->tables->at(0), index); } std::pair ArmorRandomSet::get_shield_table(size_t index) const { return this->get_table(this->tables->at(1), index); } std::pair ArmorRandomSet::get_unit_table(size_t index) const { return this->get_table(this->tables->at(2), index); } ToolRandomSet::ToolRandomSet(std::shared_ptr data) : RELFileSet(data) { uint32_t specs_offset = r.pget_u32b(data->size() - 0x10); this->common_recovery_table_spec = &r.pget(r.pget_u32b(specs_offset)); this->rare_recovery_table_spec = &r.pget(r.pget_u32b(specs_offset + sizeof(uint32_t)), 2 * sizeof(TableSpec)); this->tech_disk_table_spec = this->rare_recovery_table_spec + 1; this->tech_disk_level_table_spec = &r.pget(r.pget_u32b(specs_offset + 2 * sizeof(uint32_t))); } std::pair ToolRandomSet::get_common_recovery_table(size_t index) const { return this->get_table(*this->common_recovery_table_spec, index); } std::pair ToolRandomSet::get_rare_recovery_table(size_t index) const { return this->get_table(*this->rare_recovery_table_spec, index); } std::pair ToolRandomSet::get_tech_disk_table(size_t index) const { return this->get_table(*this->tech_disk_table_spec, index); } std::pair ToolRandomSet::get_tech_disk_level_table(size_t index) const { return this->get_table(*this->tech_disk_level_table_spec, index); } WeaponRandomSet::WeaponRandomSet(std::shared_ptr data) : RELFileSet(data) { uint32_t offsets_offset = this->r.pget_u32b(data->size() - 0x10); this->offsets = &this->r.pget(offsets_offset); } std::pair WeaponRandomSet::get_weapon_type_table(size_t index) const { const auto& spec = this->r.pget(this->offsets->weapon_type_table + index * sizeof(TableSpec)); const auto* data = &this->r.pget(spec.offset, spec.entries_per_table * sizeof(WeightTableEntry8)); return std::make_pair(data, spec.entries_per_table); } const parray* WeaponRandomSet::get_bonus_type_table(size_t which, size_t index) const { uint32_t base_offset = which ? this->offsets->bonus_type_table2 : this->offsets->bonus_type_table1; return &this->r.pget>(base_offset + sizeof(parray) * index); } const WeaponRandomSet::RangeTableEntry* WeaponRandomSet::get_bonus_range(size_t which, size_t index) const { uint32_t base_offset = which ? this->offsets->bonus_range_table2 : this->offsets->bonus_range_table1; return &this->r.pget(base_offset + sizeof(RangeTableEntry) * index); } const parray* WeaponRandomSet::get_special_mode_table(size_t index) const { return &this->r.pget>( this->offsets->special_mode_table + sizeof(parray) * index); } const WeaponRandomSet::RangeTableEntry* WeaponRandomSet::get_standard_grind_range(size_t index) const { return &this->r.pget(this->offsets->standard_grind_range_table + sizeof(RangeTableEntry) * index); } const WeaponRandomSet::RangeTableEntry* WeaponRandomSet::get_favored_grind_range(size_t index) const { return &this->r.pget(this->offsets->favored_grind_range_table + sizeof(RangeTableEntry) * index); }