add explanation in expr field in cards.html

This commit is contained in:
Martin Michelsen
2025-12-19 00:11:09 -08:00
parent 9ebaaacd46
commit 849cca37c8
5 changed files with 234 additions and 169 deletions
+41 -110
View File
@@ -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<pair<ExpressionTokenType, int32_t>> 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<double>(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<double>(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<const char*> 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<char**>(&expr), 10);
return expr;
}
throw runtime_error("invalid card effect expression");
}
vector<shared_ptr<const Card>> CardSpecial::get_targeted_cards_for_condition(
uint16_t card_ref,
uint8_t def_effect_index,
-12
View File
@@ -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<const Card> card1, uint16_t default_card_id, std::shared_ptr<const Card> 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<std::shared_ptr<const Card>> get_targeted_cards_for_condition(
uint16_t card_ref,
uint8_t def_effect_index,
+163 -42
View File
@@ -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<string, const char*> 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<ExprTokenDefinition> 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<std::string, const ExprTokenDefinition*> index;
if (index.empty()) {
for (const auto& def : expr_token_defs) {
index.emplace(def.token, &def);
}
}
return *index.at(token);
}
static const vector<const char*> description_for_n_condition({
/* n00 */ "Always true",
@@ -485,6 +499,71 @@ string CardDefinition::Stat::str() const {
}
}
std::vector<CardDefinition::Effect::ExprToken> CardDefinition::Effect::ExprToken::parse(const char* expr) {
using T = ExprToken::Type;
std::vector<ExprToken> 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<size_t>((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<char**>(&end_ptr), 10);
ret.emplace_back(ExprToken{
.type = T::NUMBER, .value = value, .text = expr, .text_size = static_cast<size_t>(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<std::string> 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("<<invalid reference: {:X}>>", 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("<<invalid token>>");
}
}
} 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<uint8_t>(this->when), phosg::name_for_enum(this->when)));
tokens.emplace_back("arg1=" + this->str_for_arg(this->arg1.decode()));
+21
View File
@@ -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<ExprToken> 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;
+9 -5
View File
@@ -2736,13 +2736,17 @@ Action a_generate_ep3_cards_html(
deque<string> blocks;
blocks.emplace_back("<html><head><title>Phantasy Star Online Episode III cards</title></head><body style=\"background-color:#222222; color: #EEEEEE\">");
blocks.emplace_back("<table><tr><th style=\"text-align: left\">Legend:</th></tr><tr style=\"background-color: #663333\"><td>Card has no definition and is obviously incomplete</td></tr><tr style=\"background-color: #336633\"><td>Card is unobtainable in random draws but may be a quest or event reward</td></tr><tr style=\"background-color: #333333\"><td>Card is obtainable in random draws</td></tr></table><br /><br />");
blocks.emplace_back("<table><tr><th rowspan=\"2\" style=\"text-align: left; padding: 4px\">ID</th>");
for (const auto& vi : version_infos) {
blocks.emplace_back(std::format("<th colspan=\"{}\" style=\"text-align: left; padding: 4px\">{}</th>",
vi.num_output_columns, vi.name));
if (version_infos.size() > 1) {
blocks.emplace_back("<table><tr><th rowspan=\"2\" style=\"text-align: left; padding: 4px\">ID</th>");
for (const auto& vi : version_infos) {
blocks.emplace_back(std::format("<th colspan=\"{}\" style=\"text-align: left; padding: 4px\">{}</th>",
vi.num_output_columns, vi.name));
}
blocks.emplace_back("</tr><tr>");
} else {
blocks.emplace_back("<table><tr><th style=\"text-align: left; padding: 4px\">ID</th>");
}
blocks.emplace_back("</tr><tr>");
for (const auto& vi : version_infos) {
if (vi.show_small_column) {
blocks.emplace_back("<th style=\"text-align: left; padding: 4px\">Small</th>");