add more options in IntegralExpression

This commit is contained in:
Martin Michelsen
2026-06-13 20:07:23 -07:00
parent 554fc5d208
commit 1737d8abc8
13 changed files with 117 additions and 69 deletions
+2 -2
View File
@@ -5,7 +5,7 @@
#include "Client.hh"
const std::vector<ChoiceSearchCategory> CHOICE_SEARCH_CATEGORIES({
const std::vector<ChoiceSearchCategory> CHOICE_SEARCH_CATEGORIES{
ChoiceSearchCategory{
.id = 0x0001,
.name = "Level",
@@ -145,4 +145,4 @@ const std::vector<ChoiceSearchCategory> CHOICE_SEARCH_CATEGORIES({
return (choice_id == 0) || (target_choice_id == 0) || (choice_id == target_choice_id);
},
},
});
};
+3 -1
View File
@@ -377,7 +377,9 @@ bool Client::evaluate_quest_availability_expression(
}
auto p = this->character_file();
IntegralExpression::Env env = {
.flags = &p->quest_flags.data.at(static_cast<size_t>(difficulty)),
.section_id = p->disp.visual.sh.section_id,
.difficulty = difficulty,
.flags = &p->quest_flags,
.challenge_records = &p->challenge_records,
.team = this->team(),
.num_players = num_players,
+2 -2
View File
@@ -811,7 +811,7 @@ void DownloadSession::on_request_complete() {
}
}
const std::vector<DownloadSession::GameConfig> DownloadSession::game_configs({
const std::vector<DownloadSession::GameConfig> DownloadSession::game_configs{
{.mode = GameMode::NORMAL, .episode = Episode::EP1, .v1 = true, .v2 = true, .v3 = true},
{.mode = GameMode::NORMAL, .episode = Episode::EP2, .v1 = false, .v2 = false, .v3 = true},
{.mode = GameMode::NORMAL, .episode = Episode::EP4, .v1 = false, .v2 = false, .v3 = false},
@@ -821,4 +821,4 @@ const std::vector<DownloadSession::GameConfig> DownloadSession::game_configs({
{.mode = GameMode::SOLO, .episode = Episode::EP1, .v1 = false, .v2 = false, .v3 = false},
{.mode = GameMode::SOLO, .episode = Episode::EP2, .v1 = false, .v2 = false, .v3 = false},
{.mode = GameMode::SOLO, .episode = Episode::EP4, .v1 = false, .v2 = false, .v3 = false},
});
};
+2 -2
View File
@@ -1738,7 +1738,7 @@ bool Server::update_registration_phase() {
return true;
}
const std::unordered_map<uint8_t, Server::handler_t> Server::subcommand_handlers({
const std::unordered_map<uint8_t, Server::handler_t> Server::subcommand_handlers{
{0x0B, &Server::handle_CAx0B_redraw_initial_hand},
{0x0C, &Server::handle_CAx0C_end_redraw_initial_hand_phase},
{0x0D, &Server::handle_CAx0D_end_non_action_phase},
@@ -1762,7 +1762,7 @@ const std::unordered_map<uint8_t, Server::handler_t> Server::subcommand_handlers
{0x41, &Server::handle_CAx41_map_request},
{0x48, &Server::handle_CAx48_end_turn},
{0x49, &Server::handle_CAx49_card_counts},
});
};
void Server::on_server_data_input(std::shared_ptr<Client> sender_c, const std::string& data) {
auto header = check_size_t<G_CardBattleCommandHeader>(data, 0xFFFF);
+39 -10
View File
@@ -158,26 +158,42 @@ std::string IntegralExpression::UnaryOperatorNode::str() const {
}
}
IntegralExpression::FlagLookupNode::FlagLookupNode(uint16_t flag_index) : flag_index(flag_index) {}
bool IntegralExpression::SectionIDLookupNode::operator==(const Node& other) const {
return (dynamic_cast<const SectionIDLookupNode*>(&other) != nullptr);
}
bool IntegralExpression::FlagLookupNode::operator==(const Node& other) const {
int64_t IntegralExpression::SectionIDLookupNode::evaluate(const Env& env) const {
return env.section_id;
}
std::string IntegralExpression::SectionIDLookupNode::str() const {
return "P_SID";
}
IntegralExpression::QuestFlagLookupNode::QuestFlagLookupNode(Difficulty difficulty, uint16_t flag_index)
: difficulty(difficulty), flag_index(flag_index) {}
bool IntegralExpression::QuestFlagLookupNode::operator==(const Node& other) const {
try {
const FlagLookupNode& other_flag = dynamic_cast<const FlagLookupNode&>(other);
return other_flag.flag_index == this->flag_index;
const QuestFlagLookupNode& other_flag = dynamic_cast<const QuestFlagLookupNode&>(other);
return ((other_flag.difficulty == this->difficulty) && (other_flag.flag_index == this->flag_index));
} catch (const std::bad_cast&) {
return false;
}
}
int64_t IntegralExpression::FlagLookupNode::evaluate(const Env& env) const {
int64_t IntegralExpression::QuestFlagLookupNode::evaluate(const Env& env) const {
if (!env.flags) {
throw std::runtime_error("quest flags not available");
}
return env.flags->get(this->flag_index) ? 1 : 0;
Difficulty effective_difficulty = (this->difficulty == Difficulty::UNKNOWN) ? env.difficulty : this->difficulty;
return env.flags->get(effective_difficulty, this->flag_index) ? 1 : 0;
}
std::string IntegralExpression::FlagLookupNode::str() const {
return std::format("F_{:04X}", this->flag_index);
std::string IntegralExpression::QuestFlagLookupNode::str() const {
return (this->difficulty == Difficulty::UNKNOWN)
? std::format("F_{:04X}", this->flag_index)
: std::format("F_{:c}_{:04X}", abbreviation_for_difficulty(this->difficulty), this->flag_index);
}
IntegralExpression::ChallengeCompletionLookupNode::ChallengeCompletionLookupNode(Episode episode, uint8_t stage_index)
@@ -380,16 +396,29 @@ std::unique_ptr<const IntegralExpression::Node> IntegralExpression::parse_expr(s
}
// Check for env lookups
if (text == "P_SID") {
return std::make_unique<SectionIDLookupNode>();
}
if (text.starts_with("F_")) {
Difficulty difficulty = Difficulty::UNKNOWN;
if (text.starts_with("F_N_")) {
difficulty = Difficulty::NORMAL;
} else if (text.starts_with("F_H_")) {
difficulty = Difficulty::HARD;
} else if (text.starts_with("F_V_")) {
difficulty = Difficulty::VERY_HARD;
} else if (text.starts_with("F_U_")) {
difficulty = Difficulty::ULTIMATE;
}
char* endptr = nullptr;
uint64_t flag = strtoul(text.data() + 2, &endptr, 16);
uint64_t flag = strtoul(text.data() + ((difficulty == Difficulty::UNKNOWN) ? 2 : 4), &endptr, 16);
if (endptr != text.data() + text.size()) {
throw std::runtime_error("invalid flag lookup token");
}
if (flag >= 0x400) {
throw std::runtime_error("invalid flag index");
}
return std::make_unique<FlagLookupNode>(flag);
return std::make_unique<QuestFlagLookupNode>(difficulty, flag);
}
if (text.starts_with("CC_")) {
Episode episode;
+16 -4
View File
@@ -15,7 +15,9 @@
class IntegralExpression {
public:
struct Env {
const QuestFlagsForDifficulty* flags;
uint8_t section_id;
Difficulty difficulty;
const QuestFlags* flags;
const PlayerRecordsChallengeBB* challenge_records;
std::shared_ptr<const TeamIndex::Team> team;
size_t num_players;
@@ -105,15 +107,25 @@ protected:
std::unique_ptr<const Node> sub;
};
class FlagLookupNode : public Node {
class SectionIDLookupNode : public Node {
public:
FlagLookupNode(uint16_t flag_index);
virtual ~FlagLookupNode() = default;
SectionIDLookupNode() = default;
virtual ~SectionIDLookupNode() = default;
virtual bool operator==(const Node& other) const;
virtual int64_t evaluate(const Env& env) const;
virtual std::string str() const;
};
class QuestFlagLookupNode : public Node {
public:
QuestFlagLookupNode(Difficulty difficulty, uint16_t flag_index);
virtual ~QuestFlagLookupNode() = default;
virtual bool operator==(const Node& other) const;
virtual int64_t evaluate(const Env& env) const;
virtual std::string str() const;
protected:
Difficulty difficulty;
uint16_t flag_index;
};
+4 -4
View File
@@ -6,10 +6,10 @@
#include "ItemParameterTable.hh"
#include "StaticGameData.hh"
const std::vector<uint8_t> ItemData::StackLimits::DEFAULT_TOOL_LIMITS_DC_NTE({10});
const std::vector<uint8_t> ItemData::StackLimits::DEFAULT_TOOL_LIMITS_V1_V2({10, 10, 1, 10, 10, 10, 10, 10, 10, 1});
const std::vector<uint8_t> ItemData::StackLimits::DEFAULT_TOOL_LIMITS_V3_V4(
{10, 10, 1, 10, 10, 10, 10, 10, 10, 1, 1, 1, 1, 1, 1, 1, 99, 1});
const std::vector<uint8_t> ItemData::StackLimits::DEFAULT_TOOL_LIMITS_DC_NTE{10};
const std::vector<uint8_t> ItemData::StackLimits::DEFAULT_TOOL_LIMITS_V1_V2{10, 10, 1, 10, 10, 10, 10, 10, 10, 1};
const std::vector<uint8_t> ItemData::StackLimits::DEFAULT_TOOL_LIMITS_V3_V4{
10, 10, 1, 10, 10, 10, 10, 10, 10, 1, 1, 1, 1, 1, 1, 1, 99, 1};
const ItemData::StackLimits ItemData::StackLimits::DEFAULT_STACK_LIMITS_DC_NTE(
Version::DC_NTE, ItemData::StackLimits::DEFAULT_TOOL_LIMITS_DC_NTE, 999999);
+3 -1
View File
@@ -4735,7 +4735,9 @@ std::shared_ptr<Lobby> create_game_generic(
if (quest_flag_rewrites && !quest_flag_rewrites->empty()) {
IntegralExpression::Env env = {
.flags = &p->quest_flags.array(difficulty),
.section_id = game->effective_section_id(),
.difficulty = game->difficulty,
.flags = &p->quest_flags,
.challenge_records = &p->challenge_records,
.team = creator_c->team(),
.num_players = 1,
+6 -6
View File
@@ -33,7 +33,7 @@ inline uint8_t get_pre_v1_subcommand(Version v, uint8_t nte_subcommand, uint8_t
}
}
const std::unordered_set<uint32_t> v2_crypt_initial_client_commands({
const std::unordered_set<uint32_t> v2_crypt_initial_client_commands{
0x00260088, // (17) DCNTE license check
0x00B0008B, // (02) DCNTE login
0x00B0018B, // (02) DCNTE login (UDP off)
@@ -52,20 +52,20 @@ const std::unordered_set<uint32_t> v2_crypt_initial_client_commands({
0x0130019D, // (02) DCv2/GCNTE extended login (UDP off)
// Note: PSO PC initial commands are not listed here because we don't use a detector encryption for PSO PC
// (instead, we use the split reconnect command to send PC to a different port).
});
const std::unordered_set<uint32_t> v3_crypt_initial_client_commands({
};
const std::unordered_set<uint32_t> v3_crypt_initial_client_commands{
0x00E000DB, // (17) GC/XB license check
0x00EC009E, // (02) GC login
0x00EC019E, // (02) GC login (UDP off)
0x0150009E, // (02) GC extended login
0x0150019E, // (02) GC extended login (UDP off)
});
};
const std::unordered_set<std::string> bb_crypt_initial_client_commands({
const std::unordered_set<std::string> bb_crypt_initial_client_commands{
std::string("\xB4\x00\x93\x00\x00\x00\x00\x00", 8),
std::string("\xAC\x00\x93\x00\x00\x00\x00\x00", 8),
std::string("\xDC\x00\xDB\x00\x00\x00\x00\x00", 8),
});
};
void send_command(
std::shared_ptr<Client> c,
+6 -6
View File
@@ -515,7 +515,7 @@ struct WeaponRootT {
U32T<BE> favored_grind_range_table; // {u32 min, u32 max}[6]
} __packed_ws_be__(WeaponRootT, 0x20);
const std::array<std::pair<uint8_t, uint8_t>, 0x48> WeaponShopRandomSet::type_defs({
const std::array<std::pair<uint8_t, uint8_t>, 0x48> WeaponShopRandomSet::type_defs{{
/* 00 */ {0x01, 0x00}, // Saber
/* 01 */ {0x01, 0x01}, // Brand
/* 02 */ {0x01, 0x02}, // Buster
@@ -588,9 +588,9 @@ const std::array<std::pair<uint8_t, uint8_t>, 0x48> WeaponShopRandomSet::type_de
/* 45 */ {0x0A, 0x05}, // MACE OF ADAMAN
/* 46 */ {0x0C, 0x05}, // ICE STAFF:DAGON
/* 47 */ {0x0B, 0x05}, // BRAVE HAMMER
});
}};
const std::array<std::pair<uint8_t, uint8_t>, 10> WeaponShopRandomSet::type_defs_39({
const std::array<std::pair<uint8_t, uint8_t>, 10> WeaponShopRandomSet::type_defs_39{{
// Indexed by section_id
{0x28, 0x00}, // HARISEN BATTLE FAN
{0x2A, 0x00}, // AKIKO'S WOK
@@ -602,9 +602,9 @@ const std::array<std::pair<uint8_t, uint8_t>, 10> WeaponShopRandomSet::type_defs
{0x59, 0x00}, // BROOM
{0x8A, 0x00}, // SANGE
{0x99, 0x00}, // ANGEL HARP
});
}};
const std::array<std::pair<uint8_t, uint8_t>, 10> WeaponShopRandomSet::type_defs_3A({
const std::array<std::pair<uint8_t, uint8_t>, 10> WeaponShopRandomSet::type_defs_3A{{
// Indexed by section_id
{0x99, 0x00}, // ANGEL HARP
{0x64, 0x00}, // CHAMELEON SCYTHE
@@ -616,7 +616,7 @@ const std::array<std::pair<uint8_t, uint8_t>, 10> WeaponShopRandomSet::type_defs
{0x2A, 0x00}, // AKIKO'S WOK
{0x48, 0x00}, // SAMBA MARACAS
{0x35, 0x00}, // CRAZY TUNE
});
}};
const std::array<int8_t, 20> WeaponShopRandomSet::bonus_values{
-50, -45, -40, -35, -30, -25, -20, -15, -10, -5, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50};
+8 -8
View File
@@ -121,13 +121,13 @@ static const std::array<const char*, 10> section_id_to_name = {
static const std::array<const char*, 10> section_id_to_abbreviation = {
"Vir", "Grn", "Sky", "Blu", "Prp", "Pnk", "Red", "Orn", "Ylw", "Wht"};
const std::unordered_map<std::string, uint8_t> name_to_section_id({{"viridia", 0},
const std::unordered_map<std::string, uint8_t> name_to_section_id{
// Greennill is spelled Greenill in some places, so we accept both spellings
{"greennill", 1}, {"greenill", 1}, {"skyly", 2}, {"bluefull", 3}, {"purplenum", 4}, {"pinkal", 5}, {"redria", 6},
{"oran", 7}, {"yellowboze", 8}, {"whitill", 9},
{"viridia", 0}, {"greennill", 1}, {"greenill", 1}, {"skyly", 2}, {"bluefull", 3}, {"purplenum", 4}, {"pinkal", 5},
{"redria", 6}, {"oran", 7}, {"yellowboze", 8}, {"whitill", 9},
// Shortcuts for chat commands
{"b", 3}, {"g", 1}, {"o", 7}, {"pi", 5}, {"pu", 4}, {"r", 6}, {"s", 2}, {"v", 0}, {"w", 9}, {"y", 8}});
{"b", 3}, {"g", 1}, {"o", 7}, {"pi", 5}, {"pu", 4}, {"r", 6}, {"s", 2}, {"v", 0}, {"w", 9}, {"y", 8}};
const std::vector<std::string> lobby_event_to_name = {
"none", "xmas", "none", "val", "easter", "hallo", "sonic", "newyear",
@@ -673,10 +673,10 @@ char char_for_challenge_rank(uint8_t rank) {
return "BAS"[rank];
}
const std::array<size_t, 4> DEFAULT_MIN_LEVELS_V123({0, 19, 39, 79});
const std::array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP1({0, 19, 39, 79});
const std::array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP2({0, 29, 49, 89});
const std::array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP4({0, 39, 79, 109});
const std::array<size_t, 4> DEFAULT_MIN_LEVELS_V123{{0, 19, 39, 79}};
const std::array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP1{{0, 19, 39, 79}};
const std::array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP2{{0, 29, 49, 89}};
const std::array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP4{{0, 39, 79, 109}};
const std::array<GameMode, 2> ALL_GAME_MODES_V1 = {GameMode::NORMAL, GameMode::BATTLE};
const std::array<GameMode, 3> ALL_GAME_MODES_V23 = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE};
+22 -22
View File
@@ -1338,35 +1338,35 @@
// doesn't have direct access to the client's quest flags from their save file.
// If you use an expression, the format is the same as the AvailableIf and EnabledIf fields in quest JSONs (see
// system/quests/retrieval/q058.json for details). Note that the expression is only evaluated at the time the game is
// created, and the player-specific tokens like C_EpX_YY refer to the player who created the game.
// created, and the player-specific tokens like CC_EpX_YY refer to the player who created the game.
// The UnlockAllAreas option is now gone; if you want the same behavior as if it were enabled, uncomment all the
// "area unlocks" lines below. Note that some late PSOBB client versions (for example, the Tethealla client) open all
// "area unlock" lines below. Note that some late PSOBB client versions (for example, the Tethealla client) open all
// areas by default, so the area unlock flags have no effect for them.
"QuestFlagRewritesV1V2": {
// "F_0017": true, // Ep1 area unlocks
// "F_0020": true, // Ep1 area unlocks
// "F_002A": true, // Ep1 area unlocks
// "F_0017": true, // Ep1 area unlock (Caves)
// "F_0020": true, // Ep1 area unlock (Mines)
// "F_002A": true, // Ep1 area unlock (Ruins)
},
"QuestFlagRewritesV3": {
// "F_0017": true, // Ep1 area unlocks
// "F_0020": true, // Ep1 area unlocks
// "F_002A": true, // Ep1 area unlocks
// "F_004C": true, // Ep2 area unlocks
// "F_004F": true, // Ep2 area unlocks
// "F_0052": true, // Ep2 area unlocks
// "F_0017": true, // Ep1 area unlock (Caves)
// "F_0020": true, // Ep1 area unlock (Mines)
// "F_002A": true, // Ep1 area unlock (Ruins)
// "F_004C": true, // Ep2 area unlock (VR Spaceship)
// "F_004F": true, // Ep2 area unlock (CCA)
// "F_0052": true, // Ep2 area unlock (Seabed)
},
"QuestFlagRewritesV4": {
// "F_01F9": true, // Ep1 area unlocks
// "F_0201": true, // Ep1 area unlocks
// "F_0207": true, // Ep1 area unlocks
// "F_021B": true, // Ep2 area unlocks
// "F_0225": true, // Ep2 area unlocks
// "F_022F": true, // Ep2 area unlocks
// "F_02BD": true, // Ep4 area unlocks
// "F_02BE": true, // Ep4 area unlocks
// "F_02BF": true, // Ep4 area unlocks
// "F_02C0": true, // Ep4 area unlocks
// "F_02C1": true, // Ep4 area unlocks
// "F_01F9": true, // Ep1 area unlock (Caves)
// "F_0201": true, // Ep1 area unlock (Mines)
// "F_0207": true, // Ep1 area unlock (Ruins)
// "F_021B": true, // Ep2 area unlock (VR Spaceship)
// "F_0225": true, // Ep2 area unlock (CCA)
// "F_022F": true, // Ep2 area unlock (Seabed)
// "F_02BD": true, // Ep4 area unlock (Crater West)
// "F_02BE": true, // Ep4 area unlock (Crater South)
// "F_02BF": true, // Ep4 area unlock (Crater North)
// "F_02C0": true, // Ep4 area unlock (Crater Interior)
// "F_02C1": true, // Ep4 area unlock (Desert)
"F_0046": false, // Ep2 CCA door lock fix
"F_0047": false, // Ep2 CCA door lock fix
"F_0048": false, // Ep2 CCA door lock fix
+4 -1
View File
@@ -5,7 +5,10 @@
// Quests that are considered unavailable don't appear in the quest menu at all. To set a condition for a quest to be
// available, you can set AvailableIf in the quest's JSON file. The value for AvailableIf should be an expression
// that tests any of the following:
// F_XXXX: Quest flag specified in hex (e.g. F_014D)
// P_SID: Current effective section ID for the game; see name_to_section_id in StaticGameData.cc for values
// F_XXXX: Quest flag specified in hex (e.g. F_014D) for the difficulty the player is currently playing in
// F_N_XXXX, F_H_XXXX, F_V_XXXX, F_U_XXXX: Same as F_XXXX, but read from a specific difficulty regardless of which
// difficulty the player is currently playing in
// CC_EpX_Y: Whether or not Challenge stage X in Episode Y is complete (e.g. CC_Ep1_7)
// T_ZZZ: Whether or not the player's BB team has reward with key ZZZ (as defined in TeamRewards in config.json)
// V_NumPlayers: The number of players in the current game