diff --git a/src/Episode3/CardSpecial.cc b/src/Episode3/CardSpecial.cc index 4727a626..d4f47cf3 100644 --- a/src/Episode3/CardSpecial.cc +++ b/src/Episode3/CardSpecial.cc @@ -1686,6 +1686,8 @@ bool CardSpecial::evaluate_effect_arg2_condition( } int32_t CardSpecial::evaluate_effect_expr(const AttackEnvStats& ast, const char* expr, DiceRoll& dice_roll) const { + using ExprToken = CardDefinition::Effect::ExprToken; + auto log = this->server()->log_stack("evaluate_effect_expr: "); if (log.min_level == phosg::LogLevel::L_DEBUG) { log.debug_f( @@ -1699,75 +1701,65 @@ int32_t CardSpecial::evaluate_effect_expr(const AttackEnvStats& ast, const char* ast.print(stderr); } - // Note: This implementation is not based on the original code because the - // original code was hard to follow - it used a look-behind approach with lots - // of local variables instead of the look-ahead approach that this - // implementation uses. Hopefully this implementation is easier to follow. - vector> tokens; - while (expr) { - ExpressionTokenType type; - int32_t value = 0; - expr = this->get_next_expr_token(expr, &type, &value); - if (expr) { - if (type == ExpressionTokenType::SPACE) { - throw runtime_error("expression contains space token"); - } - // Turn references into numbers, so only numbers and operators can appear - // in the tokens vector - if (type == ExpressionTokenType::REFERENCE) { - if ((value == 1) || (value == 11)) { - dice_roll.value_used_in_expr = true; - } - tokens.emplace_back(make_pair(ExpressionTokenType::NUMBER, ast.at(value))); - } else { - tokens.emplace_back(make_pair(type, value)); + // Note: This implementation is not based on the original code because the original code was hard to follow - it used + // look-behind approach with lots of local variables instead of the look-ahead approach that this implementation + // uses. Hopefully this implementation is easier to follow. + auto tokens = ExprToken::parse(expr); + for (auto& token : tokens) { + if (token.type == ExprToken::Type::SPACE) { + throw runtime_error("expression contains space token"); + } + // Turn references into numbers, so only numbers and operators can appear in the tokens vector + if (token.type == ExprToken::Type::REFERENCE) { + if ((token.value == 1) || (token.value == 11)) { + dice_roll.value_used_in_expr = true; } + token.type = ExprToken::Type::NUMBER; + token.value = ast.at(token.value); } } - // Operators are evaluated left-to-right - there are no operator precedence - // rules + // Operators are evaluated left-to-right - there are no operator precedence rules int32_t value = 0; log.debug_f("value={} (start)", value); for (size_t token_index = 0; token_index < tokens.size(); token_index++) { - auto token_type = tokens[token_index].first; - int32_t token_value = tokens[token_index].second; - if ((token_type == ExpressionTokenType::SPACE) || (token_type == ExpressionTokenType::REFERENCE)) { + const auto& token = tokens[token_index]; + if ((token.type == ExprToken::Type::SPACE) || (token.type == ExprToken::Type::REFERENCE)) { throw logic_error("space or reference token present in expr evaluation phase 2"); } - if (token_type == ExpressionTokenType::NUMBER) { - value = token_value; - log.debug_f("value={} (token_type=NUMBER, token_value={})", value, token_value); + if (token.type == ExprToken::Type::NUMBER) { + value = token.value; + log.debug_f("value={} (token_type=NUMBER, token_value={})", value, token.value); } else { if (token_index >= tokens.size() - 1) { throw runtime_error("no token on right side of binary operator"); } token_index++; - auto right_token_type = tokens[token_index].first; - auto right_value = tokens[token_index].second; - if (right_token_type != ExpressionTokenType::NUMBER) { + const auto& right_token = tokens[token_index]; + if (right_token.type != ExprToken::Type::NUMBER) { + // REFERENCE was converted to NUMBER after parsing, based on the attack env stats throw runtime_error("non-number, non-reference token on right side of operator"); } - switch (token_type) { - case ExpressionTokenType::ROUND_DIVIDE: - value = lround(static_cast(value) / right_value); - log.debug_f("value={} (token_type=ROUND_DIVIDE, right_token_value={})", value, right_value); + switch (token.type) { + case ExprToken::Type::ROUND_DIVIDE: + value = lround(static_cast(value) / right_token.value); + log.debug_f("value={} (token_type=ROUND_DIVIDE, right_token_value={})", value, right_token.value); break; - case ExpressionTokenType::SUBTRACT: - value -= right_value; - log.debug_f("value={} (token_type=SUBTRACT, right_token_value={})", value, right_value); + case ExprToken::Type::SUBTRACT: + value -= right_token.value; + log.debug_f("value={} (token_type=SUBTRACT, right_token_value={})", value, right_token.value); break; - case ExpressionTokenType::ADD: - value += right_value; - log.debug_f("value={} (token_type=ADD, right_token_value={})", value, right_value); + case ExprToken::Type::ADD: + value += right_token.value; + log.debug_f("value={} (token_type=ADD, right_token_value={})", value, right_token.value); break; - case ExpressionTokenType::MULTIPLY: - value *= right_value; - log.debug_f("value={} (token_type=MULTIPLY, right_token_value={})", value, right_value); + case ExprToken::Type::MULTIPLY: + value *= right_token.value; + log.debug_f("value={} (token_type=MULTIPLY, right_token_value={})", value, right_token.value); break; - case ExpressionTokenType::FLOOR_DIVIDE: - value = floor(value / right_value); - log.debug_f("value={} (token_type=FLOOR_DIVIDE, right_token_value={})", value, right_value); + case ExprToken::Type::FLOOR_DIVIDE: + value = floor(value / right_token.value); + log.debug_f("value={} (token_type=FLOOR_DIVIDE, right_token_value={})", value, right_token.value); break; default: throw logic_error("invalid binary operator"); @@ -2743,67 +2735,6 @@ void CardSpecial::get_effective_ap_tp( } } -const char* CardSpecial::get_next_expr_token( - const char* expr, ExpressionTokenType* out_type, int32_t* out_value) const { - switch (*expr) { - case '\0': - *out_type = ExpressionTokenType::SPACE; - return nullptr; - case ' ': - *out_type = ExpressionTokenType::SPACE; - return expr + 1; - case '+': - *out_type = ExpressionTokenType::ADD; - return expr + 1; - case '-': - *out_type = ExpressionTokenType::SUBTRACT; - return expr + 1; - case '*': - *out_type = ExpressionTokenType::MULTIPLY; - return expr + 1; - case '/': - if (expr[1] == '/') { - *out_type = ExpressionTokenType::FLOOR_DIVIDE; - return expr + 2; - } else { - *out_type = ExpressionTokenType::ROUND_DIVIDE; - return expr + 1; - } - } - - if ((*expr >= 'a') && (*expr <= 'z')) { - string token_buf; - for (; (*expr >= 'a') && (*expr <= 'z'); expr++) { - token_buf.push_back(*expr); - } - - *out_type = ExpressionTokenType::SPACE; - *out_value = 0x27; - - static const vector tokens = { - "f", "d", "ap", "tp", "hp", "mhp", "dm", "tdm", "tf", "ac", "php", - "dc", "cs", "a", "kap", "ktp", "dn", "hf", "df", "ff", "ef", "bi", - "ab", "mc", "dk", "sa", "gn", "wd", "tt", "lv", "adm", "ddm", "sat", - "edm", "ldm", "rdm", "fdm", "ndm", "ehp"}; - for (size_t z = 0; z < tokens.size(); z++) { - if (token_buf == tokens[z]) { - *out_type = ExpressionTokenType::REFERENCE; - *out_value = z; - return expr; - } - } - return expr; - } - - if ((*expr >= '0') && (*expr <= '9')) { - *out_type = ExpressionTokenType::NUMBER; - *out_value = strtol(expr, const_cast(&expr), 10); - return expr; - } - - throw runtime_error("invalid card effect expression"); -} - vector> CardSpecial::get_targeted_cards_for_condition( uint16_t card_ref, uint8_t def_effect_index, diff --git a/src/Episode3/CardSpecial.hh b/src/Episode3/CardSpecial.hh index f97b8454..7a87c13d 100644 --- a/src/Episode3/CardSpecial.hh +++ b/src/Episode3/CardSpecial.hh @@ -21,17 +21,6 @@ const InterferenceProbabilityEntry* get_interference_probability_entry( class CardSpecial { public: - enum class ExpressionTokenType { - SPACE = 0, // Also used for end of string (get_next_expr_token returns null) - REFERENCE = 1, // Reference to a value from the env stats (e.g. hp) - NUMBER = 2, // Constant value (e.g. 2) - SUBTRACT = 3, // "-" in input string - ADD = 4, // "+" in input string - ROUND_DIVIDE = 5, // "/" in input string - FLOOR_DIVIDE = 6, // "//" in input string - MULTIPLY = 7, // "*" in input string - }; - struct DiceRoll { uint8_t client_id; uint8_t unknown_a2; @@ -192,7 +181,6 @@ public: std::shared_ptr card1, uint16_t default_card_id, std::shared_ptr card2) const; static void get_effective_ap_tp( StatSwapType type, int16_t* effective_ap, int16_t* effective_tp, int16_t hp, int16_t ap, int16_t tp); - const char* get_next_expr_token(const char* expr, ExpressionTokenType* out_type, int32_t* out_value) const; std::vector> get_targeted_cards_for_condition( uint16_t card_ref, uint8_t def_effect_index, diff --git a/src/Episode3/DataIndexes.cc b/src/Episode3/DataIndexes.cc index ac99f811..ad8ea785 100644 --- a/src/Episode3/DataIndexes.cc +++ b/src/Episode3/DataIndexes.cc @@ -179,47 +179,61 @@ bool card_class_is_tech_like(CardClass cc, bool is_nte) { return (cc == CardClass::TECH) || (cc == CardClass::PHOTON_BLAST) || (!is_nte && (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", "Attack bonus"}, - {"sat", "Number of Sword-type items on SC\'s team"}, - {"edm", "Target attack bonus"}, - {"ldm", "Last attack damage before defense"}, // Unused - {"rdm", "Last attack damage"}, - {"fdm", "Final damage (after defense)"}, - {"ndm", "Unknown: ndm"}, // Unused - {"ehp", "Attacker HP"}, -}); +struct ExprTokenDefinition { + int32_t value; + std::string token; + std::string description; +}; +static const vector expr_token_defs{ + {0x00, "f", "Number of FCs controlled by current SC"}, + {0x01, "d", "Die roll"}, + {0x02, "ap", "Attacker effective AP"}, + {0x03, "tp", "Attacker effective TP"}, + {0x04, "hp", "Current HP"}, + {0x05, "mhp", "Maximum HP"}, + {0x06, "dm", "Physical damage"}, + {0x07, "tdm", "Technique damage"}, + {0x08, "tf", "Number of SC\'s destroyed FCs"}, + {0x09, "ac", "Remaining ATK points"}, + {0x0A, "php", "Maximum HP"}, + {0x0B, "dc", "Die roll"}, + {0x0C, "cs", "Card set cost"}, + {0x0D, "a", "Number of FCs on all teams"}, + {0x0E, "kap", "Action cards AP"}, + {0x0F, "ktp", "Action cards TP"}, + {0x10, "dn", "Unknown: dn"}, + {0x11, "hf", "Number of item or creature cards in hand"}, + {0x12, "df", "Number of destroyed ally FCs (including SC\'s own)"}, + {0x13, "ff", "Number of ally FCs (including SC\'s own)"}, + {0x14, "ef", "Number of enemy FCs"}, + {0x15, "bi", "Number of Native FCs on either team"}, + {0x16, "ab", "Number of A.Beast FCs on either team"}, + {0x17, "mc", "Number of Machine FCs on either team"}, + {0x18, "dk", "Number of Dark FCs on either team"}, + {0x19, "sa", "Number of Sword-type items on either team"}, + {0x1A, "gn", "Number of Gun-type items on either team"}, + {0x1B, "wd", "Number of Cane-type items on either team"}, + {0x1C, "tt", "Physical damage"}, + {0x1D, "lv", "Dice boost"}, + {0x1E, "adm", "SC attack damage"}, + {0x1F, "ddm", "Attack bonus"}, + {0x20, "sat", "Number of Sword-type items on SC\'s team"}, + {0x21, "edm", "Target attack bonus"}, + {0x22, "ldm", "Last attack damage before defense"}, // Unused + {0x23, "rdm", "Last attack damage"}, + {0x24, "fdm", "Final damage (after defense)"}, + {0x25, "ndm", "Unknown: ndm"}, // Unused + {0x26, "ehp", "Attacker HP"}, +}; +const ExprTokenDefinition& def_for_expr_token(const std::string& token) { + static unordered_map index; + if (index.empty()) { + for (const auto& def : expr_token_defs) { + index.emplace(def.token, &def); + } + } + return *index.at(token); +} static const vector description_for_n_condition({ /* n00 */ "Always true", @@ -485,6 +499,71 @@ string CardDefinition::Stat::str() const { } } +std::vector CardDefinition::Effect::ExprToken::parse(const char* expr) { + using T = ExprToken::Type; + + std::vector ret; + while (*expr) { + size_t ret_count = ret.size(); + switch (*expr) { + case ' ': + ret.emplace_back(ExprToken{.type = T::SPACE, .value = 0, .text = expr, .text_size = 1}); + break; + case '+': + ret.emplace_back(ExprToken{.type = T::ADD, .value = 0, .text = expr, .text_size = 1}); + break; + case '-': + ret.emplace_back(ExprToken{.type = T::SUBTRACT, .value = 0, .text = expr, .text_size = 1}); + break; + case '*': + ret.emplace_back(ExprToken{.type = T::MULTIPLY, .value = 0, .text = expr, .text_size = 1}); + break; + case '/': + ret.emplace_back(ExprToken{ + .type = ((expr[1] == '/') ? T::FLOOR_DIVIDE : T::ROUND_DIVIDE), + .value = 0, + .text = expr, + .text_size = static_cast((expr[1] == '/') ? 2 : 1)}); + break; + + default: + if ((*expr >= 'a') && (*expr <= 'z')) { + string token_buf; + for (const char* z = expr; (*z >= 'a') && (*z <= 'z'); z++) { + token_buf.push_back(*z); + } + + try { + const auto& def = def_for_expr_token(token_buf); + ret.emplace_back(ExprToken{ + .type = T::REFERENCE, + .value = def.value, + .text = expr - token_buf.size(), + .text_size = token_buf.size()}); + } catch (const std::out_of_range&) { + throw std::runtime_error("unknown token in card effect expression: " + token_buf); + } + + } else if ((*expr >= '0') && (*expr <= '9')) { + const char* end_ptr; + int32_t value = strtol(expr, const_cast(&end_ptr), 10); + ret.emplace_back(ExprToken{ + .type = T::NUMBER, .value = value, .text = expr, .text_size = static_cast(end_ptr - expr)}); + + } else { + throw runtime_error("invalid card effect expression"); + } + } + + if (ret.size() != ret_count + 1) { + throw std::logic_error("expr parsing step did not add exactly one token"); + } + expr += ret.back().text_size; + } + + return ret; +} + bool CardDefinition::Effect::is_empty() const { return (this->effect_num == 0 && this->type == ConditionType::NONE && @@ -573,7 +652,49 @@ string CardDefinition::Effect::str(const char* separator, const TextSet* text_ar tokens.emplace_back(std::move(cmd_str)); } if (!this->expr.empty()) { - tokens.emplace_back("expr=" + this->expr.decode()); + std::string expr_decoded = this->expr.decode(); + std::vector explanation_tokens; + try { + auto tokens = ExprToken::parse(expr_decoded.c_str()); + for (const auto& token : tokens) { + switch (token.type) { + case ExprToken::Type::REFERENCE: + try { + explanation_tokens.emplace_back(expr_token_defs.at(token.value).description); + } catch (const out_of_range&) { + explanation_tokens.emplace_back(std::format("<>", token.value)); + } + break; + case ExprToken::Type::NUMBER: + explanation_tokens.emplace_back(std::format("{}", token.value)); + break; + case ExprToken::Type::SUBTRACT: + explanation_tokens.emplace_back("-"); + break; + case ExprToken::Type::ADD: + explanation_tokens.emplace_back("+"); + break; + case ExprToken::Type::ROUND_DIVIDE: + explanation_tokens.emplace_back("/ (round-divide)"); + break; + case ExprToken::Type::FLOOR_DIVIDE: + explanation_tokens.emplace_back("/ (floor-divide)"); + break; + case ExprToken::Type::MULTIPLY: + explanation_tokens.emplace_back("*"); + break; + default: + explanation_tokens.emplace_back("<>"); + } + } + } catch (const exception& e) { + explanation_tokens.emplace_back(std::format("failed to parse expr: {}", e.what())); + } + if ((explanation_tokens.size() == 1) && (explanation_tokens.front() == expr_decoded)) { + tokens.emplace_back(std::format("expr={}", expr_decoded)); + } else { + tokens.emplace_back(std::format("expr={} ({})", expr_decoded, phosg::join(explanation_tokens, " "))); + } } tokens.emplace_back(std::format("when={:02X}:{}", static_cast(this->when), phosg::name_for_enum(this->when))); tokens.emplace_back("arg1=" + this->str_for_arg(this->arg1.decode())); diff --git a/src/Episode3/DataIndexes.hh b/src/Episode3/DataIndexes.hh index fd629ef8..776650ec 100644 --- a/src/Episode3/DataIndexes.hh +++ b/src/Episode3/DataIndexes.hh @@ -510,6 +510,27 @@ struct CardDefinition { } __packed_ws__(Stat, 4); struct Effect { + // Parsed version of expr (see below) + struct ExprToken { + enum class Type : uint8_t { + SPACE = 0, // Also used for end of string (get_next_expr_token returns null) + REFERENCE = 1, // Reference to a value from the env stats (e.g. hp) + NUMBER = 2, // Constant value (e.g. 2) + SUBTRACT = 3, // "-" in input string + ADD = 4, // "+" in input string + ROUND_DIVIDE = 5, // "/" in input string + FLOOR_DIVIDE = 6, // "//" in input string + MULTIPLY = 7, // "*" in input string + UNKNOWN = 0xFF, + }; + Type type = Type::UNKNOWN; + int32_t value = 0; + const char* text = nullptr; + size_t text_size = 0; + + static std::vector parse(const char* expr); + }; + // effect_num is the 1-based index of this effect within the card definition (that is, .effects[0] should have // effect_num == 1 if it is used). /* 00 */ uint8_t effect_num; diff --git a/src/Main.cc b/src/Main.cc index db425fbc..590a83f0 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -2736,13 +2736,17 @@ Action a_generate_ep3_cards_html( 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(""); - for (const auto& vi : version_infos) { - blocks.emplace_back(std::format("", - vi.num_output_columns, vi.name)); + if (version_infos.size() > 1) { + blocks.emplace_back("
ID{}
"); + for (const auto& vi : version_infos) { + blocks.emplace_back(std::format("", + vi.num_output_columns, vi.name)); + } + blocks.emplace_back(""); + } else { + blocks.emplace_back("
ID{}
"); } - blocks.emplace_back(""); for (const auto& vi : version_infos) { if (vi.show_small_column) { blocks.emplace_back("");
ID
Small