From 911b17df7e448b11f2f4ede5de6bd35ccf0ee9e7 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Fri, 4 Aug 2023 22:40:18 -0700 Subject: [PATCH] improve ep3 data formatting --- README.md | 2 +- src/Episode3/DataIndex.cc | 347 ++++++++++++++++++++++++++++++++------ src/Episode3/DataIndex.hh | 9 +- src/Main.cc | 38 ++--- 4 files changed, 327 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index f5e3ff43..3db03d9c 100644 --- a/README.md +++ b/README.md @@ -399,7 +399,7 @@ newserv has many CLI options, which can be used to access functionality other th * Convert quests in .gci, .vms, .dlq, or .qst format to .bin/.dat format (`decode-gci`, `decode-vms`, `decode-dlq`, `decode-qst`) * Convert quests in .bin/.dat to .qst format (`encode-qst`) * Disassemble quest scripts (`disassemble-quest-script`) -* Format Episode 3 game data in a human-readable manner (`show-ep3-data`) +* Format Episode 3 game data in a human-readable manner (`show-ep3-maps`, `show-ep3-cards`) * Convert item data to a human-readable description, or vice versa (`describe-item`, `encode-item`) * Connect to another PSO server and pretend to be a client (`cat-client`) * Replay a session log for testing (`replay-log`) diff --git a/src/Episode3/DataIndex.cc b/src/Episode3/DataIndex.cc index afc51a13..554d9cd7 100644 --- a/src/Episode3/DataIndex.cc +++ b/src/Episode3/DataIndex.cc @@ -18,6 +18,99 @@ using namespace std; namespace Episode3 { +const char* name_for_link_color(uint8_t color) { + switch (color) { + case 1: + return "blue"; // HP halver + case 2: + return "red"; // Physical attacks + case 3: + return "yellow"; // Techniques + case 4: + return "brown"; // Leukon Knight + case 5: + return "orange"; // Penetrate/confuse + case 6: + return "purple"; // Instant death + case 7: + return "white"; // Castor + case 8: + return "gray"; // Pollux + case 9: + return "green"; // Status effects + default: + throw invalid_argument("unknown color"); + } +} + +const char* name_for_card_type(CardType type) { + switch (type) { + case CardType::HUNTERS_SC: + return "HUNTERS_SC"; + case CardType::ARKZ_SC: + return "ARKZ_SC"; + case CardType::ITEM: + return "ITEM"; + case CardType::CREATURE: + return "CREATURE"; + case CardType::ACTION: + return "ACTION"; + case CardType::ASSIST: + return "ASSIST"; + case CardType::INVALID_FF: + return "INVALID_FF"; + default: + throw invalid_argument("invalid card type"); + } +} + +const char* name_for_card_class(CardClass cc) { + switch (cc) { + case CardClass::HU_SC: + return "HU_SC"; + case CardClass::RA_SC: + return "RA_SC"; + case CardClass::FO_SC: + return "FO_SC"; + case CardClass::NATIVE_CREATURE: + return "NATIVE_CREATURE"; + case CardClass::A_BEAST_CREATURE: + return "A_BEAST_CREATURE"; + case CardClass::MACHINE_CREATURE: + return "MACHINE_CREATURE"; + case CardClass::DARK_CREATURE: + return "DARK_CREATURE"; + case CardClass::GUARD_ITEM: + return "GUARD_ITEM"; + case CardClass::MAG_ITEM: + return "MAG_ITEM"; + case CardClass::SWORD_ITEM: + return "SWORD_ITEM"; + case CardClass::GUN_ITEM: + return "GUN_ITEM"; + case CardClass::CANE_ITEM: + return "CANE_ITEM"; + case CardClass::ATTACK_ACTION: + return "ATTACK_ACTION"; + case CardClass::DEFENSE_ACTION: + return "DEFENSE_ACTION"; + case CardClass::TECH: + return "TECH"; + case CardClass::PHOTON_BLAST: + return "PHOTON_BLAST"; + case CardClass::CONNECT_ONLY_ATTACK_ACTION: + return "CONNECT_ONLY_ATTACK_ACTION"; + case CardClass::BOSS_ATTACK_ACTION: + return "BOSS_ATTACK_ACTION"; + case CardClass::BOSS_TECH: + return "BOSS_TECH"; + case CardClass::ASSIST: + return "ASSIST"; + default: + throw invalid_argument("invalid card class"); + } +} + const char* name_for_attack_medium(AttackMedium medium) { switch (medium) { case AttackMedium::UNKNOWN: @@ -35,6 +128,83 @@ const char* name_for_attack_medium(AttackMedium medium) { } } +const char* name_for_criterion_code(CriterionCode code) { + switch (code) { + case CriterionCode::NONE: + return "NONE"; + case CriterionCode::HU_CLASS_SC: + return "HU_CLASS_SC"; + case CriterionCode::RA_CLASS_SC: + return "RA_CLASS_SC"; + case CriterionCode::FO_CLASS_SC: + return "FO_CLASS_SC"; + case CriterionCode::SAME_TEAM: + return "SAME_TEAM"; + case CriterionCode::SAME_PLAYER: + return "SAME_PLAYER"; + case CriterionCode::SAME_TEAM_NOT_SAME_PLAYER: + return "SAME_TEAM_NOT_SAME_PLAYER"; + case CriterionCode::UNKNOWN_07: + return "UNKNOWN_07"; + case CriterionCode::NOT_SC: + return "NOT_SC"; + case CriterionCode::SC: + return "SC"; + case CriterionCode::HU_OR_RA_CLASS_SC: + return "HU_OR_RA_CLASS_SC"; + case CriterionCode::HUNTER_HUMAN_SC: + return "HUNTER_HUMAN_SC"; + case CriterionCode::HUNTER_HU_CLASS_MALE_SC: + return "HUNTER_HU_CLASS_MALE_SC"; + case CriterionCode::HUNTER_FEMALE_SC: + return "HUNTER_FEMALE_SC"; + case CriterionCode::HUNTER_HU_OR_FO_CLASS_HUMAN_SC: + return "HUNTER_HU_OR_FO_CLASS_HUMAN_SC"; + case CriterionCode::HUNTER_HU_CLASS_ANDROID_SC: + return "HUNTER_HU_CLASS_ANDROID_SC"; + case CriterionCode::UNKNOWN_10: + return "UNKNOWN_10"; + case CriterionCode::UNKNOWN_11: + return "UNKNOWN_11"; + case CriterionCode::HUNTER_HUNEWEARL_CLASS_SC: + return "HUNTER_HUNEWEARL_CLASS_SC"; + case CriterionCode::HUNTER_RA_CLASS_MALE_SC: + return "HUNTER_RA_CLASS_MALE_SC"; + case CriterionCode::HUNTER_RA_CLASS_FEMALE_SC: + return "HUNTER_RA_CLASS_FEMALE_SC"; + case CriterionCode::HUNTER_RA_OR_FO_CLASS_FEMALE_SC: + return "HUNTER_RA_OR_FO_CLASS_FEMALE_SC"; + case CriterionCode::HUNTER_HU_OR_RA_CLASS_HUMAN_SC: + return "HUNTER_HU_OR_RA_CLASS_HUMAN_SC"; + case CriterionCode::HUNTER_RA_CLASS_ANDROID_SC: + return "HUNTER_RA_CLASS_ANDROID_SC"; + case CriterionCode::HUNTER_FO_CLASS_FEMALE_SC: + return "HUNTER_FO_CLASS_FEMALE_SC"; + case CriterionCode::HUNTER_FEMALE_HUMAN_SC: + return "HUNTER_FEMALE_HUMAN_SC"; + case CriterionCode::HUNTER_ANDROID_SC: + return "HUNTER_ANDROID_SC"; + case CriterionCode::HU_OR_FO_CLASS_SC: + return "HU_OR_FO_CLASS_SC"; + case CriterionCode::RA_OR_FO_CLASS_SC: + return "RA_OR_FO_CLASS_SC"; + case CriterionCode::PHYSICAL_OR_UNKNOWN_ATTACK_MEDIUM: + return "PHYSICAL_OR_UNKNOWN_ATTACK_MEDIUM"; + case CriterionCode::TECH_OR_UNKNOWN_ATTACK_MEDIUM: + return "TECH_OR_UNKNOWN_ATTACK_MEDIUM"; + case CriterionCode::PHYSICAL_OR_TECH_OR_UNKNOWN_ATTACK_MEDIUM: + return "PHYSICAL_OR_TECH_OR_UNKNOWN_ATTACK_MEDIUM"; + case CriterionCode::UNKNOWN_20: + return "UNKNOWN_20"; + case CriterionCode::UNKNOWN_21: + return "UNKNOWN_21"; + case CriterionCode::UNKNOWN_22: + return "UNKNOWN_22"; + default: + throw invalid_argument("invalid criterion code"); + } +} + Location::Location() : Location(0, 0) {} Location::Location(uint8_t x, uint8_t y) : Location(x, y, Direction::RIGHT) {} Location::Location(uint8_t x, uint8_t y, Direction direction) @@ -140,15 +310,6 @@ bool card_class_is_tech_like(CardClass cc) { (cc == CardClass::BOSS_TECH); } -static const vector name_for_card_type({ - "HunterSC", - "ArkzSC", - "Item", - "Creature", - "Action", - "Assist", -}); - static const unordered_map description_for_expr_token({ {"f", "Number of FCs controlled by current SC"}, {"d", "Die roll"}, @@ -724,7 +885,14 @@ string string_for_colors(const parray& colors) { string ret; for (size_t x = 0; x < 8; x++) { if (colors[x]) { - ret += '0' + colors[x]; + if (!ret.empty()) { + ret += ","; + } + try { + ret += name_for_link_color(colors[x]); + } catch (const invalid_argument) { + ret += string_printf("%02hhX", colors[x]); + } } } if (ret.empty()) { @@ -757,16 +925,27 @@ string string_for_range(const parray& range) { return ret; } -string CardDefinition::str() const { +string CardDefinition::str(bool single_line) const { string type_str; try { - type_str = name_for_card_type.at(static_cast(this->type)); - } catch (const out_of_range&) { + type_str = name_for_card_type(this->type); + } catch (const invalid_argument&) { type_str = string_printf("%02hhX", static_cast(this->type)); } + string criterion_str; + try { + criterion_str = name_for_criterion_code(this->usable_criterion); + } catch (const invalid_argument&) { + criterion_str = string_printf("%02hhX", static_cast(this->usable_criterion)); + } + string card_class_str; + try { + card_class_str = name_for_card_class(this->card_class()); + } catch (const invalid_argument&) { + card_class_str = string_printf("%04hX", this->be_card_class.load()); + } string rarity_str = name_for_rarity(this->rarity); string target_mode_str = name_for_target_mode(this->target_mode); - string range_str = string_for_range(this->range); string assist_turns_str = string_for_assist_turns(this->assist_turns); string hp_str = this->hp.str(); string ap_str = this->ap.str(); @@ -780,44 +959,115 @@ string CardDefinition::str() const { if (this->effects[x].is_empty()) { continue; } - if (!effects_str.empty()) { + if (!single_line) { + effects_str += "\n "; + } else if (!effects_str.empty()) { effects_str += ", "; } effects_str += this->effects[x].str(); } - return string_printf( - "[Card: %04" PRIX32 " name=%s type=%s usable_condition=%02hhX rare=%s " - "cost=%hhX+%hhX target=%s range=%s assist_turns=%s cannot_move=%s " - "cannot_attack=%s cannot_drop=%s hp=%s ap=%s tp=%s mv=%s left=%s right=%s " - "top=%s a2=%04hX class=%04hX assist_effect=[%hu, %hu] " - "drop_rates=[%hu, %hu] effects=[%s]]", - this->card_id.load(), - this->en_name.data(), - type_str.c_str(), - static_cast(this->usable_criterion), - rarity_str.c_str(), - this->self_cost, - this->ally_cost, - target_mode_str.c_str(), - range_str.c_str(), - assist_turns_str.c_str(), - this->cannot_move ? "true" : "false", - this->cannot_attack ? "true" : "false", - this->cannot_drop ? "true" : "false", - hp_str.c_str(), - ap_str.c_str(), - tp_str.c_str(), - mv_str.c_str(), - left_str.c_str(), - right_str.c_str(), - top_str.c_str(), - this->unknown_a2.load(), - this->be_card_class.load(), - this->assist_effect[0].load(), - this->assist_effect[1].load(), - this->drop_rates[0].load(), - this->drop_rates[1].load(), - effects_str.c_str()); + if (!single_line && effects_str.empty()) { + effects_str = " (none)"; + } + + if (single_line) { + string range_str = string_for_range(this->range); + return string_printf( + "[Card: %04" PRIX32 " name=%s type=%s usable_condition=%s rare=%s " + "cost=%hhX+%hhX target=%s range=%s assist_turns=%s cannot_move=%s " + "cannot_attack=%s cannot_drop=%s hp=%s ap=%s tp=%s mv=%s left=%s right=%s " + "top=%s a2=%04hX class=%s assist_effect=[%hu, %hu] " + "drop_rates=[%hu, %hu] effects=[%s]]", + this->card_id.load(), + this->en_name.data(), + type_str.c_str(), + criterion_str.c_str(), + rarity_str.c_str(), + this->self_cost, + this->ally_cost, + target_mode_str.c_str(), + range_str.c_str(), + assist_turns_str.c_str(), + this->cannot_move ? "true" : "false", + this->cannot_attack ? "true" : "false", + this->cannot_drop ? "true" : "false", + hp_str.c_str(), + ap_str.c_str(), + tp_str.c_str(), + mv_str.c_str(), + left_str.c_str(), + right_str.c_str(), + top_str.c_str(), + this->unknown_a2.load(), + card_class_str.c_str(), + this->assist_effect[0].load(), + this->assist_effect[1].load(), + this->drop_rates[0].load(), + this->drop_rates[1].load(), + effects_str.c_str()); + + } else { // Not single-line + string range_str; + if (this->range[0] == 0x000FFFFF) { + range_str = " (entire field)"; + } else { + for (size_t x = 0; x < 6; x++) { + range_str += "\n "; + for (size_t z = 0; z < 5; z++) { + bool is_included = ((this->range[x] >> (16 - (z * 4))) & 0xF); + if (x == 4 && z == 2) { + range_str += is_included ? "@" : "#"; + } else { + range_str += is_included ? "*" : "-"; + } + } + } + } + return string_printf( + "\ +Card: %04" PRIX32 " \"%s\"\n\ + Type: %s, class: %s\n\ + Usability condition: %s\n\ + Rarity: %s\n\ + Cost: %hhX (self) + %hhX (ally)\n\ + Target mode: %s\n\ + Range:%s\n\ + Assist turns: %s\n\ + Capabilities: %s move, %s attack\n\ + HP: %s, AP: %s, TP: %s, MV: %s\n\ + Left colors: %s; right colors: %s; top colors: %s\n\ + Unknown a2: %04hX\n\ + Assist effect: [%hu, %hu]\n\ + Drop rates: [%hu, %hu] (%s drop)\n\ + Effects:%s", + this->card_id.load(), + this->en_name.data(), + type_str.c_str(), + card_class_str.c_str(), + criterion_str.c_str(), + rarity_str.c_str(), + this->self_cost, + this->ally_cost, + target_mode_str.c_str(), + range_str.c_str(), + assist_turns_str.c_str(), + this->cannot_move ? "cannot" : "can", + this->cannot_attack ? "cannot" : "can", + hp_str.c_str(), + ap_str.c_str(), + tp_str.c_str(), + mv_str.c_str(), + left_str.c_str(), + right_str.c_str(), + top_str.c_str(), + this->unknown_a2.load(), + this->assist_effect[0].load(), + this->assist_effect[1].load(), + this->drop_rates[0].load(), + this->drop_rates[1].load(), + this->cannot_drop ? "cannot" : "can", + effects_str.c_str()); + } } HPType hp_type_for_name(const char* name) { @@ -1514,6 +1764,7 @@ DataIndex::DataIndex(const string& directory, uint32_t behavior_flags) tags.emplace_back(std::move(tag)); } } + strip_leading_whitespace(orig_text); if (!card_text.emplace(card_id, std::move(orig_text)).second) { throw runtime_error("duplicate card text id"); diff --git a/src/Episode3/DataIndex.hh b/src/Episode3/DataIndex.hh index 626d36a9..61a74a21 100644 --- a/src/Episode3/DataIndex.hh +++ b/src/Episode3/DataIndex.hh @@ -21,6 +21,8 @@ namespace Episode3 { class DataIndex; +const char* name_for_link_color(uint8_t color); + enum BehaviorFlag { SKIP_DECK_VERIFY = 0x00000001, IGNORE_CARD_COUNTS = 0x00000002, @@ -93,6 +95,8 @@ enum class CriterionCode : uint8_t { UNKNOWN_22 = 0x22, }; +const char* name_for_criterion_code(CriterionCode code); + enum class CardRarity : uint8_t { N1 = 0x01, R1 = 0x02, @@ -121,6 +125,8 @@ enum class CardType : uint8_t { END_CARD_LIST = 0xFF, }; +const char* name_for_card_type(CardType type); + enum class CardClass : uint16_t { HU_SC = 0x0000, RA_SC = 0x0001, @@ -144,6 +150,7 @@ enum class CardClass : uint16_t { ASSIST = 0x0028, }; +const char* name_for_card_class(CardClass cc); bool card_class_is_tech_like(CardClass cc); enum class TargetMode : uint8_t { @@ -552,7 +559,7 @@ struct CardDefinition { CardClass card_class() const; void decode_range(); - std::string str() const; + std::string str(bool single_line = true) const; } __attribute__((packed)); // 0x128 bytes in total struct CardDefinitionsFooter { diff --git a/src/Main.cc b/src/Main.cc index c0935d60..77806bd8 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -205,9 +205,12 @@ The actions are:\n\ --gc, and --bb options can be used to select the command format and\n\ encryption. If --bb is used, the --key=KEY-NAME option is also required (as\n\ in decrypt-data above).\n\ - show-ep3-data\n\ - Print the Episode 3 maps and card definitions from the system/ep3 directory\n\ - in a (sort of) human-readable format.\n\ + show-ep3-maps\n\ + Print the Episode 3 maps from the system/ep3 directory in a (sort of)\n\ + human-readable format.\n\ + show-ep3-cards\n\ + Print the Episode 3 card definitions from the system/ep3 directory in a\n\ + human-readable format.\n\ describe-item DATA\n\ Print the name of the item given by DATA (in hex). DATA must not contain\n\ spaces. If DATA is 20 bytes, newserv assumes it contains an unused item ID\n\ @@ -276,7 +279,8 @@ enum class Behavior { EXTRACT_BML, FORMAT_RARE_ITEM_SET, CONVERT_ITEMRT_REL_TO_JSON, - SHOW_EP3_DATA, + SHOW_EP3_MAPS, + SHOW_EP3_CARDS, DESCRIBE_ITEM, ENCODE_ITEM, PARSE_OBJECT_GRAPH, @@ -378,7 +382,6 @@ int main(int argc, char** argv) { const char* replay_required_access_key = ""; const char* replay_required_password = ""; uint32_t root_object_address = 0; - uint16_t ep3_card_id = 0xFFFF; uint8_t domain = 1; uint8_t subdomain = 0xFF; for (int x = 1; x < argc; x++) { @@ -538,8 +541,10 @@ int main(int argc, char** argv) { 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], "show-ep3-maps")) { + behavior = Behavior::SHOW_EP3_MAPS; + } else if (!strcmp(argv[x], "show-ep3-cards")) { + behavior = Behavior::SHOW_EP3_CARDS; } else if (!strcmp(argv[x], "describe-item")) { behavior = Behavior::DESCRIBE_ITEM; } else if (!strcmp(argv[x], "encode-item")) { @@ -1438,11 +1443,12 @@ int main(int argc, char** argv) { break; } - case Behavior::SHOW_EP3_DATA: { + case Behavior::SHOW_EP3_MAPS: + case Behavior::SHOW_EP3_CARDS: { config_log.info("Collecting Episode 3 data"); Episode3::DataIndex index("system/ep3", Episode3::BehaviorFlag::LOAD_CARD_TEXT); - if (ep3_card_id == 0xFFFF) { + if (behavior == Behavior::SHOW_EP3_MAPS) { auto map_ids = index.all_map_ids(); log_info("%zu maps", map_ids.size()); for (uint32_t map_id : map_ids) { @@ -1451,22 +1457,16 @@ int main(int argc, char** argv) { fprintf(stdout, "%s\n", s.c_str()); } + } else { auto card_ids = index.all_card_ids(); log_info("%zu card definitions", card_ids.size()); for (uint32_t card_id : card_ids) { auto entry = index.definition_for_card_id(card_id); - string s = entry->def.str(); + string s = entry->def.str(false); string tags = entry->debug_tags.empty() ? "(none)" : join(entry->debug_tags, ", "); - string text = entry->text.empty() ? "(No text available)" : entry->text; - fprintf(stdout, "%s\nTags: %s\n%s\n\n", s.c_str(), tags.c_str(), text.c_str()); + string text = entry->text.empty() ? "(No text available)" : str_replace_all(entry->text, "\n", "\n "); + fprintf(stdout, "%s\n Tags: %s\n Text:\n %s\n\n", s.c_str(), tags.c_str(), text.c_str()); } - - } else { - auto entry = index.definition_for_card_id(ep3_card_id); - string s = entry->def.str(); - string tags = entry->debug_tags.empty() ? "(none)" : join(entry->debug_tags, ", "); - string text = entry->text.empty() ? "(No text available)" : entry->text; - fprintf(stdout, "%s\nTags: %s\n%s\n", s.c_str(), tags.c_str(), text.c_str()); } break;