diff --git a/src/CommonItemSet.cc b/src/CommonItemSet.cc index f54993b3..69608418 100644 --- a/src/CommonItemSet.cc +++ b/src/CommonItemSet.cc @@ -16,6 +16,16 @@ JSON to_json(const parray& v) { return ret; } +template +void from_json_into(const JSON& json, parray& ret) { + if (json.size() != Count) { + throw runtime_error("incorrect array length"); + } + for (size_t z = 0; z < Count; z++) { + ret[z] = json.at(z).as_int(); + } +} + template JSON to_json(const parray, Count>& v) { auto ret = JSON::list(); @@ -25,6 +35,16 @@ JSON to_json(const parray, Count>& v) { return ret; } +template +void from_json_into(const JSON& json, parray, Count>& ret) { + if (json.size() != Count) { + throw runtime_error("incorrect array length"); + } + for (size_t z = 0; z < Count; z++) { + from_json_into(json.at(z), ret[z]); + } +} + template JSON to_json(const CommonItemSet::Table::Range& v) { if (v.min == v.max) { @@ -34,6 +54,22 @@ JSON to_json(const CommonItemSet::Table::Range& v) { } } +template +void from_json_into(const 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 runtime_error("incorrect range list length"); + } + ret.min = l.at(0)->as_int(); + ret.max = l.at(1)->as_int(); + } +} + template JSON to_json(const parray, Count1>& v) { auto ret = JSON::list(); @@ -43,6 +79,262 @@ JSON to_json(const parray, Count1>& v) { return ret; } +template +void from_json_into(const JSON& json, parray, Count1>& ret) { + if (json.size() != Count1) { + throw 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 JSON& json, parray, Count2>, Count1>& ret) { + if (json.size() != Count1) { + throw runtime_error("incorrect array length"); + } + for (size_t z = 0; z < Count1; z++) { + from_json_into(json.at(z), ret[z]); + } +} + +CommonItemSet::Table::Table(const JSON& json, Episode episode) + : episode(episode) { + from_json_into(json.at("BaseWeaponTypeProbTable"), this->base_weapon_type_prob_table); + from_json_into(json.at("SubtypeBaseTable"), this->subtype_base_table); + from_json_into(json.at("SubtypeAreaLengthTable"), this->subtype_area_length_table); + from_json_into(json.at("GrindProbTable"), this->grind_prob_table); + from_json_into(json.at("ArmorShieldTypeIndexProbTable"), this->armor_shield_type_index_prob_table); + from_json_into(json.at("ArmorSlotCountProbTable"), this->armor_slot_count_prob_table); + from_json_into(json.at("BoxMesetaRanges"), this->box_meseta_ranges); + this->has_rare_bonus_value_prob_table = json.at("HasRareBonusValueProbTable").as_bool(); + from_json_into(json.at("BonusValueProbTable"), this->bonus_value_prob_table); + from_json_into(json.at("NonRareBonusProbSpec"), this->nonrare_bonus_prob_spec); + from_json_into(json.at("BonusTypeProbTable"), this->bonus_type_prob_table); + from_json_into(json.at("SpecialMult"), this->special_mult); + from_json_into(json.at("SpecialPercent"), this->special_percent); + from_json_into(json.at("ToolClassProbTable"), this->tool_class_prob_table); + from_json_into(json.at("TechniqueIndexProbTable"), this->technique_index_prob_table); + from_json_into(json.at("TechniqueLevelRanges"), this->technique_level_ranges); + this->armor_or_shield_type_bias = json.at("ArmorOrShieldTypeBias").as_int(); + from_json_into(json.at("UnitMaxStarsTable"), this->unit_max_stars_table); + from_json_into(json.at("BoxItemClassProbTable"), this->box_item_class_prob_table); + + const auto& enemy_meseta_ranges_json = json.at("EnemyMesetaRanges").as_dict(); + const auto& enemy_type_drop_probs_json = json.at("EnemyTypeDropProbs").as_dict(); + const auto& enemy_item_classes_json = json.at("EnemyItemClasses").as_dict(); + for (size_t z = 0; z < 0x64; z++) { + static const array episodes = {Episode::EP1, Episode::EP2, Episode::EP4}; + for (Episode episode : episodes) { + for (auto type : enemy_types_for_rare_table_index(episode, z)) { + string name = string_printf("%s:%s", abbreviation_for_episode(episode), name_for_enum(type)); + from_json_into(*enemy_meseta_ranges_json.at(name), this->enemy_meseta_ranges[z]); + this->enemy_type_drop_probs[z] = enemy_type_drop_probs_json.at(name)->as_int(); + this->enemy_item_classes[z] = enemy_item_classes_json.at(name)->as_int(); + } + } + } +} + +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_meseta_ranges; + const auto& drop_probs = this->enemy_type_drop_probs; + const auto& item_classes = this->enemy_item_classes; + fprintf(stream, "Enemy tables:\n"); + fprintf(stream, " ## $LOW $HIGH DAR%% ITEM ENEMIES\n"); + for (size_t z = 0; z < 0x64; z++) { + string enemies_str; + for (EnemyType enemy_type : enemy_types_for_rare_table_index(this->episode, z)) { + if (!enemies_str.empty()) { + enemies_str += ", "; + } + enemies_str += name_for_enum(enemy_type); + } + if (drop_probs[z]) { + fprintf(stream, " %02zX %5hu %5hu %3hhu%% %02hX:%s %s\n", + z, meseta_ranges[z].min, meseta_ranges[z].max, drop_probs[z], item_classes[z], + name_for_common_item_class(item_classes[z]), enemies_str.c_str()); + } else { + fprintf(stream, " %02zX ----- ----- 0%% -- %s\n", z, enemies_str.c_str()); + } + } + + static const array base_weapon_type_names = { + "SABER ", + "SWORD ", + "DAGGER ", + "PARTISAN", + "SLICER ", + "HANDGUN ", + "RIFLE ", + "MECHGUN ", + "SHOT ", + "CANE ", + "ROD ", + "WAND ", + }; + fprintf(stream, "Base weapon config:\n"); + fprintf(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]); + } + } + fprintf(stream, " %02zX:%s %3hhu%% [%02hhX %02hhX] %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX\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]); + } + + fprintf(stream, "Box configuration:\n"); + fprintf(stream, " AR $LOW $HIGH WEP%% ARM%% SHD%% UNI%% TL%% MST%% NO%%\n"); + for (size_t z = 0; z < 10; z++) { + fprintf(stream, " %02zX %5hu %5hu %3hhu%% %3hhu%% %3hhu%% %3hhu%% %3hhu%% %3hhu%% %3hhu%%\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]); + } + + fprintf(stream, "Weapon drops:\n"); + fprintf(stream, " Grinds:\n"); + fprintf(stream, " GD AR0%% AR1%% AR2%% AR3%%\n"); + for (size_t z = 0; z < 9; z++) { + fprintf(stream, " +%zu %3hhd%% %3hhd%% %3hhd%% %3hhd%%\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]); + } + fprintf(stream, " Bonus value table:\n"); + fprintf(stream, " ID"); + for (int8_t v = -10; v <= 100; v += 5) { + fprintf(stream, " %5hhd%%", v); + } + fputc('\n', stream); + for (size_t z = 0; z < (this->has_rare_bonus_value_prob_table ? 6 : 5); z++) { + fprintf(stream, " %02zX", z); + for (size_t x = 0; x < 0x17; x++) { + fprintf(stream, " %5hu#", this->bonus_value_prob_table[x][z]); + } + fputc('\n', stream); + } + fprintf(stream, " Area config tables:\n"); + fprintf(stream, " AR BONUS SP NO%% NTV%% AB%% MAC%% DRK%% HIT%% SM SPC%%\n"); + for (size_t z = 0; z < 10; z++) { + fprintf(stream, " %02zX %02hhX %02hhX %02hhX %3hhu%% %3hhu%% %3hhu%% %3hhu%% %3hhu%% %3hhu%% %02hhX %3hhu%%\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]); + } + + fprintf(stream, " Tool class table:\n"); + fprintf(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++) { + fprintf(stream, " %02zX", tool_class); + for (size_t area_norm = 0; area_norm < 10; area_norm++) { + fprintf(stream, " %5hu", this->tool_class_prob_table[tool_class][area_norm]); + } + fputc('\n', stream); + } + + static const array technique_names = { + "FOIE ", + "GIFOIE ", + "RAFOIE ", + "BARTA ", + "GIBARTA ", + "RABARTA ", + "ZONDE ", + "GIZONDE ", + "RAZONDE ", + "GRANTS ", + "DEBAND ", + "JELLEN ", + "ZALURE ", + "SHIFTA ", + "RYUKER ", + "RESTA ", + "ANTI ", + "REVERSER", + "MEGID ", + }; + + fprintf(stream, " Technique table:\n"); + fprintf(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++) { + fprintf(stream, " %02zX:%s", 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; + fprintf(stream, " %5hu[%2zu-%2zu]", prob, min_level, max_level); + } else { + fprintf(stream, " 0[-----]"); + } + } + fputc('\n', stream); + } + + fprintf(stream, " Armor/shield type bias: %hhu\n", this->armor_or_shield_type_bias); + + fprintf(stream, " Armor/shield type index table:\n"); + fprintf(stream, " TY PROB\n"); + for (size_t z = 0; z < 5; z++) { + fprintf(stream, " %02zX %3hhu%%\n", z, this->armor_shield_type_index_prob_table[z]); + } + + fprintf(stream, " Armor/shield slot count table:\n"); + fprintf(stream, " #S PROB\n"); + for (size_t z = 0; z < 5; z++) { + fprintf(stream, " %02zX %3hhu%%\n", z, this->armor_slot_count_prob_table[z]); + } + + fprintf(stream, " Unit maximum stars table:\n"); + fprintf(stream, " AR #*\n"); + for (size_t z = 0; z < 10; z++) { + fprintf(stream, " %02zX %3hhu\n", z, this->unit_max_stars_table[z]); + } +} + JSON CommonItemSet::Table::json() const { JSON enemy_meseta_ranges_json = JSON::dict(); JSON enemy_type_drop_probs_json = JSON::dict(); @@ -111,7 +403,28 @@ JSON CommonItemSet::json() const { return modes_dict; } -CommonItemSet::Table::Table(const StringReader& r, bool is_big_endian, bool is_v3) { +void CommonItemSet::print(FILE* stream) const { + static const array modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO}; + for (const auto& mode : modes) { + static const array episodes = {Episode::EP1, Episode::EP2, Episode::EP4}; + for (const auto& episode : episodes) { + for (uint8_t difficulty = 0; difficulty < 4; difficulty++) { + for (uint8_t section_id = 0; section_id < 10; section_id++) { + try { + auto table = this->get_table(episode, mode, difficulty, section_id); + fprintf(stream, "============ %s %s %s %s\n", + name_for_mode(mode), name_for_episode(episode), name_for_difficulty(difficulty), name_for_section_id(section_id)); + table->print(stream); + } catch (const runtime_error&) { + } + } + } + } + } +} + +CommonItemSet::Table::Table(const 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 { @@ -179,41 +492,6 @@ void CommonItemSet::Table::parse_itempt_t(const StringReader& r, bool is_v3) { this->box_item_class_prob_table = r.pget, 7>>(offsets.box_item_class_prob_table_offset); } -void CommonItemSet::Table::print_enemy_table(FILE* stream) const { - const auto& meseta_ranges = this->enemy_meseta_ranges; - const auto& drop_probs = this->enemy_type_drop_probs; - const auto& item_classes = this->enemy_item_classes; - // const parray, 0x64>& enemy_meseta_ranges() const; - // const parray& enemy_type_drop_probs() const; - // const parray& enemy_item_classes() const; - fprintf(stream, "## $_LOW $_HIGH DAR ITEM\n"); - for (size_t z = 0; z < 0x64; z++) { - const char* item_class_name = "__UNKNOWN__"; - switch (item_classes[z]) { - case 0x00: - item_class_name = "WEAPON"; - break; - case 0x01: - item_class_name = "ARMOR"; - break; - case 0x02: - item_class_name = "SHIELD"; - break; - case 0x03: - item_class_name = "UNIT"; - break; - case 0x04: - item_class_name = "TOOL"; - break; - case 0x05: - item_class_name = "MESETA"; - break; - } - fprintf(stream, "%02zX %5hu %5hu %3hhu %02hX (%s)\n", - z, meseta_ranges[z].min, meseta_ranges[z].max, drop_probs[z], item_classes[z], item_class_name); - } -} - uint16_t CommonItemSet::key_for_table(Episode episode, GameMode mode, uint8_t difficulty, uint8_t secid) { // Bits: -----EEEMMDDSSSS return (((static_cast(episode) << 8) & 0x0700) | @@ -241,7 +519,7 @@ AFSV2CommonItemSet::AFSV2CommonItemSet( for (size_t section_id = 0; section_id < 10; section_id++) { auto entry = pt_afs.get(difficulty * 10 + section_id); StringReader r(entry.first, entry.second); - auto table = make_shared(r, false, false); + auto table = 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); @@ -253,7 +531,7 @@ AFSV2CommonItemSet::AFSV2CommonItemSet( AFSArchive ct_afs(ct_afs_data); for (size_t difficulty = 0; difficulty < 4; difficulty++) { auto r = ct_afs.get_reader(difficulty * 10); - auto table = make_shared
(r, false, false); + auto table = 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); } @@ -305,7 +583,7 @@ GSLV3V4CommonItemSet::GSLV3V4CommonItemSet(std::shared_ptr gs throw; } } - auto table = make_shared
(r, is_big_endian, true); + auto table = 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); @@ -315,7 +593,7 @@ GSLV3V4CommonItemSet::GSLV3V4CommonItemSet(std::shared_ptr gs if (episode != Episode::EP4) { for (size_t difficulty = 0; difficulty < 4; difficulty++) { auto r = gsl.get_reader(filename_for_table(episode, difficulty, 0, true)); - auto table = make_shared
(r, is_big_endian, true); + auto table = 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); } @@ -324,6 +602,33 @@ GSLV3V4CommonItemSet::GSLV3V4CommonItemSet(std::shared_ptr gs } } +JSONCommonItemSet::JSONCommonItemSet(const JSON& json) { + for (const auto& mode_it : json.as_dict()) { + static const unordered_map mode_keys( + {{"Normal", GameMode::NORMAL}, {"Battle", GameMode::BATTLE}, {"Challenge", GameMode::CHALLENGE}, {"Solo", GameMode::SOLO}}); + GameMode mode = mode_keys.at(mode_it.first); + + for (const auto& episode_it : mode_it.second->as_dict()) { + static const unordered_map episode_keys( + {{"Episode1", Episode::EP1}, {"Episode2", Episode::EP2}, {"Episode4", Episode::EP4}}); + Episode episode = episode_keys.at(episode_it.first); + + for (const auto& difficulty_it : episode_it.second->as_dict()) { + static const unordered_map difficulty_keys( + {{"Normal", 0}, {"Hard", 1}, {"VeryHard", 2}, {"Ultimate", 3}}); + uint8_t difficulty = difficulty_keys.at(difficulty_it.first); + + for (const auto& section_id_it : difficulty_it.second->as_dict()) { + uint8_t section_id = section_id_for_name(section_id_it.first); + this->tables.emplace( + this->key_for_table(episode, mode, difficulty, section_id), + make_shared
(*section_id_it.second, episode)); + } + } + } + } +} + RELFileSet::RELFileSet(std::shared_ptr data) : data(data), r(*this->data) {} diff --git a/src/CommonItemSet.hh b/src/CommonItemSet.hh index 693d536f..cf840dea 100644 --- a/src/CommonItemSet.hh +++ b/src/CommonItemSet.hh @@ -14,7 +14,8 @@ public: class Table { public: Table() = delete; - Table(const StringReader& r, bool big_endian, bool is_v3); + Table(const JSON& json, Episode episode); + Table(const StringReader& r, bool big_endian, bool is_v3, Episode episode); template struct Range { @@ -22,6 +23,7 @@ public: IntT max; } __packed__; + Episode episode; parray base_weapon_type_prob_table; parray subtype_base_table; parray subtype_area_length_table; @@ -45,8 +47,8 @@ public: parray unit_max_stars_table; parray, 7> box_item_class_prob_table; - void print_enemy_table(FILE* stream) const; JSON json() const; + void print(FILE* stream) const; private: template @@ -263,6 +265,7 @@ public: std::shared_ptr get_table(Episode episode, GameMode mode, uint8_t difficulty, uint8_t secid) const; JSON json() const; + void print(FILE* stream) const; protected: CommonItemSet() = default; @@ -282,6 +285,11 @@ public: GSLV3V4CommonItemSet(std::shared_ptr gsl_data, bool is_big_endian); }; +class JSONCommonItemSet : public CommonItemSet { +public: + explicit JSONCommonItemSet(const JSON& json); +}; + // Note: There are clearly better ways of doing this, but this implementation // closely follows what the original code in the client does. template diff --git a/src/ItemNameIndex.cc b/src/ItemNameIndex.cc index e0a4c4ce..ed0e0267 100644 --- a/src/ItemNameIndex.cc +++ b/src/ItemNameIndex.cc @@ -852,7 +852,7 @@ void ItemNameIndex::print_table(FILE* stream) const { t.amount.load(), t.tech.load(), t.cost.load(), - t.item_flag.load(), + t.item_flags.load(), stars, divisor_str.c_str(), name.c_str()); diff --git a/src/ItemParameterTable.cc b/src/ItemParameterTable.cc index a3ea6a56..aa105ebb 100644 --- a/src/ItemParameterTable.cc +++ b/src/ItemParameterTable.cc @@ -397,7 +397,7 @@ ItemParameterTable::ToolV4 ItemParameterTable::ToolV1V2::to_v4() const { ret.amount = this->amount; ret.tech = this->tech; ret.cost = this->cost; - ret.item_flag = this->item_flag; + ret.item_flags = this->item_flags; return ret; } @@ -410,7 +410,7 @@ ItemParameterTable::ToolV4 ItemParameterTable::ToolV3T::to_v4() con ret.amount = this->amount.load(); ret.tech = this->tech.load(); ret.cost = this->cost.load(); - ret.item_flag = this->item_flag.load(); + ret.item_flags = this->item_flags.load(); return ret; } @@ -1008,7 +1008,7 @@ uint8_t ItemParameterTable::get_item_base_stars(const ItemData& item) const { const auto& def = (item.data1[1] == 2) ? this->get_tool(2, item.data1[4]) : this->get_tool(item.data1[1], item.data1[2]); - return (def.item_flag & 0x80) ? 12 : 0; + return (def.item_flags & 0x80) ? 12 : 0; } else { return 0; } diff --git a/src/ItemParameterTable.hh b/src/ItemParameterTable.hh index a2c2f5b1..b609c5c7 100644 --- a/src/ItemParameterTable.hh +++ b/src/ItemParameterTable.hh @@ -341,7 +341,16 @@ public: /* 04 */ U16T amount = 0; /* 06 */ U16T tech = 0; /* 08 */ S32T cost = 0; - /* 0C */ U32T item_flag = 0; + // Bits in item_flags: + // 00000001 - ever usable by player ("Use" appears in inventory menu) + // 00000002 - unknown + // 00000004 - unknown + // 00000008 - usable by android characters + // 00000010 - usable in Pioneer 2 / Lab + // 00000020 - usable in boss arenas + // 00000040 - usable in Challenge mode + // 00000080 - is rare (renders as red box; V3+) + /* 0C */ U32T item_flags = 0; /* 10 */ } __packed__; diff --git a/src/Main.cc b/src/Main.cc index eeb2851e..57ae8031 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -1604,6 +1604,42 @@ Action a_convert_rare_item_set( throw runtime_error("cannot determine output format; use a filename ending with .json, .gsl, .gslb, or .afs"); } }); +Action a_convert_common_item_set( + "convert-common-item-set", "\ + convert-common-item-set INPUT-FILENAME [OUTPUT-FILENAME]\n\ + Convert the input rare item set to a JSON representation and write it to\n\ + OUTPUT-FILENAME or stdout. The input filename must end in one of the\n\ + following extensions:\n\ + .json (newserv JSON common item table)\n\ + .gsl (PSO BB little-endian GSL archive)\n\ + .gslb (PSO GC big-endian GSL archive)\n", + +[](Arguments& args) { + string input_filename = args.get(1, false); + if (input_filename.empty() || (input_filename == "-")) { + throw runtime_error("input filename must be given"); + } + + auto data = make_shared(read_input_data(args)); + shared_ptr cs; + if (ends_with(input_filename, ".json")) { + cs = make_shared(JSON::parse(*data)); + } else if (ends_with(input_filename, ".gsl")) { + cs = make_shared(data, args.get("big-endian")); + } else if (ends_with(input_filename, ".gslb")) { + cs = make_shared(data, true); + } else { + throw runtime_error("cannot determine input format; use a filename ending with .json, .gsl, .gslb, or .afs"); + } + + const string& output_filename = args.get(2, false); + if (output_filename.empty()) { + cs->print(stdout); + } else { + auto json = cs->json(); + string json_data = json.serialize(JSON::SerializeOption::FORMAT | JSON::SerializeOption::HEX_INTEGERS | JSON::SerializeOption::SORT_DICT_KEYS); + write_output_data(args, json_data.data(), json_data.size(), "json"); + } + }); Action a_describe_item( "describe-item", "\