From 4442ca02503a0550e6b81f2a334dcdf7da6cae28 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Thu, 29 Jun 2023 22:20:39 -0700 Subject: [PATCH] support JSON rare item sets --- src/EnemyType.hh | 1 + src/Main.cc | 138 ++++++++++++++++++++++++++++++++++++++--- src/RareItemSet.cc | 39 ++++++++++-- src/ReceiveCommands.cc | 2 +- src/ServerState.cc | 13 +++- src/StaticGameData.cc | 93 +++++++++++++++++++++++---- src/StaticGameData.hh | 3 + 7 files changed, 258 insertions(+), 31 deletions(-) diff --git a/src/EnemyType.hh b/src/EnemyType.hh index 7eae91da..414dd790 100644 --- a/src/EnemyType.hh +++ b/src/EnemyType.hh @@ -132,6 +132,7 @@ enum class EnemyType { ZOL_GIBBON, ZU, ZU_ALT, + MAX_ENEMY_TYPE, }; template <> diff --git a/src/Main.cc b/src/Main.cc index effbadd7..9347b634 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -232,7 +232,8 @@ enum class Behavior { DECODE_SJIS, EXTRACT_GSL, EXTRACT_BML, - FORMAT_ITEMRT_REL, + FORMAT_RARE_ITEM_SET, + CONVERT_ITEMRT_REL_TO_JSON, SHOW_EP3_DATA, DESCRIBE_ITEM, ENCODE_ITEM, @@ -265,7 +266,8 @@ static bool behavior_takes_input_filename(Behavior b) { (b == Behavior::DECODE_QUEST_FILE) || (b == Behavior::DISASSEMBLE_QUEST_SCRIPT) || (b == Behavior::DECODE_SJIS) || - (b == Behavior::FORMAT_ITEMRT_REL) || + (b == Behavior::FORMAT_RARE_ITEM_SET) || + (b == Behavior::CONVERT_ITEMRT_REL_TO_JSON) || (b == Behavior::EXTRACT_GSL) || (b == Behavior::EXTRACT_BML) || (b == Behavior::DESCRIBE_ITEM) || @@ -290,6 +292,7 @@ static bool behavior_takes_output_filename(Behavior b) { (b == Behavior::DECRYPT_GCI_SAVE) || (b == Behavior::ENCRYPT_GCI_SAVE) || (b == Behavior::DISASSEMBLE_QUEST_SCRIPT) || + (b == Behavior::CONVERT_ITEMRT_REL_TO_JSON) || (b == Behavior::DECODE_SJIS) || (b == Behavior::EXTRACT_GSL) || (b == Behavior::EXTRACT_BML); @@ -316,6 +319,7 @@ int main(int argc, char** argv) { size_t bytes = 0; ssize_t compression_level = 0; bool compress_optimal = false; + bool json = false; const char* find_decryption_seed_ciphertext = nullptr; vector find_decryption_seed_plaintexts; const char* input_filename = nullptr; @@ -392,6 +396,8 @@ int main(int argc, char** argv) { skip_little_endian = true; } else if (!strcmp(argv[x], "--skip-big-endian")) { skip_big_endian = true; + } else if (!strcmp(argv[x], "--json")) { + json = true; } else if (!strncmp(argv[x], "--require-password=", 19)) { replay_required_password = &argv[x][19]; } else if (!strncmp(argv[x], "--require-access-key=", 21)) { @@ -459,8 +465,10 @@ int main(int argc, char** argv) { behavior = Behavior::DISASSEMBLE_QUEST_SCRIPT; } else if (!strcmp(argv[x], "cat-client")) { behavior = Behavior::CAT_CLIENT; - } else if (!strcmp(argv[x], "format-itemrt-rel")) { - behavior = Behavior::FORMAT_ITEMRT_REL; + } else if (!strcmp(argv[x], "format-rare-item-set")) { + behavior = Behavior::FORMAT_RARE_ITEM_SET; + } else if (!strcmp(argv[x], "convert-itemrt-rel-to-json")) { + behavior = Behavior::CONVERT_ITEMRT_REL_TO_JSON; } else if (!strcmp(argv[x], "show-ep3-data")) { behavior = Behavior::SHOW_EP3_DATA; } else if (!strcmp(argv[x], "describe-item")) { @@ -552,12 +560,16 @@ int main(int argc, char** argv) { } } else if (behavior == Behavior::DISASSEMBLE_QUEST_SCRIPT) { filename += ".txt"; + } else if (behavior == Behavior::CONVERT_ITEMRT_REL_TO_JSON) { + filename += ".json"; } else { filename += ".dec"; } save_file(filename, data, size); - } else if (isatty(fileno(stdout)) && (behavior != Behavior::DISASSEMBLE_QUEST_SCRIPT)) { + } else if (isatty(fileno(stdout)) && + (behavior != Behavior::DISASSEMBLE_QUEST_SCRIPT) && + (behavior != Behavior::CONVERT_ITEMRT_REL_TO_JSON)) { // If stdout is a terminal and the data is not known to be text, use // print_data to write the result print_data(stdout, data, size); @@ -1105,9 +1117,14 @@ int main(int argc, char** argv) { break; } - case Behavior::FORMAT_ITEMRT_REL: { + case Behavior::FORMAT_RARE_ITEM_SET: { shared_ptr data(new string(read_input_data())); - RELRareItemSet rs(data); + shared_ptr rs; + if (json) { + rs.reset(new JSONRareItemSet(JSONObject::parse(read_input_data()))); + } else { + rs.reset(new RELRareItemSet(data)); + } auto format_drop = +[](const RareItemSet::ExpandedDrop& r) -> string { ItemData item; @@ -1132,7 +1149,7 @@ int main(int argc, char** argv) { fprintf(stdout, " Monster rares:\n"); for (size_t z = 0; z < 0x65; z++) { - for (const auto& spec : rs.get_enemy_specs(mode, episode, difficulty, section_id, z)) { + for (const auto& spec : rs->get_enemy_specs(mode, episode, difficulty, section_id, z)) { string s = format_drop(spec); fprintf(stdout, " %02zX: %s\n", z, s.c_str()); } @@ -1140,7 +1157,7 @@ int main(int argc, char** argv) { fprintf(stdout, " Box rares:\n"); for (size_t area = 0; area < 0x12; area++) { - for (const auto& spec : rs.get_box_specs(mode, episode, difficulty, section_id, area)) { + for (const auto& spec : rs->get_box_specs(mode, episode, difficulty, section_id, area)) { string s = format_drop(spec); fprintf(stdout, " (area %02zX) %s\n", area, s.c_str()); } @@ -1159,6 +1176,109 @@ int main(int argc, char** argv) { } } } + + break; + } + + case Behavior::CONVERT_ITEMRT_REL_TO_JSON: { + shared_ptr data(new string(read_input_data())); + RELRareItemSet rs(data); + + // Compute the mapping of {rt_index: EnemyType} + vector> rt_index_to_enemy_type; + for (size_t z = 0; z < static_cast(EnemyType::MAX_ENEMY_TYPE); z++) { + EnemyType t = static_cast(z); + try { + uint8_t rt_index = rare_table_index_for_enemy_type(t); + if (rt_index >= rt_index_to_enemy_type.size()) { + rt_index_to_enemy_type.resize(rt_index + 1); + } + rt_index_to_enemy_type[rt_index].emplace_back(t); + } catch (const exception&) { + } + } + + JSONObject::dict_type episodes_dict; + static const array episodes = {Episode::EP1, Episode::EP2, Episode::EP4}; + for (Episode episode : episodes) { + JSONObject::dict_type difficulty_dict; + for (uint8_t difficulty = 0; difficulty < 4; difficulty++) { + JSONObject::dict_type section_id_dict; + for (uint8_t section_id = 0; section_id < 10; section_id++) { + JSONObject::dict_type collection_dict; + + for (size_t rt_index = 0; rt_index < rt_index_to_enemy_type.size(); rt_index++) { + const auto& enemy_types = rt_index_to_enemy_type[rt_index]; + if (enemy_types.empty()) { + continue; + } + + for (const auto& spec : rs.get_enemy_specs(GameMode::NORMAL, episode, difficulty, section_id, rt_index)) { + JSONObject::list_type spec_list; + + auto frac = reduce_fraction(spec.probability, 0x100000000); + spec_list.emplace_back(make_json_str(string_printf("%" PRIu64 "/%" PRIu64, frac.first, frac.second))); + + ItemData item; + item.data1[0] = spec.item_code[0]; + item.data1[1] = spec.item_code[1]; + item.data1[2] = spec.item_code[2]; + spec_list.emplace_back(make_json_str(item.name(false))); + + JSONObject::list_type specs_list; + specs_list.emplace_back(make_json_list(std::move(spec_list))); + auto specs_json = make_json_list(std::move(specs_list)); + for (const auto& enemy_type : enemy_types) { + collection_dict.emplace(name_for_enum(enemy_type), specs_json); + } + } + } + + for (size_t area = 0; area < 0x12; area++) { + JSONObject::list_type area_list; + + for (const auto& spec : rs.get_box_specs(GameMode::NORMAL, episode, difficulty, section_id, area)) { + JSONObject::list_type spec_list; + + auto frac = reduce_fraction(spec.probability, 0x100000000); + spec_list.emplace_back(make_json_str(string_printf("%" PRIu64 "/%" PRIu64, frac.first, frac.second))); + + ItemData item; + item.data1[0] = spec.item_code[0]; + item.data1[1] = spec.item_code[1]; + item.data1[2] = spec.item_code[2]; + spec_list.emplace_back(make_json_str(item.name(false))); + + area_list.emplace_back(make_json_list(std::move(spec_list))); + } + + if (!area_list.empty()) { + collection_dict.emplace( + string_printf("Box-%s", name_for_area(episode, area)), + make_json_list(std::move(area_list))); + } + } + + section_id_dict.emplace(name_for_section_id(section_id), make_json_dict(std::move(collection_dict))); + } + difficulty_dict.emplace(name_for_difficulty(difficulty), make_json_dict(std::move(section_id_dict))); + } + episodes_dict.emplace(name_for_episode(episode), make_json_dict(std::move(difficulty_dict))); + } + + JSONObject::dict_type root_dict; + root_dict.emplace("Normal", make_json_dict(std::move(episodes_dict))); + auto root_json = make_json_dict(std::move(root_dict)); + string json_data = root_json->serialize( + JSONObject::SerializeOption::FORMAT | + JSONObject::SerializeOption::HEX_INTEGERS | + JSONObject::SerializeOption::SORT_DICT_KEYS); + write_output_data(json_data.data(), json_data.size()); + + // {"Normal": {"Episode1": {"Ultimate": {"Viridia": {"GRASS_ASSASSIN": [...], "Box-Ruins2": [...]}}}}} + // ["1/32", "Slicer of Assassin"] + // [0xE0000000, 0x031000] + break; } diff --git a/src/RareItemSet.cc b/src/RareItemSet.cc index 65f60065..d8401d7f 100644 --- a/src/RareItemSet.cc +++ b/src/RareItemSet.cc @@ -190,7 +190,7 @@ JSONRareItemSet::JSONRareItemSet(std::shared_ptr json) { uint8_t section_id = section_id_for_name(section_id_it.first); auto& collection = this->collections[this->key_for_params(mode, episode, difficulty, section_id)]; - for (const auto& item_it : difficulty_it.second->as_dict()) { + for (const auto& item_it : section_id_it.second->as_dict()) { vector* target; if (starts_with(item_it.first, "Box-")) { uint8_t area = drop_area_for_name(item_it.first.substr(4)); @@ -209,11 +209,38 @@ JSONRareItemSet::JSONRareItemSet(std::shared_ptr json) { for (const auto& spec_json : item_it.second->as_list()) { auto& spec_list = spec_json->as_list(); auto& d = target->emplace_back(); - d.probability = spec_list.at(0)->as_int(); - uint32_t item_code = spec_list.at(1)->as_int(); - d.item_code[0] = (item_code >> 16) & 0xFF; - d.item_code[1] = (item_code >> 8) & 0xFF; - d.item_code[2] = item_code & 0xFF; + + auto prob_desc = spec_list.at(1); + if (prob_desc->is_int()) { + d.probability = spec_list.at(0)->as_int(); + } else if (prob_desc->is_string()) { + auto tokens = split(prob_desc->as_string(), '/'); + if (tokens.size() != 2) { + throw runtime_error("invalid probability specification"); + } + uint64_t numerator = stoull(tokens[0], nullptr, 0); + uint64_t denominator = stoull(tokens[1], nullptr, 0); + if (numerator == denominator) { + d.probability = 0xFFFFFFFF; + } else { + d.probability = (static_cast(numerator) << 32) / denominator; + } + } + + auto item_desc = spec_list.at(1); + if (item_desc->is_int()) { + uint32_t item_code = spec_list.at(1)->as_int(); + d.item_code[0] = (item_code >> 16) & 0xFF; + d.item_code[1] = (item_code >> 8) & 0xFF; + d.item_code[2] = item_code & 0xFF; + } else if (item_desc->is_string()) { + ItemData data(item_desc->as_string()); + d.item_code[0] = data.data1[0]; + d.item_code[1] = data.data1[1]; + d.item_code[2] = data.data1[2]; + } else { + throw runtime_error("invalid item description type"); + } } } } diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 308f87ac..1852b02d 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -3178,7 +3178,7 @@ shared_ptr create_game_generic( s->weapon_random_sets.at(game->difficulty), s->item_parameter_table, game->episode, - game->mode, + (game->mode == GameMode::SOLO) ? GameMode::NORMAL : game->mode, game->difficulty, game->section_id, game->random_seed)); diff --git a/src/ServerState.cc b/src/ServerState.cc index 20fa644f..70902ea5 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -830,9 +830,16 @@ void ServerState::load_level_table() { } void ServerState::load_item_tables() { - config_log.info("Loading rare item table"); - this->rare_item_set.reset(new RELRareItemSet( - this->load_bb_file("ItemRT.rel"))); + try { + config_log.info("Loading JSON rare item table"); + auto json = JSONObject::parse(load_file("system/blueburst/rare-table.json")); + this->rare_item_set.reset(new JSONRareItemSet(json)); + } catch (const exception& e) { + config_log.info("Failed to load JSON rare item table: %s", e.what()); + config_log.info("Loading REL rare item table"); + this->rare_item_set.reset(new RELRareItemSet( + this->load_bb_file("ItemRT.rel"))); + } // Note: These files don't exist in BB, so we use the GC versions of them // instead. This doesn't include Episode 4 of course, so we use Episode 1 diff --git a/src/StaticGameData.cc b/src/StaticGameData.cc index c1cc1900..a827b229 100644 --- a/src/StaticGameData.cc +++ b/src/StaticGameData.cc @@ -4,18 +4,6 @@ using namespace std; -size_t area_limit_for_episode(Episode ep) { - switch (ep) { - case Episode::EP1: - case Episode::EP2: - return 17; - case Episode::EP4: - return 10; - default: - return 0; - } -} - bool episode_has_arpg_semantics(Episode ep) { return (ep == Episode::EP1) || (ep == Episode::EP2) || (ep == Episode::EP4); } @@ -648,3 +636,84 @@ uint8_t drop_area_for_name(const std::string& name) { }); return areas.at(tolower(name)); } + +static const array ep1_area_names = { + "Pioneer2", + "Forest1", + "Forest2", + "Caves1", + "Caves2", + "Caves3", + "Mines1", + "Mines2", + "Ruins1", + "Ruins2", + "Ruins3", + "Dragon", + "DeRolLe", + "VolOpt", + "DarkFalz", + "Lobby", + "Battle1", + "Battle2", +}; + +static const array ep2_area_names = { + "Pioneer2", + "TempleAlpha", + "TempleBeta", + "SpaceshipAlpha", + "CentralControlArea", + "JungleNorth", + "JungleSouth", + "Mountain", + "Seaside", + "SeabedUpper", + "SeabedLower", + "GalGryphon", + "OlgaFlow", + "BarbaRay", + "GolDragon", + "SeasideNight", + "Tower", +}; + +static const array ep4_area_names = { + "Pioneer2", + "CraterEast", + "CraterWest", + "CraterSouth", + "CraterNorth", + "CraterInterior", + "Desert1", + "Desert2", + "Desert3", + "SaintMillion", + "Purgatory", +}; + +size_t area_limit_for_episode(Episode ep) { + switch (ep) { + case Episode::EP1: + return ep1_area_names.size() - 1; + case Episode::EP2: + return ep2_area_names.size() - 1; + case Episode::EP4: + return ep4_area_names.size() - 1; + default: + return 0; + } +} + +const char* name_for_area(Episode episode, uint8_t area) { + switch (episode) { + case Episode::EP1: + return ep1_area_names.at(area); + case Episode::EP2: + return ep2_area_names.at(area); + case Episode::EP4: + return ep4_area_names.at(area); + default: + throw logic_error("invalid episode for drop area"); + } +} diff --git a/src/StaticGameData.hh b/src/StaticGameData.hh index 0122d3b1..019b4fcc 100644 --- a/src/StaticGameData.hh +++ b/src/StaticGameData.hh @@ -79,3 +79,6 @@ extern const std::vector name_for_mag_color; extern const std::unordered_map mag_color_for_name; uint8_t drop_area_for_name(const std::string& name); + +size_t area_limit_for_episode(Episode ep); +const char* name_for_area(Episode episode, uint8_t drop_area);