#include "DataIndexes.hh" #include #include #include #include #include #include #include "../Compression.hh" #include "../Loggers.hh" #include "../PSOEncryption.hh" #include "../Quest.hh" #include "../Text.hh" 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: return "UNKNOWN"; case AttackMedium::PHYSICAL: return "PHYSICAL"; case AttackMedium::TECH: return "TECH"; case AttackMedium::UNKNOWN_03: return "UNKNOWN_03"; case AttackMedium::INVALID_FF: return "INVALID_FF"; default: return "__INVALID__"; } } 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::FC: return "FC"; 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_NON_ANDROID_SC: return "HUNTER_NON_ANDROID_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_NON_RA_CLASS_HUMAN_SC: return "HUNTER_NON_RA_CLASS_HUMAN_SC"; case CriterionCode::HUNTER_HU_CLASS_ANDROID_SC: return "HUNTER_HU_CLASS_ANDROID_SC"; case CriterionCode::HUNTER_NON_RA_CLASS_NON_NEWMAN_SC: return "HUNTER_NON_RA_CLASS_NON_NEWMAN_SC"; case CriterionCode::HUNTER_NON_NEWMAN_NON_FORCE_MALE_SC: return "HUNTER_NON_NEWMAN_NON_FORCE_MALE_SC"; 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_HUMAN_FEMALE_SC: return "HUNTER_HUMAN_FEMALE_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::NON_PHYSICAL_NON_UNKNOWN_ATTACK_MEDIUM_NON_SC: return "NON_PHYSICAL_NON_UNKNOWN_ATTACK_MEDIUM_NON_SC"; case CriterionCode::NON_PHYSICAL_NON_TECH_ATTACK_MEDIUM_NON_SC: return "NON_PHYSICAL_NON_TECH_ATTACK_MEDIUM_NON_SC"; case CriterionCode::NON_PHYSICAL_NON_TECH_NON_UNKNOWN_ATTACK_MEDIUM_NON_SC: return "NON_PHYSICAL_NON_TECH_NON_UNKNOWN_ATTACK_MEDIUM_NON_SC"; 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) : x(x), y(y), direction(direction), unused(0) {} bool Location::operator==(const Location& other) const { return (this->x == other.x) && (this->y == other.y) && (this->direction == other.direction) && (this->unused == other.unused); } bool Location::operator!=(const Location& other) const { return !this->operator==(other); } std::string Location::str() const { return string_printf("Location[x=%hhu, y=%hhu, dir=%s, u=%hhu]", this->x, this->y, name_for_direction(this->direction), this->unused); } void Location::clear() { this->x = 0; this->y = 0; this->direction = Direction::RIGHT; this->unused = 0; } void Location::clear_FF() { this->x = 0xFF; this->y = 0xFF; this->direction = Direction::INVALID_FF; this->unused = 0xFF; } Direction turn_left(Direction d) { switch (d) { case Direction::RIGHT: return Direction::UP; case Direction::UP: return Direction::LEFT; case Direction::LEFT: return Direction::DOWN; case Direction::DOWN: return Direction::RIGHT; default: return Direction::INVALID_FF; } } Direction turn_right(Direction d) { switch (d) { case Direction::RIGHT: return Direction::DOWN; case Direction::UP: return Direction::RIGHT; case Direction::LEFT: return Direction::UP; case Direction::DOWN: return Direction::LEFT; default: return Direction::INVALID_FF; } } Direction turn_around(Direction d) { switch (d) { case Direction::RIGHT: return Direction::LEFT; case Direction::UP: return Direction::DOWN; case Direction::LEFT: return Direction::RIGHT; case Direction::DOWN: return Direction::UP; default: return Direction::INVALID_FF; } } const char* name_for_direction(Direction d) { switch (d) { case Direction::RIGHT: return "LEFT"; case Direction::UP: return "DOWN"; case Direction::LEFT: return "RIGHT"; case Direction::DOWN: return "UP"; case Direction::INVALID_FF: return "INVALID_FF"; default: return "__INVALID__"; } } bool card_class_is_tech_like(CardClass cc) { return (cc == CardClass::TECH) || (cc == CardClass::PHOTON_BLAST) || (cc == CardClass::BOSS_TECH); } static const unordered_map description_for_expr_token({ {"f", "Number of FCs controlled by current SC"}, {"d", "Die roll"}, {"ap", "Attacker effective AP"}, {"tp", "Attacker effective TP"}, {"hp", "Current HP"}, {"mhp", "Maximum HP"}, {"dm", "Physical damage"}, {"tdm", "Technique damage"}, {"tf", "Number of SC\'s destroyed FCs"}, {"ac", "Remaining ATK points"}, {"php", "Maximum HP"}, {"dc", "Die roll"}, {"cs", "Card set cost"}, {"a", "Number of FCs on all teams"}, {"kap", "Action cards AP"}, {"ktp", "Action cards TP"}, {"dn", "Unknown: dn"}, {"hf", "Number of item or creature cards in hand"}, {"df", "Number of destroyed ally FCs (including SC\'s own)"}, {"ff", "Number of ally FCs (including SC\'s own)"}, {"ef", "Number of enemy FCs"}, {"bi", "Number of Native FCs on either team"}, {"ab", "Number of A.Beast FCs on either team"}, {"mc", "Number of Machine FCs on either team"}, {"dk", "Number of Dark FCs on either team"}, {"sa", "Number of Sword-type items on either team"}, {"gn", "Number of Gun-type items on either team"}, {"wd", "Number of Cane-type items on either team"}, {"tt", "Physical damage"}, {"lv", "Dice boost"}, {"adm", "SC attack damage"}, {"ddm", "Defending damage"}, {"sat", "Number of Sword-type items on SC\'s team"}, {"edm", "Defending damage"}, // TODO: How is this different from ddm? {"ldm", "Unknown: ldm"}, // Unused {"rdm", "Defending damage"}, // TODO: How is this different from ddm/edm? {"fdm", "Final damage (after defense)"}, {"ndm", "Unknown: ndm"}, // Unused {"ehp", "Attacker HP"}, }); // Arguments are encoded as 3-character null-terminated strings (why?!), and are // used for adding conditions to effects (e.g. making them only trigger in // certain situations) or otherwise customizing their results. The arguments are // heterogeneous based on their position; that is, the first argument always has // the same meaning, and meaning letters that are valid in arg1 are not // necessarily valid in arg2, etc. // Argument meanings: // a01 = ??? // e00 = effect lasts while equipped? (in contrast to tXX) // pXX = who to target (see description_for_p_target) // In arg2: // bXX = require attack doing not more than XX damage // cXY/CXY = linked items (require item with cYX/CYX to be equipped as well) // dXY = roll one die; require result between X and Y inclusive // hXX = require HP >= XX // iXX = require HP <= XX // mXX = require attack doing at least XX damage // nXX = require condition (see description_for_n_condition below) // oXX = seems to be "require previous random-condition effect to have happened" // TODO: this is used as both o01 (recovery) and o11 (reflection) // conditions - why the difference? // rXX = randomly pass with XX% chance (if XX == 00, 100% chance?) // sXY = require card cost between X and Y ATK points (inclusive) // tXX = lasts XX turns, or activate after XX turns static const vector description_for_n_condition({ /* n00 */ "Always true", /* n01 */ "Card is Hunters-side SC", /* n02 */ "Destroyed with a single attack", /* n03 */ "Technique or PB action card was used", /* n04 */ "Attack has Pierce", /* n05 */ "Attack has Rampage", /* n06 */ "Native attribute", /* n07 */ "A.Beast attribute", /* n08 */ "Machine attribute", /* n09 */ "Dark attribute", /* n10 */ "Sword-type item", /* n11 */ "Gun-type item", /* n12 */ "Cane-type item", /* n13 */ "Guard item or MAG", /* n14 */ "Story Character", /* n15 */ "Attacker does not use action cards", /* n16 */ "Aerial attribute", /* n17 */ "Same AP as opponent", /* n18 */ "Any target is an SC", /* n19 */ "Has Paralyzed condition", /* n20 */ "Has Frozen condition", /* n21 */ "???", // TODO: This appears related to Pierce/Rampage /* n22 */ "???", // TODO: This appears related to Pierce/Rampage }); static const vector description_for_p_target({ /* p00 */ "Unknown: p00", // Unused; probably invalid /* p01 */ "SC / FC who set the card", /* p02 */ "Attacking SC / FC", /* p03 */ "Unknown: p03", // Unused /* p04 */ "Unknown: p04", // Unused /* p05 */ "SC / FC who set the card", // Identical to p01 /* p06 */ "??? (TODO)", /* p07 */ "??? (TODO; Weakness)", /* p08 */ "FC\'s owner SC", /* p09 */ "Unknown: p09", // Unused /* p10 */ "All ally FCs", /* p11 */ "All ally FCs", // TODO: how is this different from p10? /* p12 */ "All non-aerial FCs on both teams", /* p13 */ "All FCs on both teams that are Frozen", /* p14 */ "All FCs on both teams with <= 3 HP", /* p15 */ "All FCs on both teams", /* p16 */ "All FCs on both teams with >= 8 HP", /* p17 */ "This card", /* p18 */ "SC who equipped this card", /* p19 */ "Unknown: p19", // Unused /* p20 */ "Unknown: p20", // Unused /* p21 */ "Unknown: p21", // Unused /* p22 */ "All characters (SCs & FCs) including this card", // TODO: But why does Shifta apply only to allies then? /* p23 */ "All characters (SCs & FCs) except this card", /* p24 */ "All FCs on both teams that have Paralysis", /* p25 */ "Unknown: p25", // Unused /* p26 */ "Unknown: p26", // Unused /* p27 */ "Unknown: p27", // Unused /* p28 */ "Unknown: p28", // Unused /* p29 */ "Unknown: p29", // Unused /* p30 */ "Unknown: p30", // Unused /* p31 */ "Unknown: p31", // Unused /* p32 */ "Unknown: p32", // Unused /* p33 */ "Unknown: p33", // Unused /* p34 */ "Unknown: p34", // Unused /* p35 */ "All characters (SCs & FCs) within range", // Used for Explosion effect /* p36 */ "All ally SCs within range, but not the caster", // Resta /* p37 */ "All FCs or all opponent FCs (TODO)", // TODO: when to use which selector? is a3 involved here somehow? /* p38 */ "All allies except items within range (and not this card)", /* p39 */ "All FCs that cost 4 or more points", /* p40 */ "All FCs that cost 3 or fewer points", /* p41 */ "Unknown: p41", // Unused /* p42 */ "Attacker during defense phase", // Only used by TP Defense /* p43 */ "Owner SC of defending FC during attack", /* p44 */ "SC\'s own creature FCs within range", /* p45 */ "Both attacker and defender", // Used for Snatch, which moves EXP from one to the other /* p46 */ "All SCs & FCs one space left or right of this card", /* p47 */ "FC\'s owner Boss SC", // Only used for Gibbles+ which explicitly mentions Boss SC, so it looks like this is p08 but for bosses /* p48 */ "Everything within range, including this card\'s user", // Madness /* p49 */ "All ally FCs within range except this card", }); struct ConditionDescription { bool has_expr; const char* name; const char* description; }; static const vector description_for_condition_type({ /* 0x00 */ {false, "NONE", nullptr}, /* 0x01 */ {true, "AP_BOOST", "Temporarily increase AP by N"}, /* 0x02 */ {false, "RAMPAGE", "Rampage"}, /* 0x03 */ {true, "MULTI_STRIKE", "Duplicate attack N times"}, /* 0x04 */ {true, "DAMAGE_MOD_1", "Set attack damage / AP to N after action cards applied (step 1)"}, /* 0x05 */ {false, "IMMOBILE", "Give Immobile condition"}, /* 0x06 */ {false, "HOLD", "Give Hold condition"}, /* 0x07 */ {false, "CANNOT_DEFEND", "Cannot defend"}, /* 0x08 */ {true, "TP_BOOST", "Add N TP temporarily during attack"}, /* 0x09 */ {true, "GIVE_DAMAGE", "Cause direct N HP loss"}, /* 0x0A */ {false, "GUOM", "Give Guom condition"}, /* 0x0B */ {false, "PARALYZE", "Give Paralysis condition"}, /* 0x0C */ {false, "A_T_SWAP_0C", "Swap AP and TP temporarily"}, /* 0x0D */ {false, "A_H_SWAP", "Swap AP and HP temporarily"}, /* 0x0E */ {false, "PIERCE", "Attack SC directly even if they have items equipped"}, /* 0x0F */ {false, "UNUSED_0F", nullptr}, /* 0x10 */ {true, "HEAL", "Increase HP by N"}, /* 0x11 */ {false, "RETURN_TO_HAND", "Return card to hand"}, /* 0x12 */ {false, "SET_MV_COST_TO_0", "Movement costs nothing"}, /* 0x13 */ {false, "UNKNOWN_13", nullptr}, /* 0x14 */ {false, "ACID", "Give Acid condition"}, /* 0x15 */ {false, "ADD_1_TO_MV_COST", "Add 1 to move costs"}, /* 0x16 */ {true, "MIGHTY_KNUCKLE", "Temporarily increase AP by N, and set ATK dice to zero"}, /* 0x17 */ {true, "UNIT_BLOW", "Temporarily increase AP by N * number of this card set within phase"}, /* 0x18 */ {false, "CURSE", "Give Curse condition"}, /* 0x19 */ {false, "COMBO_AP", "Temporarily increase AP by number of this card set within phase"}, /* 0x1A */ {false, "PIERCE_RAMPAGE_BLOCK", "Block attack if Pierce/Rampage (?)"}, /* 0x1B */ {false, "ABILITY_TRAP", "Temporarily disable opponent abilities"}, /* 0x1C */ {false, "FREEZE", "Give Freeze condition"}, /* 0x1D */ {false, "ANTI_ABNORMALITY_1", "Cure all conditions"}, /* 0x1E */ {false, "UNKNOWN_1E", nullptr}, /* 0x1F */ {false, "EXPLOSION", "Damage all SCs and FCs by number of this same card set * 2"}, /* 0x20 */ {false, "UNKNOWN_20", nullptr}, /* 0x21 */ {false, "UNKNOWN_21", nullptr}, /* 0x22 */ {false, "UNKNOWN_22", nullptr}, /* 0x23 */ {false, "RETURN_TO_DECK", "Cancel discard and move to bottom of deck instead"}, /* 0x24 */ {false, "AERIAL", "Give Aerial status"}, /* 0x25 */ {true, "AP_LOSS", "Make attacker temporarily lose N AP during defense"}, /* 0x26 */ {true, "BONUS_FROM_LEADER", "Gain AP equal to the number of cards of type N on the field"}, /* 0x27 */ {false, "FREE_MANEUVER", "Enable movement over occupied tiles"}, /* 0x28 */ {false, "SCALE_MV_COST", "Multiply movement costs by a factor"}, /* 0x29 */ {true, "CLONE", "Make setting this card free if at least one card of type N is already on the field"}, /* 0x2A */ {true, "DEF_DISABLE_BY_COST", "Disable use of any defense cards costing between (N / 10) and (N % 10) points, inclusive"}, /* 0x2B */ {true, "FILIAL", "Increase controlling SC\'s HP by N when this card is destroyed"}, /* 0x2C */ {true, "SNATCH", "Steal N EXP during attack"}, /* 0x2D */ {true, "HAND_DISRUPTER", "Discard N cards from hand immediately"}, /* 0x2E */ {false, "DROP", "Give Drop condition"}, /* 0x2F */ {false, "ACTION_DISRUPTER", "Destroy all action cards used by attacker"}, /* 0x30 */ {true, "SET_HP", "Set HP to N"}, /* 0x31 */ {false, "NATIVE_SHIELD", "Block attacks from Native creatures"}, /* 0x32 */ {false, "A_BEAST_SHIELD", "Block attacks from A.Beast creatures"}, /* 0x33 */ {false, "MACHINE_SHIELD", "Block attacks from Machine creatures"}, /* 0x34 */ {false, "DARK_SHIELD", "Block attacks from Dark creatures"}, /* 0x35 */ {false, "SWORD_SHIELD", "Block attacks from Sword items"}, /* 0x36 */ {false, "GUN_SHIELD", "Block attacks from Gun items"}, /* 0x37 */ {false, "CANE_SHIELD", "Block attacks from Cane items"}, /* 0x38 */ {false, "UNKNOWN_38", nullptr}, /* 0x39 */ {false, "UNKNOWN_39", nullptr}, /* 0x3A */ {false, "DEFENDER", "Make attacks go to setter of this card instead of original target"}, /* 0x3B */ {false, "SURVIVAL_DECOYS", "Redirect damage for multi-sided attack"}, /* 0x3C */ {true, "GIVE_OR_TAKE_EXP", "Give N EXP, or take if N is negative"}, /* 0x3D */ {false, "UNKNOWN_3D", nullptr}, /* 0x3E */ {false, "DEATH_COMPANION", "If this card has 1 or 2 HP, set its HP to N"}, /* 0x3F */ {true, "EXP_DECOY", "If defender has EXP, lose EXP instead of getting damage when attacked"}, /* 0x40 */ {true, "SET_MV", "Set MV to N"}, /* 0x41 */ {true, "GROUP", "Temporarily increase AP by N * number of this card on field, excluding itself"}, /* 0x42 */ {false, "BERSERK", "User of this card receives the same damage as target, and isn\'t helped by target\'s defense cards"}, /* 0x43 */ {false, "GUARD_CREATURE", "Attacks on controlling SC damage this card instead"}, /* 0x44 */ {false, "TECH", "Technique cards cost 1 fewer ATK point"}, /* 0x45 */ {false, "BIG_SWING", "Increase all attacking ATK costs by 1"}, /* 0x46 */ {false, "UNKNOWN_46", nullptr}, /* 0x47 */ {false, "SHIELD_WEAPON", "Limit attacker\'s choice of target to guard items"}, /* 0x48 */ {false, "ATK_DICE_BOOST", "Increase ATK dice roll by 1"}, /* 0x49 */ {false, "UNKNOWN_49", nullptr}, /* 0x4A */ {false, "MAJOR_PIERCE", "If SC has over half of max HP, attacks target SC instead of equipped items"}, /* 0x4B */ {false, "HEAVY_PIERCE", "If SC has 3 or more items equipped, attacks target SC instead of equipped items"}, /* 0x4C */ {false, "MAJOR_RAMPAGE", "If SC has over half of max HP, attacks target SC and all equipped items"}, /* 0x4D */ {false, "HEAVY_RAMPAGE", "If SC has 3 or more items equipped, attacks target SC and all equipped items"}, /* 0x4E */ {true, "AP_GROWTH", "Permanently increase AP by N"}, /* 0x4F */ {true, "TP_GROWTH", "Permanently increase TP by N"}, /* 0x50 */ {true, "REBORN", "If any card of type N is on the field, this card goes to the hand when destroyed instead of being discarded"}, /* 0x51 */ {true, "COPY", "Temporarily set AP/TP to N percent (or 100% if N is 0) of opponent\'s values"}, /* 0x52 */ {false, "UNKNOWN_52", nullptr}, /* 0x53 */ {true, "MISC_GUARDS", "Add N to card\'s defense value"}, /* 0x54 */ {true, "AP_OVERRIDE", "Set AP to N temporarily"}, /* 0x55 */ {true, "TP_OVERRIDE", "Set TP to N temporarily"}, /* 0x56 */ {false, "RETURN", "Return card to hand on destruction instead of discarding"}, /* 0x57 */ {false, "A_T_SWAP_PERM", "Permanently swap AP and TP"}, /* 0x58 */ {false, "A_H_SWAP_PERM", "Permanently swap AP and HP"}, /* 0x59 */ {true, "SLAYERS_ASSASSINS", "Temporarily increase AP during attack"}, /* 0x5A */ {false, "ANTI_ABNORMALITY_2", "Remove all conditions"}, /* 0x5B */ {false, "FIXED_RANGE", "Use SC\'s range instead of weapon or attack card ranges"}, /* 0x5C */ {false, "ELUDE", "SC does not lose HP when equipped items are destroyed"}, /* 0x5D */ {false, "PARRY", "Forward attack to a random FC within one tile of original target, excluding attacker and original target"}, /* 0x5E */ {false, "BLOCK_ATTACK", "Completely block attack"}, /* 0x5F */ {false, "UNKNOWN_5F", nullptr}, /* 0x60 */ {false, "UNKNOWN_60", nullptr}, /* 0x61 */ {true, "COMBO_TP", "Gain TP equal to the number of cards of type N on the field"}, /* 0x62 */ {true, "MISC_AP_BONUSES", "Temporarily increase AP by N"}, /* 0x63 */ {true, "MISC_TP_BONUSES", "Temporarily increase TP by N"}, /* 0x64 */ {false, "UNKNOWN_64", nullptr}, /* 0x65 */ {true, "MISC_DEFENSE_BONUSES", "Decrease damage by N"}, /* 0x66 */ {true, "MOSTLY_HALFGUARDS", "Reduce damage from incoming attack by N"}, /* 0x67 */ {false, "PERIODIC_FIELD", "Swap immunity to tech or physical attacks"}, /* 0x68 */ {false, "FC_LIMIT_BY_COUNT", "Change FC limit from 8 ATK points total to 4 FCs total"}, /* 0x69 */ {false, "UNKNOWN_69", nullptr}, /* 0x6A */ {true, "MV_BONUS", "Increase MV by N"}, /* 0x6B */ {true, "FORWARD_DAMAGE", "Give N damage back to attacker during defense (?) (TODO)"}, /* 0x6C */ {true, "WEAK_SPOT_INFLUENCE", "Temporarily decrease AP by N"}, /* 0x6D */ {true, "DAMAGE_MODIFIER_2", "Set attack damage / AP after action cards applied (step 2)"}, /* 0x6E */ {true, "WEAK_HIT_BLOCK", "Block all attacks of N damage or less"}, /* 0x6F */ {true, "AP_SILENCE", "Temporarily decrease AP of opponent by N"}, /* 0x70 */ {true, "TP_SILENCE", "Temporarily decrease TP of opponent by N"}, /* 0x71 */ {false, "A_T_SWAP", "Temporarily swap AP and TP"}, /* 0x72 */ {true, "HALFGUARD", "Halve damage from attacks that would inflict N or more damage"}, /* 0x73 */ {false, "UNKNOWN_73", nullptr}, /* 0x74 */ {true, "RAMPAGE_AP_LOSS", "Temporarily reduce AP by N"}, /* 0x75 */ {false, "UNKNOWN_75", nullptr}, /* 0x76 */ {false, "REFLECT", "Generate reverse attack"}, /* 0x77 */ {false, "UNKNOWN_77", nullptr}, /* 0x78 */ {false, "ANY", nullptr}, // Treated as "any condition" in find functions /* 0x79 */ {false, "UNKNOWN_79", nullptr}, /* 0x7A */ {false, "UNKNOWN_7A", nullptr}, /* 0x7B */ {false, "UNKNOWN_7B", nullptr}, /* 0x7C */ {false, "UNKNOWN_7C", nullptr}, /* 0x7D */ {false, "UNKNOWN_7D", nullptr}, }); const char* name_for_condition_type(ConditionType cond_type) { try { return description_for_condition_type.at(static_cast(cond_type)).name; } catch (const out_of_range&) { return "__INVALID__"; } } const char* name_for_action_subphase(ActionSubphase subphase) { switch (subphase) { case ActionSubphase::ATTACK: return "ATTACK"; case ActionSubphase::DEFENSE: return "DEFENSE"; case ActionSubphase::INVALID_FF: return "INVALID_FF"; default: return "__INVALID__"; } } void CardDefinition::Stat::decode_code() { this->type = static_cast(this->code / 1000); int16_t value = this->code - (this->type * 1000); if (value != 999) { switch (this->type) { case Type::BLANK: this->stat = 0; break; case Type::STAT: case Type::PLUS_STAT: case Type::EQUALS_STAT: this->stat = value; break; case Type::MINUS_STAT: this->stat = -value; break; default: throw runtime_error("invalid card stat type"); } } else { this->stat = 0; this->type = static_cast(this->type + 4); } } string CardDefinition::Stat::str() const { switch (this->type) { case Type::BLANK: return "(blank)"; case Type::STAT: return string_printf("%hhd", this->stat); case Type::PLUS_STAT: return string_printf("+%hhd", this->stat); case Type::MINUS_STAT: return string_printf("-%d", -this->stat); case Type::EQUALS_STAT: return string_printf("=%hhd", this->stat); case Type::UNKNOWN: return "?"; case Type::PLUS_UNKNOWN: return "+?"; case Type::MINUS_UNKNOWN: return "-?"; case Type::EQUALS_UNKNOWN: return "=?"; default: return string_printf("[%02hhX %02hhX]", this->type, this->stat); } } bool CardDefinition::Effect::is_empty() const { return (this->effect_num == 0 && this->type == ConditionType::NONE && this->expr.is_filled_with(0) && this->when == 0 && this->arg1.is_filled_with(0) && this->arg2.is_filled_with(0) && this->arg3.is_filled_with(0) && this->apply_criterion == CriterionCode::NONE && this->unknown_a2 == 0); } string CardDefinition::Effect::str_for_arg(const string& arg) { if (arg.empty()) { return arg; } if (arg.size() != 3) { return arg + "/(invalid)"; } size_t value; try { value = stoul(arg.c_str() + 1, nullptr, 10); } catch (const invalid_argument&) { return arg + "/(invalid)"; } switch (arg[0]) { case 'a': return arg + "/(unknown)"; case 'C': case 'c': return string_printf("%s/Req. linked item (%zu=>%zu)", arg.c_str(), value / 10, value % 10); case 'd': return string_printf("%s/Req. die roll in [%zu, %zu]", arg.c_str(), value / 10, value % 10); case 'e': return arg + "/While equipped"; case 'h': return string_printf("%s/Req. HP >= %zu", arg.c_str(), value); case 'i': return string_printf("%s/Req. HP <= %zu", arg.c_str(), value); case 'n': try { return string_printf("%s/Req. condition: %s", arg.c_str(), description_for_n_condition.at(value)); } catch (const out_of_range&) { return arg + "/(unknown)"; } case 'o': return arg + "/Req. prev effect conditions passed"; case 'p': try { return string_printf("%s/Target: %s", arg.c_str(), description_for_p_target.at(value)); } catch (const out_of_range&) { return arg + "/(unknown)"; } case 'r': return string_printf("%s/Req. random with %zu%% chance", arg.c_str(), value == 0 ? 100 : value); case 's': return string_printf("%s/Req. cost in [%zu, %zu]", arg.c_str(), value / 10, value % 10); case 't': return string_printf("%s/Turns: %zu", arg.c_str(), value); default: return arg + "/(unknown)"; } } 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; } } catch (const out_of_range&) { } string expr_str = this->expr; if (!expr_str.empty()) { expr_str = ", expr=" + expr_str; } 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); } bool CardDefinition::is_sc() const { return (this->type == CardType::HUNTERS_SC) || (this->type == CardType::ARKZ_SC); } bool CardDefinition::is_fc() const { return (this->type == CardType::ITEM) || (this->type == CardType::CREATURE); } bool CardDefinition::is_named_android_sc() const { static const unordered_set TARGET_IDS({0x0005, 0x0007, 0x0110, 0x0113, 0x0114, 0x0117, 0x011B, 0x011F}); return TARGET_IDS.count(this->card_id); } bool CardDefinition::any_top_color_matches(const CardDefinition& other) const { for (size_t x = 0; x < this->top_colors.size(); x++) { if (this->top_colors[x] != 0) { for (size_t y = 0; y < other.top_colors.size(); y++) { if (this->top_colors[x] == other.top_colors[y]) { return true; } } } } return false; } CardClass CardDefinition::card_class() const { return static_cast(this->be_card_class.load()); } void CardDefinition::decode_range() { // If the cell representing the FC is nonzero, the card has a range from a // list of constants. Otherwise, its range is already defined in the range // array and should be left alone. uint8_t index = (this->range[4] >> 8) & 0xF; if (index != 0) { this->range.clear(0); switch (index) { case 1: // Single cell in front of FC this->range[3] = 0x00000100; break; case 2: // Cell in front of FC and the front-left and front-right (Slash) this->range[3] = 0x00001110; break; case 3: // 3 cells in a line in front of FC this->range[1] = 0x00000100; this->range[2] = 0x00000100; this->range[3] = 0x00000100; break; case 4: // All 8 cells around FC this->range[3] = 0x00001110; this->range[4] = 0x00001010; this->range[5] = 0x00001110; break; case 5: // 2 cells in a line in front of FC this->range[2] = 0x00000100; this->range[3] = 0x00000100; break; case 6: // Entire field (renders as "A") for (size_t x = 0; x < 6; x++) { this->range[x] = 0x000FFFFF; } break; case 7: // Superposition of 4 and 5 (unused) this->range[2] = 0x00000100; this->range[3] = 0x00001110; this->range[4] = 0x00001010; this->range[5] = 0x00001110; break; case 8: // All 8 cells around FC and FC's cell this->range[3] = 0x00001110; this->range[4] = 0x00001110; this->range[5] = 0x00001110; break; case 9: // No cells break; // The table in the DOL file only appears to contain 9 entries; there are // some pointers immediately after. So probably if a card specified A-F, // its range would be filled in with garbage in the original game. default: throw runtime_error("invalid fixed range index"); } } } string name_for_rarity(CardRarity rarity) { 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); } catch (const out_of_range&) { return string_printf("(%02hhX)", static_cast(rarity)); } } string name_for_target_mode(TargetMode target_mode) { static const vector names({ "NONE", "SINGLE", "MULTI", "SELF", "TEAM", "ALL", "MULTI-ALLY", "ALL-ALLY", "ALL-ATTACK", "OWN-FCS", }); try { return names.at(static_cast(target_mode)); } catch (const out_of_range&) { return string_printf("(%02hhX)", static_cast(target_mode)); } } string string_for_colors(const parray& colors) { string ret; for (size_t x = 0; x < 8; x++) { if (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()) { return "none"; } return ret; } string string_for_assist_turns(uint8_t turns) { if (turns == 90) { return "ONCE"; } else if (turns == 99) { return "FOREVER"; } else { return string_printf("%hhu", turns); } } string string_for_range(const parray& range) { string ret; for (size_t x = 0; x < 6; x++) { ret += string_printf("%05" PRIX32 "/", range[x].load()); } while (starts_with(ret, "00000/")) { ret = ret.substr(6); } if (!ret.empty()) { ret.resize(ret.size() - 1); } return ret; } string string_for_drop_rate(uint16_t drop_rate) { vector tokens; switch (drop_rate % 10) { case 0: tokens.emplace_back("mode=ANY"); break; case 1: tokens.emplace_back("mode=OFFLINE_STORY"); break; case 2: tokens.emplace_back("mode=OFFLINE_FREE_BATTLE"); break; case 3: tokens.emplace_back("mode=OFFLINE_FREE_BATTLE_PVP"); break; case 4: tokens.emplace_back("mode=ONLINE"); break; case 5: tokens.emplace_back("mode=TOURNAMENT"); break; case 6: tokens.emplace_back("mode=FORBIDDEN"); break; default: tokens.emplace_back("mode=__UNKNOWN__"); } uint8_t environment_number = (drop_rate / 10) % 100; if (environment_number) { tokens.emplace_back(string_printf("environment_number=%02hhX", static_cast(environment_number - 1))); } else { tokens.emplace_back("environment_number=ANY"); } tokens.emplace_back(string_printf("rarity_class=%hhu", static_cast((drop_rate / 1000) % 10))); switch ((drop_rate / 10000) % 10) { case 0: tokens.emplace_back("deck_type=ANY"); break; case 1: tokens.emplace_back("deck_type=HUNTERS"); break; case 2: tokens.emplace_back("deck_type=ARKZ"); break; default: tokens.emplace_back("deck_type=__UNKNOWN__"); } string description = join(tokens, ", "); return string_printf("[%hu: %s]", drop_rate, description.c_str()); } static const char* short_name_for_assist_ai_param_target(uint8_t target) { switch (target) { case 0: return "ANY"; case 1: return "SELF"; case 2: return "SELF_OR_ALLY"; case 3: return "ENEMY"; default: return "__UNKNOWN__"; } } static const char* name_for_assist_ai_param_target(uint8_t target) { switch (target) { case 0: return "any player"; case 1: return "self"; case 2: return "self or ally"; case 3: return "enemy player"; default: return "__UNKNOWN__"; } } string CardDefinition::str(bool single_line) const { string type_str; try { 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 assist_turns_str = string_for_assist_turns(this->assist_turns); string hp_str = this->hp.str(); string ap_str = this->ap.str(); string tp_str = this->tp.str(); string mv_str = this->mv.str(); string left_str = string_for_colors(this->left_colors); string right_str = string_for_colors(this->right_colors); string top_str = string_for_colors(this->top_colors); string effects_str; for (size_t x = 0; x < 3; x++) { if (this->effects[x].is_empty()) { continue; } if (!single_line) { effects_str += "\n "; } else if (!effects_str.empty()) { effects_str += ", "; } effects_str += this->effects[x].str(); } if (!single_line && effects_str.empty()) { effects_str = " (none)"; } string drop0_str = string_for_drop_rate(this->drop_rates[0]); string drop1_str = string_for_drop_rate(this->drop_rates[1]); string cost_str = string_printf("%hhX", this->self_cost); if (this->ally_cost) { if (single_line) { cost_str += string_printf("+%hhX", this->ally_cost); } else { cost_str += string_printf(" (self) + %hhX (ally)", this->ally_cost); } } 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=%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]]", this->card_id.load(), this->en_name.data(), type_str.c_str(), criterion_str.c_str(), rarity_str.c_str(), cost_str.c_str(), 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(), card_class_str.c_str(), short_name_for_assist_ai_param_target((this->assist_ai_params / 1000) % 10), static_cast((this->assist_ai_params / 100) % 10), static_cast(this->assist_ai_params % 100), drop0_str.c_str(), drop1_str.c_str(), 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: %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\ Assist AI parameters: [target %s, priority %hu, effect %hu]\n\ Drop rates: [%s, %s] (%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(), cost_str.c_str(), 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(), name_for_assist_ai_param_target((this->assist_ai_params / 1000) % 10), static_cast((this->assist_ai_params / 100) % 10), static_cast(this->assist_ai_params % 100), drop0_str.c_str(), drop1_str.c_str(), this->cannot_drop ? "cannot" : "can", effects_str.c_str()); } } void PlayerConfig::decrypt() { if (!this->is_encrypted) { return; } decrypt_trivial_gci_data( &this->card_counts, offsetof(PlayerConfig, decks) - offsetof(PlayerConfig, card_counts), this->basis); this->is_encrypted = 0; this->basis = 0; } void PlayerConfig::encrypt(uint8_t basis) { if (this->is_encrypted) { if (this->basis == basis) { return; } this->decrypt(); } decrypt_trivial_gci_data( &this->card_counts, offsetof(PlayerConfig, decks) - offsetof(PlayerConfig, card_counts), basis); this->is_encrypted = 1; this->basis = basis; } Rules::Rules() { this->clear(); } Rules::Rules(const JSON& json) { this->clear(); this->overall_time_limit = json.get_int("overall_time_limit", this->overall_time_limit); this->phase_time_limit = json.get_int("phase_time_limit", this->phase_time_limit); this->allowed_cards = json.get_enum("allowed_cards", this->allowed_cards); this->min_dice = json.get_int("min_dice", this->min_dice); this->max_dice = json.get_int("max_dice", this->max_dice); this->disable_deck_shuffle = json.get_bool("disable_deck_shuffle", this->disable_deck_shuffle); this->disable_deck_loop = json.get_bool("disable_deck_loop", this->disable_deck_loop); this->char_hp = json.get_int("char_hp", this->char_hp); this->hp_type = json.get_enum("hp_type", this->hp_type); this->no_assist_cards = json.get_bool("no_assist_cards", this->no_assist_cards); this->disable_dialogue = json.get_bool("disable_dialogue", this->disable_dialogue); this->dice_exchange_mode = json.get_enum("dice_exchange_mode", this->dice_exchange_mode); this->disable_dice_boost = json.get_bool("disable_dice_boost", this->disable_dice_boost); } JSON Rules::json() const { return JSON::dict({ {"overall_time_limit", this->overall_time_limit}, {"phase_time_limit", this->phase_time_limit}, {"allowed_cards", name_for_enum(this->allowed_cards)}, {"min_dice", this->min_dice}, {"max_dice", this->max_dice}, {"disable_deck_shuffle", static_cast(this->disable_deck_shuffle)}, {"disable_deck_loop", static_cast(this->disable_deck_loop)}, {"char_hp", this->char_hp}, {"hp_type", name_for_enum(this->hp_type)}, {"no_assist_cards", static_cast(this->no_assist_cards)}, {"disable_dialogue", static_cast(this->disable_dialogue)}, {"dice_exchange_mode", name_for_enum(this->dice_exchange_mode)}, {"disable_dice_boost", static_cast(this->disable_dice_boost)}, }); } void Rules::set_defaults() { this->clear(); this->overall_time_limit = 24; // 2 hours this->phase_time_limit = 30; this->min_dice = 1; this->max_dice = 6; this->char_hp = 15; } void Rules::clear() { this->overall_time_limit = 0; this->phase_time_limit = 0; this->allowed_cards = AllowedCards::ALL; this->min_dice = 0; this->max_dice = 0; this->disable_deck_shuffle = 0; this->disable_deck_loop = 0; this->char_hp = 0; this->hp_type = HPType::DEFEAT_PLAYER; this->no_assist_cards = 0; this->disable_dialogue = 0; this->dice_exchange_mode = DiceExchangeMode::HIGH_ATK; this->disable_dice_boost = 0; this->unused.clear(0); } string Rules::str() const { vector tokens; tokens.emplace_back(string_printf("char_hp=%hhu", this->char_hp)); switch (this->hp_type) { case HPType::DEFEAT_PLAYER: tokens.emplace_back("hp_type=DEFEAT_PLAYER"); break; case HPType::DEFEAT_TEAM: tokens.emplace_back("hp_type=DEFEAT_TEAM"); break; case HPType::COMMON_HP: tokens.emplace_back("hp_type=COMMON_HP"); break; default: tokens.emplace_back(string_printf("hp_type=(%02hhX)", static_cast(this->hp_type))); break; } tokens.emplace_back(string_printf("min_dice=%hhu", this->min_dice)); tokens.emplace_back(string_printf("max_dice=%hhu", this->max_dice)); switch (this->dice_exchange_mode) { case DiceExchangeMode::HIGH_ATK: tokens.emplace_back("dice_exchange=HIGH_ATK"); break; case DiceExchangeMode::HIGH_DEF: tokens.emplace_back("dice_exchange=HIGH_DEF"); break; case DiceExchangeMode::NONE: tokens.emplace_back("dice_exchange=NONE"); break; default: tokens.emplace_back(string_printf("dice_exchange=(%02hhX)", static_cast(this->dice_exchange_mode))); break; } tokens.emplace_back(string_printf("dice_boost=%s", this->disable_dice_boost ? "DISABLED" : "ENABLED")); tokens.emplace_back(string_printf("deck_shuffle=%s", this->disable_deck_shuffle ? "DISABLED" : "ENABLED")); tokens.emplace_back(string_printf("deck_loop=%s", this->disable_deck_loop ? "DISABLED" : "ENABLED")); switch (this->allowed_cards) { case AllowedCards::ALL: tokens.emplace_back("allowed_cards=ALL"); break; case AllowedCards::N_ONLY: tokens.emplace_back("allowed_cards=N_ONLY"); break; case AllowedCards::N_R_ONLY: tokens.emplace_back("allowed_cards=N_R_ONLY"); break; case AllowedCards::N_R_S_ONLY: tokens.emplace_back("allowed_cards=N_R_S_ONLY"); break; default: tokens.emplace_back(string_printf("allowed_cards=(%02hhX)", static_cast(this->allowed_cards))); break; } tokens.emplace_back(string_printf("assist_cards=%s", this->no_assist_cards ? "DISALLOWED" : "ALLOWED")); tokens.emplace_back(string_printf("time_limit=%zumin", static_cast(this->overall_time_limit * 5))); tokens.emplace_back(string_printf("phase_time_limit=%hhusec", this->phase_time_limit)); tokens.emplace_back(string_printf("dialogue=%s", this->disable_dialogue ? "DISABLED" : "ENABLED")); return "Rules[" + join(tokens, ", ") + "]"; } StateFlags::StateFlags() { this->clear(); } bool StateFlags::operator==(const StateFlags& other) const { return (this->turn_num == other.turn_num) && (this->battle_phase == other.battle_phase) && (this->current_team_turn1 == other.current_team_turn1) && (this->current_team_turn2 == other.current_team_turn2) && (this->action_subphase == other.action_subphase) && (this->setup_phase == other.setup_phase) && (this->registration_phase == other.registration_phase) && (this->team_exp == other.team_exp) && (this->team_dice_boost == other.team_dice_boost) && (this->first_team_turn == other.first_team_turn) && (this->tournament_flag == other.tournament_flag) && (this->client_sc_card_types == other.client_sc_card_types); } bool StateFlags::operator!=(const StateFlags& other) const { return !this->operator==(other); } void StateFlags::clear() { this->turn_num = 0; this->battle_phase = BattlePhase::INVALID_00; this->current_team_turn1 = 0; this->current_team_turn2 = 0; this->action_subphase = ActionSubphase::ATTACK; this->setup_phase = SetupPhase::REGISTRATION; this->registration_phase = RegistrationPhase::AWAITING_NUM_PLAYERS; this->team_exp.clear(0); this->team_dice_boost.clear(0); this->first_team_turn = 0; this->tournament_flag = 0; this->client_sc_card_types.clear(CardType::HUNTERS_SC); } void StateFlags::clear_FF() { this->turn_num = 0xFFFF; this->battle_phase = BattlePhase::INVALID_FF; this->current_team_turn1 = 0xFF; this->current_team_turn2 = 0xFF; this->action_subphase = ActionSubphase::INVALID_FF; this->setup_phase = SetupPhase::INVALID_FF; this->registration_phase = RegistrationPhase::INVALID_FF; this->team_exp.clear(0xFFFFFFFF); this->team_dice_boost.clear(0xFF); this->first_team_turn = 0xFF; this->tournament_flag = 0xFF; this->client_sc_card_types.clear(CardType::INVALID_FF); } string MapDefinition::CameraSpec::str() const { return string_printf( "CameraSpec[a1=(%g %g %g %g %g %g %g %g %g) camera=(%g %g %g) focus=(%g %g %g) a2=(%g %g %g)]", this->unknown_a1[0].load(), this->unknown_a1[1].load(), this->unknown_a1[2].load(), this->unknown_a1[3].load(), this->unknown_a1[4].load(), this->unknown_a1[5].load(), this->unknown_a1[6].load(), this->unknown_a1[7].load(), this->unknown_a1[8].load(), this->camera_x.load(), this->camera_y.load(), this->camera_z.load(), this->focus_x.load(), this->focus_y.load(), this->focus_z.load(), this->unknown_a2[0].load(), this->unknown_a2[1].load(), this->unknown_a2[2].load()); } string MapDefinition::str(const CardIndex* card_index) const { deque lines; auto add_map = [&](const parray, 0x10>& tiles) { for (size_t y = 0; y < this->height; y++) { string line = " "; for (size_t x = 0; x < this->height; x++) { line += string_printf(" %02hhX", tiles[y][x]); } lines.emplace_back(std::move(line)); } }; lines.emplace_back(string_printf("Map %08" PRIX32 ": %hhux%hhu", this->map_number.load(), this->width, this->height)); lines.emplace_back(string_printf(" a1=%08" PRIX32, this->unknown_a1.load())); lines.emplace_back(string_printf(" environment_number=%02hhX", this->environment_number)); lines.emplace_back(string_printf(" num_camera_zones=%02hhX", this->num_camera_zones)); lines.emplace_back(" tiles:"); add_map(this->map_tiles); lines.emplace_back(string_printf( " start_tile_definitions=[1p=%02hhX 2p=%02hhX,%02hhX 3p=%02hhX,%02hhX,%02hhX], [1p=%02hhX 2p=%02hhX,%02hhX 3p=%02hhX,%02hhX,%02hhX]", this->start_tile_definitions[0][0], this->start_tile_definitions[0][1], this->start_tile_definitions[0][2], this->start_tile_definitions[0][3], this->start_tile_definitions[0][4], this->start_tile_definitions[0][5], this->start_tile_definitions[1][0], this->start_tile_definitions[1][1], this->start_tile_definitions[1][2], this->start_tile_definitions[1][3], this->start_tile_definitions[1][4], this->start_tile_definitions[1][5])); for (size_t z = 0; z < this->num_camera_zones; z++) { for (size_t w = 0; w < 2; w++) { lines.emplace_back(string_printf(" camera zone %zu (team %zu):", z, w)); add_map(this->camera_zone_maps[w][z]); } for (size_t w = 0; w < 2; w++) { lines.emplace_back(" " + this->camera_zone_specs[w][z].str()); } } for (size_t w = 0; w < 3; w++) { for (size_t z = 0; z < 2; z++) { string spec_str = this->overview_specs[w][z].str(); lines.emplace_back(string_printf(" overview_specs[%zu][team %zu]=%s", w, z, spec_str.c_str())); } } lines.emplace_back(" modification tiles:"); add_map(this->modification_tiles); for (size_t z = 0; z < 0x70; z += 0x10) { lines.emplace_back(string_printf( " a5[0x%02zX:0x%02zX]=%02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX", z, z + 0x10, this->unknown_a5[z + 0x00], this->unknown_a5[z + 0x01], this->unknown_a5[z + 0x02], this->unknown_a5[z + 0x03], this->unknown_a5[z + 0x04], this->unknown_a5[z + 0x05], this->unknown_a5[z + 0x06], this->unknown_a5[z + 0x07], this->unknown_a5[z + 0x08], this->unknown_a5[z + 0x09], this->unknown_a5[z + 0x0A], this->unknown_a5[z + 0x0B], this->unknown_a5[z + 0x0C], this->unknown_a5[z + 0x0D], this->unknown_a5[z + 0x0E], this->unknown_a5[z + 0x0F])); } lines.emplace_back(string_printf( " a5[0x70:0x74]=%02hhX %02hhX %02hhX %02hhX", this->unknown_a5[0x70], this->unknown_a5[0x71], this->unknown_a5[0x72], this->unknown_a5[0x73])); lines.emplace_back(" default_rules: " + this->default_rules.str()); lines.emplace_back(string_printf( " a6=%02hhX %02hhX %02hhX %02hhX", this->unknown_a6[0], this->unknown_a6[1], this->unknown_a6[2], this->unknown_a6[3])); lines.emplace_back(" name: " + string(this->name)); lines.emplace_back(" location_name: " + string(this->location_name)); lines.emplace_back(" quest_name: " + string(this->quest_name)); lines.emplace_back(" description: " + string(this->description)); lines.emplace_back(string_printf(" map_xy: %hu %hu", this->map_x.load(), this->map_y.load())); for (size_t z = 0; z < 3; z++) { lines.emplace_back(string_printf(" npc_chars[%zu]:", z)); lines.emplace_back(" name: " + string(this->npc_ai_params[z].name)); lines.emplace_back(string_printf( " ai_params=(a1=%04hX %04hX, is_arkz=%02hhX, a2=%02hX %02hX %02hX)", this->npc_ai_params[z].unknown_a1[0].load(), this->npc_ai_params[z].unknown_a1[1].load(), this->npc_ai_params[z].is_arkz, this->npc_ai_params[z].unknown_a2[0], this->npc_ai_params[z].unknown_a2[1], this->npc_ai_params[z].unknown_a2[2])); for (size_t w = 0; w < 0x78; w += 0x08) { lines.emplace_back(string_printf( " ai_params.a3[0x%02zX:0x%02zX]=%04hX %04hX %04hX %04hX %04hX %04hX %04hX %04hX", w, w + 0x08, this->npc_ai_params[z].params[w + 0x00].load(), this->npc_ai_params[z].params[w + 0x01].load(), this->npc_ai_params[z].params[w + 0x02].load(), this->npc_ai_params[z].params[w + 0x03].load(), this->npc_ai_params[z].params[w + 0x04].load(), this->npc_ai_params[z].params[w + 0x05].load(), this->npc_ai_params[z].params[w + 0x06].load(), this->npc_ai_params[z].params[w + 0x07].load())); } lines.emplace_back(string_printf( " ai_params.a3[0x78:0x7E]=%04hX %04hX %04hX %04hX %04hX %04hX", this->npc_ai_params[z].params[0x78].load(), this->npc_ai_params[z].params[0x79].load(), this->npc_ai_params[z].params[0x7A].load(), this->npc_ai_params[z].params[0x7B].load(), this->npc_ai_params[z].params[0x7C].load(), this->npc_ai_params[z].params[0x7D].load())); lines.emplace_back(string_printf(" npc_decks[%zu]:", z)); lines.emplace_back(" name: " + string(this->npc_decks[z].name)); for (size_t w = 0; w < 0x20; w++) { uint16_t card_id = this->npc_decks[z].card_ids[w]; shared_ptr entry; if (card_index) { try { entry = card_index->definition_for_id(card_id); } catch (const out_of_range&) { } } if (entry) { string name = entry->def.en_name; lines.emplace_back(string_printf(" cards[%02zu]: %04hX (%s)", w, card_id, name.c_str())); } else { lines.emplace_back(string_printf(" cards[%02zu]: %04hX", w, card_id)); } } for (size_t x = 0; x < 0x10; x++) { lines.emplace_back(string_printf(" npc_dialogue[%zu][%zu]:", z, x)); lines.emplace_back(string_printf(" a1=%04hX", this->dialogue_sets[z][x].unknown_a1.load())); lines.emplace_back(string_printf(" a2=%04hX", this->dialogue_sets[z][x].unknown_a2.load())); for (size_t w = 0; w < 4; w++) { if (this->dialogue_sets[z][x].strings[w][0] != 0 && static_cast(this->dialogue_sets[z][x].strings[w][0]) != 0xFF) { lines.emplace_back(string_printf(" strings[%zu]=", w) + string(this->dialogue_sets[z][x].strings[w])); } } } } lines.emplace_back(" a7=" + format_data_string(this->unknown_a7.data(), this->unknown_a7.bytes())); lines.emplace_back(string_printf(" npc_ai_params_entry_index=[%08" PRIX32 " %08" PRIX32 " %08" PRIX32 "]", this->npc_ai_params_entry_index[0].load(), this->npc_ai_params_entry_index[1].load(), this->npc_ai_params_entry_index[2].load())); if (this->before_message[0]) { lines.emplace_back(" before_message: " + string(this->before_message)); } if (this->after_message[0]) { lines.emplace_back(" after_message: " + string(this->after_message)); } if (this->dispatch_message[0]) { lines.emplace_back(" dispatch_message: " + string(this->dispatch_message)); } for (size_t z = 0; z < 0x10; z++) { uint16_t card_id = this->reward_card_ids[z]; shared_ptr entry; if (card_index) { try { entry = card_index->definition_for_id(card_id); } catch (const out_of_range&) { } } if (entry) { string name = entry->def.en_name; lines.emplace_back(string_printf(" reward_cards[%02zu]: %04hX (%s)", z, card_id, name.c_str())); } else { lines.emplace_back(string_printf(" reward_cards[%02zu]: %04hX", z, card_id)); } } lines.emplace_back(string_printf(" level_overrides=[win=%" PRId32 ", loss=%" PRId32 "]", this->win_level_override.load(), this->loss_level_override.load())); lines.emplace_back(string_printf(" field_offset=(x: %hd units, y:%hd units) (x: %lg tiles, y: %lg tiles)", this->field_offset_x.load(), this->field_offset_y.load(), static_cast(this->field_offset_x) / 25.0, static_cast(this->field_offset_y) / 25.0)); lines.emplace_back(string_printf(" map_category=%02hhX", this->map_category)); lines.emplace_back(string_printf(" cyber_block_type=%02hhX", this->cyber_block_type)); lines.emplace_back(string_printf(" a11=%02hhX%02hhX", this->unknown_a11[0], this->unknown_a11[1])); static const array sc_card_entry_names = { "00 (Guykild; 0005)", "01 (Kylria; 0006)", "02 (Saligun; 0110)", "03 (Relmitos; 0111)", "04 (Kranz; 0002)", "05 (Sil'fer; 0004)", "06 (Ino'lis; 0003)", "07 (Viviana; 0112)", "08 (Teifu; 0113)", "09 (Orland; 0001)", "0A (Stella; 0114)", "0B (Glustar; 0115)", "0C (Hyze; 0117)", "0D (Rufina; 0118)", "0E (Peko; 0119)", "0F (Creinu; 011A)", "10 (Reiz; 011B)", "11 (Lura; 0007)", "12 (Break; 0008)", "13 (Rio; 011C)", "14 (Endu; 0116)", "15 (Memoru; 011D)", "16 (K.C.; 011E)", "17 (Ohgun; 011F)", }; string unavailable_sc_cards = " unavailable_sc_cards=["; for (size_t z = 0; z < 0x18; z++) { if (this->unavailable_sc_cards[z] == 0xFFFF) { continue; } if (unavailable_sc_cards.size() > 24) { unavailable_sc_cards += ", "; } if (this->unavailable_sc_cards[z] >= sc_card_entry_names.size()) { unavailable_sc_cards += string_printf("%04hX (invalid)", this->unavailable_sc_cards[z].load()); } else { unavailable_sc_cards += sc_card_entry_names[this->unavailable_sc_cards[z]]; } } unavailable_sc_cards += ']'; lines.emplace_back(std::move(unavailable_sc_cards)); for (size_t z = 0; z < 4; z++) { string player_type; switch (this->entry_states[z].player_type) { case 0x00: player_type = "Player"; break; case 0x01: player_type = "Player/COM"; break; case 0x02: player_type = "COM"; break; case 0x03: player_type = "FIXED_COM"; break; case 0x04: player_type = "NONE"; break; case 0xFF: player_type = "FREE"; break; default: player_type = string_printf("(%02hhX)", this->entry_states[z].player_type); break; } string deck_type; switch (this->entry_states[z].deck_type) { case 0x00: deck_type = "HERO ONLY"; break; case 0x01: deck_type = "DARK ONLY"; break; case 0xFF: deck_type = "any deck allowed"; break; default: deck_type = string_printf("(%02hhX)", this->entry_states[z].deck_type); break; } lines.emplace_back(string_printf( " entry_states[%zu] = %s / %s", z, player_type.c_str(), deck_type.c_str())); } return join(lines, "\n"); } MapDefinitionTrial::MapDefinitionTrial(const MapDefinition& map) : unknown_a1(map.unknown_a1), map_number(map.map_number), width(map.width), height(map.height), environment_number(map.environment_number), num_camera_zones(map.num_camera_zones), map_tiles(map.map_tiles), start_tile_definitions(map.start_tile_definitions), camera_zone_maps(map.camera_zone_maps), camera_zone_specs(map.camera_zone_specs), overview_specs(map.overview_specs), modification_tiles(map.modification_tiles), unknown_a5(map.unknown_a5), default_rules(map.default_rules), unknown_a6(map.unknown_a6), name(map.name), location_name(map.location_name), quest_name(map.quest_name), description(map.description), map_x(map.map_x), map_y(map.map_y), npc_decks(map.npc_decks), npc_ai_params(map.npc_ai_params), unknown_a7(map.unknown_a7), npc_ai_params_entry_index(map.npc_ai_params_entry_index), before_message(map.before_message), after_message(map.after_message), dispatch_message(map.dispatch_message), dialogue_sets(), reward_card_ids(map.reward_card_ids), win_level_override(map.win_level_override), loss_level_override(map.loss_level_override), field_offset_x(map.field_offset_x), field_offset_y(map.field_offset_y), map_category(map.map_category), cyber_block_type(map.cyber_block_type), unknown_a11(map.unknown_a11), unknown_t12(0xFF) { for (size_t z = 0; z < this->dialogue_sets.size(); z++) { this->dialogue_sets[z] = map.dialogue_sets[z].sub<8>(0); } } bool Rules::check_invalid_fields() const { Rules t = *this; return t.check_and_reset_invalid_fields(); } bool Rules::check_and_reset_invalid_fields() { bool ret = false; if (this->overall_time_limit > 36) { this->overall_time_limit = 6; ret = true; } if (this->phase_time_limit > 120) { this->phase_time_limit = 60; ret = true; } if (static_cast(this->allowed_cards) > 3) { this->allowed_cards = AllowedCards::ALL; ret = true; } if (this->min_dice > 9) { this->min_dice = 0; ret = true; } if (this->max_dice > 9) { this->max_dice = 0; ret = true; } if ((this->min_dice != 0) && (this->max_dice != 0) && (this->max_dice < this->min_dice)) { uint8_t t = this->min_dice; this->min_dice = this->max_dice; this->max_dice = t; ret = true; } if (this->disable_deck_shuffle > 1) { this->disable_deck_shuffle = 0; ret = true; } if (this->disable_deck_loop > 1) { this->disable_deck_loop = 0; ret = true; } if (this->char_hp > 99) { this->char_hp = 0; ret = true; } if (static_cast(this->hp_type) > 2) { this->hp_type = HPType::DEFEAT_PLAYER; ret = true; } if (this->no_assist_cards > 1) { this->no_assist_cards = 0; ret = true; } if (static_cast(this->dice_exchange_mode) > 2) { this->dice_exchange_mode = DiceExchangeMode::HIGH_ATK; ret = true; } if (this->disable_dice_boost > 1) { this->disable_dice_boost = 0; ret = true; } if ((this->max_dice != 0) && (this->max_dice < 3)) { this->disable_dice_boost = 1; ret = true; } return ret; } CardIndex::CardIndex(const string& filename, const string& decompressed_filename, const string& text_filename) { unordered_map> card_tags; unordered_map card_text; if (!text_filename.empty()) { try { string data = prs_decompress(load_file(text_filename)); StringReader r(data); while (!r.eof()) { string card_id_str = r.get_cstr(); if (card_id_str.empty() || (static_cast(card_id_str[0]) == 0xFF)) { break; } strip_leading_whitespace(card_id_str); uint32_t card_id = stoul(card_id_str); // Read all pages for this card string text; string first_page; for (;;) { string line = r.get_cstr(); if (line.empty()) { break; } if (first_page.empty()) { first_page = line; } text += '\n'; text += line; } // In orig_text, turn all \t into $ (following newserv conventions) string orig_text = text; for (char& ch : orig_text) { if (ch == '\t') { ch = '$'; } } // Preprocess first page: first, delete all color markers size_t offset = first_page.find("\tC"); while (offset != string::npos) { first_page = first_page.substr(0, offset) + first_page.substr(offset + 3); offset = first_page.find("\tC"); } // Preprocess first page: delete all lines that don't start with \t offset = first_page.find('\t'); if (offset == string::npos) { first_page.clear(); } else { first_page = first_page.substr(offset); } // Preprocess first page: merge lines that don't begin with \t for (offset = 0; offset < first_page.size(); offset++) { if (first_page[offset] == '\n' && first_page[offset + 1] != '\t') { first_page = first_page.substr(0, offset) + first_page.substr(offset + 1); offset--; } } // Split first page into tags, and collapse whitespace in the tag names vector tags; auto lines = split(first_page, '\n'); for (const auto& line : lines) { string tag; if (line[0] == '\t' && line[1] == 'D') { tag = "D: " + line.substr(2); } else if (line[0] == '\t' && line[1] == 'S') { tag = "S: " + line.substr(2); } if (!tag.empty()) { for (size_t offset = tag.find(" "); offset != string::npos; offset = tag.find(" ")) { tag = tag.substr(0, offset) + tag.substr(offset + 1); } 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"); } if (!card_tags.emplace(card_id, std::move(tags)).second) { throw logic_error("duplicate card tags id"); } r.go((r.where() + 0x3FF) & (~0x3FF)); } } catch (const exception& e) { static_game_data_log.warning("Failed to load card text: %s", e.what()); } } try { string decompressed_data; this->mtime_for_card_definitions = stat(filename).st_mtime; try { decompressed_data = load_file(decompressed_filename); this->compressed_card_definitions.clear(); } catch (const cannot_open_file&) { this->compressed_card_definitions = load_file(filename); decompressed_data = prs_decompress(this->compressed_card_definitions); } if (decompressed_data.size() > 0x36EC0) { throw runtime_error("decompressed card list data is too long"); } // There's a footer after the card definitions (it's a standard-format REL // file), but we ignore it if (decompressed_data.size() % sizeof(CardDefinition) != sizeof(CardDefinitionsFooter)) { throw runtime_error(string_printf( "decompressed card update file size %zX is not aligned with card definition size %zX (%zX extra bytes)", decompressed_data.size(), sizeof(CardDefinition), decompressed_data.size() % sizeof(CardDefinition))); } auto* defs = reinterpret_cast(decompressed_data.data()); size_t max_cards = decompressed_data.size() / sizeof(CardDefinition); for (size_t x = 0; x < max_cards; x++) { // The last card entry has the build date and some other metadata (and // isn't a real card, obviously), so skip it. The game detects this by // checking for a negative value in type, which we also do here. if (static_cast(defs[x].type) < 0) { continue; } shared_ptr entry(new CardEntry({defs[x], {}, {}})); if (!this->card_definitions.emplace(entry->def.card_id, entry).second) { throw runtime_error(string_printf( "duplicate card id: %08" PRIX32, entry->def.card_id.load())); } // Some cards intentionally have the same name, so we just leave them // unindexed (they can still be looked up by ID, of course) string name = entry->def.en_name; this->card_definitions_by_name.emplace(name, entry); this->card_definitions_by_name_normalized.emplace(this->normalize_card_name(name), entry); entry->def.hp.decode_code(); entry->def.ap.decode_code(); entry->def.tp.decode_code(); entry->def.mv.decode_code(); entry->def.decode_range(); if (!text_filename.empty()) { try { entry->text = std::move(card_text.at(defs[x].card_id)); } catch (const out_of_range&) { } try { entry->debug_tags = std::move(card_tags.at(defs[x].card_id)); } catch (const out_of_range&) { } } } if (this->compressed_card_definitions.empty()) { uint64_t start = now(); this->compressed_card_definitions = prs_compress(decompressed_data); uint64_t diff = now() - start; static_game_data_log.info( "Compressed card definitions (%zu bytes -> %zu bytes) in %" PRIu64 "ms", decompressed_data.size(), this->compressed_card_definitions.size(), diff); } if (this->compressed_card_definitions.size() > 0x7BF8) { // Try to reduce the compressed size by clearing out text static_game_data_log.info("Compressed card list data is too long (0x%zX bytes); removing text", this->compressed_card_definitions.size()); for (size_t x = 0; x < max_cards; x++) { if (static_cast(defs[x].type) < 0) { continue; } defs[x].jp_name.clear(); defs[x].jp_short_name.clear(); } uint64_t start = now(); this->compressed_card_definitions = prs_compress_optimal(decompressed_data.data(), decompressed_data.size()); uint64_t diff = now() - start; static_game_data_log.info( "Compressed card definitions (0x%zX bytes -> 0x%zX bytes) in %" PRIu64 "ms", decompressed_data.size(), this->compressed_card_definitions.size(), diff); } if (this->compressed_card_definitions.size() > 0x7BF8) { throw runtime_error("compressed card list data is too long"); } static_game_data_log.info("Indexed %zu Episode 3 card definitions", this->card_definitions.size()); } catch (const exception& e) { static_game_data_log.warning("Failed to load Episode 3 card update: %s", e.what()); } } const string& CardIndex::get_compressed_definitions() const { if (this->compressed_card_definitions.empty()) { throw runtime_error("card definitions are not available"); } return this->compressed_card_definitions; } shared_ptr CardIndex::definition_for_id(uint32_t id) const { return this->card_definitions.at(id); } shared_ptr CardIndex::definition_for_name(const string& name) const { return this->card_definitions_by_name.at(name); } shared_ptr CardIndex::definition_for_name_normalized(const string& name) const { return this->card_definitions_by_name_normalized.at(this->normalize_card_name(name)); } set CardIndex::all_ids() const { set ret; for (const auto& it : this->card_definitions) { ret.emplace(it.first); } return ret; } uint64_t CardIndex::definitions_mtime() const { return this->mtime_for_card_definitions; } string CardIndex::normalize_card_name(const string& name) { string ret; for (char ch : name) { if (ch == ' ') { continue; } ret.push_back(tolower(ch)); } return ret; } MapIndex::MapIndex(const string& directory) { auto add_maps_from_dir = [&](const string& dir, bool is_quest) -> void { for (const auto& filename : list_directory(dir)) { try { shared_ptr entry; if (ends_with(filename, ".mnmd") || ends_with(filename, ".bind")) { entry.reset(new MapEntry(load_object_file(dir + "/" + filename), is_quest)); } else if (ends_with(filename, ".mnm") || ends_with(filename, ".bin")) { entry.reset(new MapEntry(load_file(dir + "/" + filename), is_quest)); } else if (ends_with(filename, ".gci")) { entry.reset(new MapEntry(Quest::decode_gci_file(dir + "/" + filename), is_quest)); } else if (ends_with(filename, ".dlq")) { entry.reset(new MapEntry(Quest::decode_dlq_file(dir + "/" + filename), is_quest)); } if (entry.get()) { if (!this->maps.emplace(entry->map.map_number, entry).second) { throw runtime_error("duplicate map number"); } this->maps_by_name.emplace(entry->map.name, entry); string name = entry->map.name; static_game_data_log.info("Indexed Episode 3 %s %s (%08" PRIX32 "; %s)", is_quest ? "online quest" : "free battle map", filename.c_str(), entry->map.map_number.load(), name.c_str()); } } catch (const exception& e) { static_game_data_log.warning("Failed to index Episode 3 map %s: %s", filename.c_str(), e.what()); } } }; add_maps_from_dir(directory + "/maps-free", false); add_maps_from_dir(directory + "/maps-quest", true); } MapIndex::MapEntry::MapEntry(const MapDefinition& map, bool is_quest) : map(map), is_quest(is_quest) {} MapIndex::MapEntry::MapEntry(const string& compressed, bool is_quest) : is_quest(is_quest), compressed_data(compressed) { string decompressed = prs_decompress(this->compressed_data); if (decompressed.size() != sizeof(MapDefinition)) { throw runtime_error(string_printf( "decompressed data size is incorrect (expected %zu bytes, read %zu bytes)", sizeof(MapDefinition), decompressed.size())); } this->map = *reinterpret_cast(decompressed.data()); } const string& MapIndex::MapEntry::compressed(bool is_trial) const { if (is_trial) { if (this->compressed_trial_data.empty()) { MapDefinitionTrial mdt(this->map); this->compressed_trial_data = prs_compress(&mdt, sizeof(mdt)); } return this->compressed_trial_data; } else { if (this->compressed_data.empty()) { this->compressed_data = prs_compress(&this->map, sizeof(this->map)); } return this->compressed_data; } } const string& MapIndex::get_compressed_list(size_t num_players) const { if (num_players == 0) { throw runtime_error("cannot generate map list for no players"); } if (num_players > 4) { throw logic_error("player count is too high in map list generation"); } string& compressed_map_list = this->compressed_map_lists.at(num_players - 1); if (compressed_map_list.empty()) { StringWriter entries_w; StringWriter strings_w; size_t num_maps = 0; for (const auto& map_it : this->maps) { size_t map_num_players = 0; for (size_t z = 0; z < 4; z++) { uint8_t player_type = map_it.second->map.entry_states[z].player_type; if (player_type == 0x00 || player_type == 0x01 || player_type == 0xFF) { map_num_players++; } } if (map_num_players < num_players) { continue; } MapList::Entry e; const auto& map = map_it.second->map; e.map_x = map.map_x; e.map_y = map.map_y; e.environment_number = map.environment_number; e.map_number = map.map_number.load(); e.width = map.width; e.height = map.height; e.map_tiles = map.map_tiles; e.modification_tiles = map.modification_tiles; e.name_offset = strings_w.size(); strings_w.write(map.name.data(), map.name.len()); strings_w.put_u8(0); e.location_name_offset = strings_w.size(); strings_w.write(map.location_name.data(), map.location_name.len()); strings_w.put_u8(0); e.quest_name_offset = strings_w.size(); strings_w.write(map.quest_name.data(), map.quest_name.len()); strings_w.put_u8(0); e.description_offset = strings_w.size(); strings_w.write(map.description.data(), map.description.len()); strings_w.put_u8(0); e.map_category = map_it.second->is_quest ? 0x00 : 0xFF; entries_w.put(e); num_maps++; } MapList header; header.num_maps = num_maps; header.unknown_a1 = 0; header.strings_offset = entries_w.size(); header.total_size = sizeof(MapList) + entries_w.size() + strings_w.size(); PRSCompressor prs; prs.add(&header, sizeof(header)); prs.add(entries_w.str()); prs.add(strings_w.str()); StringWriter compressed_w; compressed_w.put_u32b(prs.input_size()); compressed_w.write(prs.close()); compressed_map_list = std::move(compressed_w.str()); if (compressed_map_list.size() > 0x7BEC) { throw runtime_error(string_printf("compressed map list for %zu players is too large (0x%zX bytes)", num_players, compressed_map_list.size())); } size_t decompressed_size = sizeof(header) + entries_w.size() + strings_w.size(); static_game_data_log.info("Generated Episode 3 compressed map list for %zu player(s) (%zu maps; 0x%zX -> 0x%zX bytes)", num_players, num_maps, decompressed_size, compressed_map_list.size()); } return compressed_map_list; } shared_ptr MapIndex::definition_for_number(uint32_t id) const { return this->maps.at(id); } shared_ptr MapIndex::definition_for_name(const string& name) const { return this->maps_by_name.at(name); } set MapIndex::all_numbers() const { set ret; for (const auto& it : this->maps) { ret.emplace(it.first); } return ret; } COMDeckIndex::COMDeckIndex(const string& filename) { try { auto json = JSON::parse(load_file(filename)); for (const auto& def_json : json.as_list()) { auto& def = this->decks.emplace_back(new COMDeckDefinition()); def->index = this->decks.size() - 1; def->player_name = def_json->at(0).as_string(); def->deck_name = def_json->at(1).as_string(); auto card_ids_json = def_json->at(2); for (size_t z = 0; z < 0x1F; z++) { def->card_ids[z] = card_ids_json.at(z).as_int(); } if (!this->decks_by_name.emplace(def->deck_name, def).second) { throw runtime_error("duplicate COM deck name: " + def->deck_name); } } } catch (const exception& e) { static_game_data_log.warning("Failed to load Episode 3 COM decks: %s", e.what()); } } size_t COMDeckIndex::num_decks() const { return this->decks.size(); } shared_ptr COMDeckIndex::deck_for_index(size_t which) const { return this->decks.at(which); } shared_ptr COMDeckIndex::deck_for_name(const string& which) const { return this->decks_by_name.at(which); } shared_ptr COMDeckIndex::random_deck() const { return this->decks[random_object() % this->decks.size()]; } } // namespace Episode3 template <> Episode3::HPType enum_for_name(const char* name) { if (!strcmp(name, "DEFEAT_PLAYER")) { return Episode3::HPType::DEFEAT_PLAYER; } else if (!strcmp(name, "DEFEAT_TEAM")) { return Episode3::HPType::DEFEAT_TEAM; } else if (!strcmp(name, "COMMON_HP")) { return Episode3::HPType::COMMON_HP; } else { throw out_of_range("invalid HP type name"); } } template <> const char* name_for_enum(Episode3::HPType hp_type) { switch (hp_type) { case Episode3::HPType::DEFEAT_PLAYER: return "DEFEAT_PLAYER"; case Episode3::HPType::DEFEAT_TEAM: return "DEFEAT_TEAM"; case Episode3::HPType::COMMON_HP: return "COMMON_HP"; default: throw out_of_range("invalid HP type"); } } template <> Episode3::DiceExchangeMode enum_for_name(const char* name) { if (!strcmp(name, "HIGH_ATK")) { return Episode3::DiceExchangeMode::HIGH_ATK; } else if (!strcmp(name, "HIGH_DEF")) { return Episode3::DiceExchangeMode::HIGH_DEF; } else if (!strcmp(name, "NONE")) { return Episode3::DiceExchangeMode::NONE; } else { throw out_of_range("invalid dice exchange mode name"); } } template <> const char* name_for_enum(Episode3::DiceExchangeMode dice_exchange_mode) { switch (dice_exchange_mode) { case Episode3::DiceExchangeMode::HIGH_ATK: return "HIGH_ATK"; case Episode3::DiceExchangeMode::HIGH_DEF: return "HIGH_DEF"; case Episode3::DiceExchangeMode::NONE: return "NONE"; default: throw out_of_range("invalid dice exchange mode"); } } template <> Episode3::AllowedCards enum_for_name(const char* name) { if (!strcmp(name, "ALL")) { return Episode3::AllowedCards::ALL; } else if (!strcmp(name, "N_ONLY")) { return Episode3::AllowedCards::N_ONLY; } else if (!strcmp(name, "N_R_ONLY")) { return Episode3::AllowedCards::N_R_ONLY; } else if (!strcmp(name, "N_R_S_ONLY")) { return Episode3::AllowedCards::N_R_S_ONLY; } else { throw out_of_range("invalid allowed cards name"); } } template <> const char* name_for_enum(Episode3::AllowedCards allowed_cards) { switch (allowed_cards) { case Episode3::AllowedCards::ALL: return "ALL"; case Episode3::AllowedCards::N_ONLY: return "N_ONLY"; case Episode3::AllowedCards::N_R_ONLY: return "N_R_ONLY"; case Episode3::AllowedCards::N_R_S_ONLY: return "N_R_S_ONLY"; default: throw out_of_range("invalid allowed cards"); } }