From 02584e44581d13c834b4476b05671bd750c37128 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Wed, 27 Sep 2023 10:00:33 -0700 Subject: [PATCH] add card list HTML generator --- TODO.md | 2 +- src/Episode3/DataIndexes.cc | 84 +++++++++++++++++----------- src/Episode3/DataIndexes.hh | 43 +++++++------- src/Episode3/RulerServer.cc | 4 +- src/Episode3/RulerServer.hh | 3 +- src/Episode3/Server.cc | 2 +- src/Main.cc | 108 ++++++++++++++++++++++++++++++++++++ src/SaveFileFormats.hh | 16 +++--- src/ServerShell.cc | 2 +- 9 files changed, 195 insertions(+), 69 deletions(-) diff --git a/TODO.md b/TODO.md index 51985517..d50e517e 100644 --- a/TODO.md +++ b/TODO.md @@ -21,7 +21,7 @@ ## Episode 3 - Make disconnecting during a tournament match cause you to forfeit the match -- Enforce tournament deck restrictions (e.g. rarity checks, No Assist option) when populating COMs at tournament start time +- Enforce tournament deck restrictions (e.g. rank checks, No Assist option) when populating COMs at tournament start time - Spectator teams - Spectator teams sometimes stop receiving commands during live battles? - It may be possible to send spectators back to the waiting room after a non-tournament battle by sending 6xB4x05 with environment 0x19, then 6xB4x3B again; try this diff --git a/src/Episode3/DataIndexes.cc b/src/Episode3/DataIndexes.cc index 51c0b10e..8a6bcf7e 100644 --- a/src/Episode3/DataIndexes.cc +++ b/src/Episode3/DataIndexes.cc @@ -727,29 +727,43 @@ string CardDefinition::Effect::str_for_arg(const string& arg) { } } -string CardDefinition::Effect::str() const { - uint8_t type = static_cast(this->type); - string cmd_str = string_printf("%02hhX", type); - try { - const char* name = description_for_condition_type.at(type).name; - if (name) { - cmd_str += ':'; - cmd_str += name; +string CardDefinition::Effect::str(const char* separator) const { + vector tokens; + tokens.emplace_back(string_printf("%hhu:", this->effect_num)); + { + uint8_t type = static_cast(this->type); + string cmd_str = string_printf("cmd=%02hhX", type); + try { + const char* name = description_for_condition_type.at(type).name; + if (name) { + cmd_str += ':'; + cmd_str += name; + } + } catch (const out_of_range&) { } - } catch (const out_of_range&) { + tokens.emplace_back(std::move(cmd_str)); } - - string expr_str = this->expr; - if (!expr_str.empty()) { - expr_str = ", expr=" + expr_str; + if (!this->expr.empty()) { + tokens.emplace_back("expr=" + string(this->expr)); } + tokens.emplace_back(string_printf("when=%02hhX", this->when)); + tokens.emplace_back(this->str_for_arg(this->arg1)); + tokens.emplace_back(this->str_for_arg(this->arg2)); + tokens.emplace_back(this->str_for_arg(this->arg3)); + { + uint8_t type = static_cast(this->apply_criterion); + string cond_str = string_printf("cond=%02hhX", type); + try { + const char* name = name_for_criterion_code(this->apply_criterion); + cond_str += ':'; + cond_str += name; + } catch (const invalid_argument&) { + } + tokens.emplace_back(std::move(cond_str)); + } + tokens.emplace_back(string_printf("a2=%02hhX", this->unknown_a2)); - string arg1str = this->str_for_arg(this->arg1); - string arg2str = this->str_for_arg(this->arg2); - string arg3str = this->str_for_arg(this->arg3); - return string_printf("((%hhu) cmd=%s%s, when=%02hhX, arg1=%s, arg2=%s, arg3=%s, cond=%02hhX, a2=%02hhX)", - this->effect_num, cmd_str.c_str(), expr_str.c_str(), this->when, arg1str.data(), - arg2str.data(), arg3str.data(), static_cast(this->apply_criterion), this->unknown_a2); + return join(tokens, separator); } bool CardDefinition::is_sc() const { @@ -837,13 +851,13 @@ void CardDefinition::decode_range() { } } -string name_for_rarity(CardRarity rarity) { +string name_for_rank(CardRank rank) { static const vector names( {"N1", "R1", "S", "E", "N2", "N3", "N4", "R2", "R3", "R4", "SS", "D1", "D2"}); try { - return names.at(static_cast(rarity) - 1); + return names.at(static_cast(rank) - 1); } catch (const out_of_range&) { - return string_printf("(%02hhX)", static_cast(rarity)); + return string_printf("(%02hhX)", static_cast(rank)); } } @@ -882,7 +896,7 @@ string string_for_colors(const parray& colors) { } } if (ret.empty()) { - return "none"; + return "(none)"; } return ret; } @@ -1011,7 +1025,7 @@ string CardDefinition::str(bool single_line) const { } catch (const invalid_argument&) { card_class_str = string_printf("%04hX", this->be_card_class.load()); } - string rarity_str = name_for_rarity(this->rarity); + string rank_str = name_for_rank(this->rank); string target_mode_str = name_for_target_mode(this->target_mode); string assist_turns_str = string_for_assist_turns(this->assist_turns); string hp_str = this->hp.str(); @@ -1031,7 +1045,7 @@ string CardDefinition::str(bool single_line) const { } else if (!effects_str.empty()) { effects_str += ", "; } - effects_str += this->effects[x].str(); + effects_str += this->effects[x].str(single_line ? ", " : "\n "); } if (!single_line && effects_str.empty()) { effects_str = " (none)"; @@ -1052,7 +1066,7 @@ string CardDefinition::str(bool single_line) const { 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 " + "[Card: %04" PRIX32 " name=%s type=%s usable_condition=%s rank=%s " "cost=%s 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 class=%s assist_ai_params=[target=%s priority=%hhu effect=%hhu] drop_rates=[%s, %s] effects=[%s]]", @@ -1060,7 +1074,7 @@ string CardDefinition::str(bool single_line) const { this->en_name.data(), type_str.c_str(), criterion_str.c_str(), - rarity_str.c_str(), + rank_str.c_str(), cost_str.c_str(), target_mode_str.c_str(), range_str.c_str(), @@ -1105,23 +1119,29 @@ string CardDefinition::str(bool single_line) const { Card: %04" PRIX32 " \"%s\"\n\ Type: %s, class: %s\n\ Usability condition: %s\n\ - Rarity: %s\n\ + Rank: %s\n\ Cost: %s\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\ + Colors:\n\ + Left: %s\n\ + Right: %s\n\ + Top: %s\n\ Assist AI parameters: [target %s, priority %hu, effect %hu]\n\ - Drop rates: [%s, %s] (%s drop)\n\ + Drop rates:\n\ + %s\n\ + %s\n\ + %s\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(), + rank_str.c_str(), cost_str.c_str(), target_mode_str.c_str(), range_str.c_str(), @@ -1140,7 +1160,7 @@ Card: %04" PRIX32 " \"%s\"\n\ static_cast(this->assist_ai_params % 100), drop0_str.c_str(), drop1_str.c_str(), - this->cannot_drop ? "cannot" : "can", + this->cannot_drop ? "Forbidden" : "Permitted", effects_str.c_str()); } } diff --git a/src/Episode3/DataIndexes.hh b/src/Episode3/DataIndexes.hh index 2d5f485f..b63a3d74 100644 --- a/src/Episode3/DataIndexes.hh +++ b/src/Episode3/DataIndexes.hh @@ -102,7 +102,7 @@ enum class CriterionCode : uint8_t { const char* name_for_criterion_code(CriterionCode code); -enum class CardRarity : uint8_t { +enum class CardRank : uint8_t { N1 = 0x01, R1 = 0x02, S = 0x03, @@ -114,14 +114,14 @@ enum class CardRarity : uint8_t { R3 = 0x09, R4 = 0x0A, SS = 0x0B, - // Cards with the D1 or D2 rarities are considered never usable by the player, + // Cards with the D1 or D2 ranks are considered never usable by the player, // and are automatically removed from player decks before battle and when - // loading the deckbuilder. Cards with the D1 rarity appear in the deckbuilder - // but are grayed out (and cannot be added to decks); cards with the D2 rarity + // loading the deckbuilder. Cards with the D1 rank appear in the deckbuilder + // but are grayed out (and cannot be added to decks); cards with the D2 rank // don't appear in the deckbuilder at all. D1 = 0x0C, D2 = 0x0D, - // The D3 rarity is referenced in a few places, including the function that + // The D3 rank is referenced in a few places, including the function that // determines whether or not a card can appear in post-battle draws, and the // function that determines whether a card should appear in the deckbuilder. // In these cases, it prevents the card from appearing. @@ -497,7 +497,7 @@ struct CardDefinition { bool is_empty() const; static std::string str_for_arg(const std::string& arg); - std::string str() const; + std::string str(const char* separator = ", ") const; } __attribute__((packed)); /* 0000 */ be_uint32_t card_id; @@ -525,7 +525,7 @@ struct CardDefinition { // random assist. /* 0091 */ uint8_t cannot_drop; /* 0092 */ CriterionCode usable_criterion; - /* 0093 */ CardRarity rarity; + /* 0093 */ CardRank rank; /* 0094 */ be_uint16_t unused4; // The card class is used for checking attributes (e.g. item types). It's // stored big-endian here, so there's a helper function (card_class()) that @@ -552,7 +552,7 @@ struct CardDefinition { // card can transform into this card if any of the following are true: // - type is SC_HUNTERS or SC_ARKZ // - card_class is BOSS_ATTACK_ACTION (0x23) or BOSS_TECH (0x24) - // - rarity is E, D1, or D2 + // - rank is E, D1, or D2 // - cannot_drop is 1 (specifically 1; other nonzero values here don't // prevent the card from appearing in post-battle draws) // If none of these conditions apply, the logic below is used. @@ -653,7 +653,7 @@ struct CardDefinition { // effect. Therefore, the final probability that a card will transform into a // VIP card is P(activate) * P(vip), and the final probability of transforming // into a rarer card is P(activate) * P(rare). - // ====== Card rarities N4-N1 ====== ====== Card rarities R4-R1 ====== + // ======== Card rank N4-N1 ======== ======== Card rank R4-R1 ======== // Count P(activate) P(rare) P(vip) P(activate) P(rare) P(vip) // 0-4 0% 0% 0% 0% 0% 0% // 5-10 1.923077% 55% 0.5% 2.0408163% 55% 0.5% @@ -665,9 +665,9 @@ struct CardDefinition { // 53-99 5% 90% 0.33333334% 5.263158% 90% 0.4347826% // // If a transformation occurs, the card transforms to a card of a different - // rarity. First, the game consults the following table to determine the - // rarity of the resulting card (original card's rarity on the left, new - // card's rarity across the top): + // rank. First, the game consults the following table to determine the rank of + // the resulting card (original card's rank on the left, new card's rank + // across the top): // N4 N3 N2 N1 R4 R3 R2 R1 S SS // N4 => 60 30 10 // N3 => 60 30 10 @@ -682,16 +682,15 @@ struct CardDefinition { // card transforms, there is a 900/1001 chance of becoming another R1, a // 100/1001 chance of becoming an S, and a 1/1001 chance of becoming an SS. // - // Once a rarity is chosen, the game puts all possible cards into buckets - // based on how many of that card the player already has, then chooses a - // random card out of bucket 0, then bucket 1, etc. all the way up to bucket - // 49 (or 2 if the final rarity is S or SS). The first drawn card that is the - // final rarity is the card that the original card transforms into. Notably, - // this logic means that cards are more likely to transform into cards that - // the player doesn't already have, or only has few copies of. Also notably, - // it is impossible for a card to transform into another card that the player - // already has 50 or more copies of, or an S or SS card that the player - // already has 3 copies of. + // Once a rank is chosen, the game puts all possible cards into buckets based + // on how many of that card the player already has, then chooses a random card + // out of bucket 0, then bucket 1, etc. all the way up to bucket 49 (or 2 if + // the final rank is S or SS). The first drawn card that is the final rank is + // the card that the original card transforms into. Notably, this logic means + // that cards are more likely to transform into cards that the player doesn't + // already have, or only has few copies of. Also notably, it is impossible for + // a card to transform into another card that the player already has 50 or + // more copies of, or an S or SS card that the player already has 3 copies of. // // One curiosity about the above procedure is that the buckets can only hold // 400 cards each for the N ranks, 300 each for the R ranks, and 100 each for diff --git a/src/Episode3/RulerServer.cc b/src/Episode3/RulerServer.cc index 6b72cb4e..ac07f2ae 100644 --- a/src/Episode3/RulerServer.cc +++ b/src/Episode3/RulerServer.cc @@ -2507,11 +2507,11 @@ void RulerServer::register_player( this->set_card_action_metadatas[client_id] = set_card_action_metadatas; } -void RulerServer::replace_D1_D2_rarity_cards_with_Attack( +void RulerServer::replace_D1_D2_rank_cards_with_Attack( parray& card_ids) const { for (size_t z = 0; z < card_ids.size(); z++) { auto ce = this->definition_for_card_id(card_ids[z]); - if (ce && ((ce->def.rarity == CardRarity::D1) || (ce->def.rarity == CardRarity::D2))) { + if (ce && ((ce->def.rank == CardRank::D1) || (ce->def.rank == CardRank::D2))) { card_ids[z] = 0x008A; // Attack action card } } diff --git a/src/Episode3/RulerServer.hh b/src/Episode3/RulerServer.hh index 1954ff8b..5b78b30f 100644 --- a/src/Episode3/RulerServer.hh +++ b/src/Episode3/RulerServer.hh @@ -197,8 +197,7 @@ public: std::shared_ptr deck_entry, std::shared_ptr> set_card_action_chains, std::shared_ptr> set_card_action_metadatas); - void replace_D1_D2_rarity_cards_with_Attack( - parray& card_ids) const; + void replace_D1_D2_rank_cards_with_Attack(parray& card_ids) const; AttackMedium get_attack_medium(const ActionState& pa) const; void set_client_team_id(uint8_t client_id, uint8_t team_id); int32_t set_cost_for_card(uint8_t client_id, uint16_t card_ref) const; diff --git a/src/Episode3/Server.cc b/src/Episode3/Server.cc index 0a0e4ccf..46a35585 100644 --- a/src/Episode3/Server.cc +++ b/src/Episode3/Server.cc @@ -1991,7 +1991,7 @@ void Server::handle_CAx14_update_deck_during_setup(const string& data) { throw runtime_error(string_printf("invalid deck: -0x%" PRIX32, verify_error)); } if (!(this->behavior_flags & BehaviorFlag::SKIP_D1_D2_REPLACE)) { - this->ruler_server->replace_D1_D2_rarity_cards_with_Attack(entry.card_ids); + this->ruler_server->replace_D1_D2_rank_cards_with_Attack(entry.card_ids); } *this->deck_entries[in_cmd.client_id] = in_cmd.entry; this->presence_entries[in_cmd.client_id].player_present = true; diff --git a/src/Main.cc b/src/Main.cc index c516090c..c51887dd 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -294,6 +294,7 @@ enum class Behavior { CONVERT_ITEMRT_REL_TO_JSON, SHOW_EP3_MAPS, SHOW_EP3_CARDS, + GENERATE_EP3_CARDS_HTML, DESCRIBE_ITEM, ENCODE_ITEM, PARSE_OBJECT_GRAPH, @@ -589,6 +590,8 @@ int main(int argc, char** argv) { behavior = Behavior::SHOW_EP3_MAPS; } else if (!strcmp(argv[x], "show-ep3-cards")) { behavior = Behavior::SHOW_EP3_CARDS; + } else if (!strcmp(argv[x], "generate-ep3-cards-html")) { + behavior = Behavior::GENERATE_EP3_CARDS_HTML; } else if (!strcmp(argv[x], "describe-item")) { behavior = Behavior::DESCRIBE_ITEM; } else if (!strcmp(argv[x], "encode-item")) { @@ -1661,6 +1664,111 @@ int main(int argc, char** argv) { break; } + case Behavior::GENERATE_EP3_CARDS_HTML: { + Episode3::CardIndex card_index("system/ep3/card-definitions.mnr", "system/ep3/card-definitions.mnrd", "system/ep3/card-text.mnr", "system/ep3/card-text.mnrd"); + struct CardInfo { + shared_ptr ce; + Image small_image; + Image medium_image; + Image large_image; + + bool is_empty() const { + return (this->ce == nullptr) && (this->small_image.get_width() == 0) && (this->medium_image.get_width() == 0) && (this->large_image.get_width() == 0); + } + }; + vector infos; + for (uint32_t card_id : card_index.all_ids()) { + if (infos.size() <= card_id) { + infos.resize(card_id + 1); + } + infos[card_id].ce = card_index.definition_for_id(card_id); + } + for (const auto& filename : list_directory_sorted("system/ep3/cardtex")) { + if ((filename[0] == 'C' || filename[0] == 'M' || filename[0] == 'L') && (filename[1] == '_')) { + size_t card_id = stoull(filename.substr(2, 3), nullptr, 10); + if (infos.size() <= card_id) { + infos.resize(card_id + 1); + } + auto& info = infos[card_id]; + Image img("system/ep3/cardtex/" + filename); + if (filename[0] == 'C') { + info.large_image = Image(512, 399); + info.large_image.blit(img, 0, 0, 512, 399, 0, 0); + } else if (filename[0] == 'L') { + info.medium_image = Image(184, 144); + info.medium_image.blit(img, 0, 0, 184, 144, 0, 0); + } else if (filename[0] == 'M') { + info.small_image = Image(58, 43); + info.small_image.blit(img, 0, 0, 58, 43, 0, 0); + } + fprintf(stderr, "... %s (%04zX)\r", filename.c_str(), card_id); + } + } + fprintf(stderr, "... Loaded card images\n"); + + deque blocks; + blocks.emplace_back("Phantasy Star Online Episode III cards"); + blocks.emplace_back("
Legend:
Card has no definition and is obviously incomplete
Card is unobtainable in random draws but may be a quest or event reward
Card is obtainable in random draws


"); + blocks.emplace_back(""); + bool gray = false; + for (size_t card_id = 0; card_id < infos.size(); card_id++) { + const auto& entry = infos[card_id]; + if (entry.is_empty()) { + continue; + } + + const char* background_color; + if (!entry.ce) { + background_color = gray ? "#EEC0C0" : "#FFC0C0"; + } else if (entry.ce->def.cannot_drop || + ((entry.ce->def.rank == Episode3::CardRank::D1) || (entry.ce->def.rank == Episode3::CardRank::D2) || (entry.ce->def.rank == Episode3::CardRank::D3)) || + ((entry.ce->def.card_class() == Episode3::CardClass::BOSS_ATTACK_ACTION) || (entry.ce->def.card_class() == Episode3::CardClass::BOSS_TECH)) || + ((entry.ce->def.drop_rates[0] == 6) && (entry.ce->def.drop_rates[1] == 6))) { + background_color = gray ? "#C0EEC0" : "#C0FFC0"; + } else { + background_color = gray ? "#EEEEEE" : "#FFFFFF"; + } + + gray = !gray; + blocks.emplace_back(string_printf("", background_color)); + blocks.emplace_back(string_printf(""); + fprintf(stderr, "... %04zX/%04zX\r", card_id, infos.size()); + } + blocks.emplace_back("
IDLargeMediumSmallTextDisassembly
%04zX
", card_id)); + if (entry.large_image.get_width() > 0) { + blocks.emplace_back(""); + } + blocks.emplace_back(""); + if (entry.medium_image.get_width() > 0) { + blocks.emplace_back(""); + } + blocks.emplace_back(""); + if (entry.small_image.get_width() > 0) { + blocks.emplace_back(""); + } + blocks.emplace_back(""); + if (entry.ce) { + blocks.emplace_back("
");
+          blocks.emplace_back(entry.ce->text);
+          blocks.emplace_back("
");
+          blocks.emplace_back(entry.ce->def.str(false));
+          blocks.emplace_back("
"); + } else { + blocks.emplace_back("
Definition is missing
"); + } + blocks.emplace_back("
"); + fprintf(stderr, "... Constructed HTML file\n"); + + save_file("cards.html", join(blocks, "")); + break; + } + case Behavior::SHOW_EP3_MAPS: { config_log.info("Collecting Episode 3 data"); Episode3::MapIndex map_index("system/ep3"); diff --git a/src/SaveFileFormats.hh b/src/SaveFileFormats.hh index 02e00903..e642e6e4 100644 --- a/src/SaveFileFormats.hh +++ b/src/SaveFileFormats.hh @@ -273,14 +273,14 @@ struct PSOGCEp3CharacterFile { /* 19428 */ be_uint32_t save_count; // This is an array of 1000 bits, represented here as 128 bytes, the last few // of which are unused. Each bit corresponds to a card ID with the bit's - // index; if the bit is set, then the card's rarity is replaced with D2 if its - // original rarity is S, SS, E, or D2, or with D1 if the original rarity is - // any other value. Upon receiving a B8 command (new card definitions), the - // game updates this array of bits based on which cards in the received update - // have D1 or D2 rarities. This could have been used by Sega to persist part - // of the online updates into offline play, but there's no indication that - // they ever used this functionality. - /* 1942C */ parray card_rarity_override_flags; + // index; if the bit is set, then the card's rank is replaced with D2 if its + // original rank is S, SS, E, or D2, or with D1 if the original rank is any + // other value. Upon receiving a B8 command (new card definitions), the game + // updates this array of bits based on which cards in the received update + // have D1 or D2 ranks. This could have been used by Sega to persist part of + // the online updates into offline play, but there's no indication that they + // ever used this functionality. + /* 1942C */ parray card_rank_override_flags; /* 194AC */ be_uint32_t round2_seed; /* 194B0 */ } __attribute__((packed)); diff --git a/src/ServerShell.cc b/src/ServerShell.cc index 69260377..eed804ed 100644 --- a/src/ServerShell.cc +++ b/src/ServerShell.cc @@ -170,7 +170,7 @@ Server commands:\n\ dice separately\n\ overall-time-limit=N: Set battle time limit (in multiples of 5 minutes)\n\ phase-time-limit=N: Set phase time limit (in seconds)\n\ - allowed-cards=ALL/N/NR/NRS: Set rarities of allowed cards\n\ + allowed-cards=ALL/N/NR/NRS: Set ranks of allowed cards\n\ deck-shuffle=ON/OFF: Enable/disable deck shuffle\n\ deck-loop=ON/OFF: Enable/disable deck loop\n\ hp=N: Set Story Character initial HP\n\