From 1737d8abc806e8fe7c07e26ffd6475ee77d111f2 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sat, 13 Jun 2026 20:07:23 -0700 Subject: [PATCH] add more options in IntegralExpression --- src/ChoiceSearch.cc | 4 +-- src/Client.cc | 4 ++- src/DownloadSession.cc | 4 +-- src/Episode3/Server.cc | 4 +-- src/IntegralExpression.cc | 49 ++++++++++++++++++++++++------- src/IntegralExpression.hh | 20 ++++++++++--- src/ItemData.cc | 8 ++--- src/ReceiveCommands.cc | 4 ++- src/SendCommands.cc | 12 ++++---- src/ShopRandomSets.cc | 12 ++++---- src/StaticGameData.cc | 16 +++++----- system/config.example.json | 44 +++++++++++++-------------- system/quests/retrieval/q058.json | 5 +++- 13 files changed, 117 insertions(+), 69 deletions(-) diff --git a/src/ChoiceSearch.cc b/src/ChoiceSearch.cc index 5d45cd79..eed10627 100644 --- a/src/ChoiceSearch.cc +++ b/src/ChoiceSearch.cc @@ -5,7 +5,7 @@ #include "Client.hh" -const std::vector CHOICE_SEARCH_CATEGORIES({ +const std::vector CHOICE_SEARCH_CATEGORIES{ ChoiceSearchCategory{ .id = 0x0001, .name = "Level", @@ -145,4 +145,4 @@ const std::vector CHOICE_SEARCH_CATEGORIES({ return (choice_id == 0) || (target_choice_id == 0) || (choice_id == target_choice_id); }, }, -}); +}; diff --git a/src/Client.cc b/src/Client.cc index 09601ae8..c6e7567c 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -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(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, diff --git a/src/DownloadSession.cc b/src/DownloadSession.cc index 93a708b1..900e291c 100644 --- a/src/DownloadSession.cc +++ b/src/DownloadSession.cc @@ -811,7 +811,7 @@ void DownloadSession::on_request_complete() { } } -const std::vector DownloadSession::game_configs({ +const std::vector 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::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}, -}); +}; diff --git a/src/Episode3/Server.cc b/src/Episode3/Server.cc index 5285d6ed..77504133 100644 --- a/src/Episode3/Server.cc +++ b/src/Episode3/Server.cc @@ -1738,7 +1738,7 @@ bool Server::update_registration_phase() { return true; } -const std::unordered_map Server::subcommand_handlers({ +const std::unordered_map 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 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 sender_c, const std::string& data) { auto header = check_size_t(data, 0xFFFF); diff --git a/src/IntegralExpression.cc b/src/IntegralExpression.cc index 79b4dc85..d0ffb362 100644 --- a/src/IntegralExpression.cc +++ b/src/IntegralExpression.cc @@ -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(&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(other); - return other_flag.flag_index == this->flag_index; + const QuestFlagLookupNode& other_flag = dynamic_cast(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 IntegralExpression::parse_expr(s } // Check for env lookups + if (text == "P_SID") { + return std::make_unique(); + } 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(flag); + return std::make_unique(difficulty, flag); } if (text.starts_with("CC_")) { Episode episode; diff --git a/src/IntegralExpression.hh b/src/IntegralExpression.hh index 553acc27..e22bd188 100644 --- a/src/IntegralExpression.hh +++ b/src/IntegralExpression.hh @@ -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 team; size_t num_players; @@ -105,15 +107,25 @@ protected: std::unique_ptr 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; }; diff --git a/src/ItemData.cc b/src/ItemData.cc index f53f1f35..acd79fa8 100644 --- a/src/ItemData.cc +++ b/src/ItemData.cc @@ -6,10 +6,10 @@ #include "ItemParameterTable.hh" #include "StaticGameData.hh" -const std::vector ItemData::StackLimits::DEFAULT_TOOL_LIMITS_DC_NTE({10}); -const std::vector ItemData::StackLimits::DEFAULT_TOOL_LIMITS_V1_V2({10, 10, 1, 10, 10, 10, 10, 10, 10, 1}); -const std::vector 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 ItemData::StackLimits::DEFAULT_TOOL_LIMITS_DC_NTE{10}; +const std::vector ItemData::StackLimits::DEFAULT_TOOL_LIMITS_V1_V2{10, 10, 1, 10, 10, 10, 10, 10, 10, 1}; +const std::vector 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); diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index ab761de0..78603e2c 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -4735,7 +4735,9 @@ std::shared_ptr 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, diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 9afa89b1..a2742289 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -33,7 +33,7 @@ inline uint8_t get_pre_v1_subcommand(Version v, uint8_t nte_subcommand, uint8_t } } -const std::unordered_set v2_crypt_initial_client_commands({ +const std::unordered_set 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 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 v3_crypt_initial_client_commands({ +}; +const std::unordered_set 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 bb_crypt_initial_client_commands({ +const std::unordered_set 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 c, diff --git a/src/ShopRandomSets.cc b/src/ShopRandomSets.cc index ef3bf273..a31e7071 100644 --- a/src/ShopRandomSets.cc +++ b/src/ShopRandomSets.cc @@ -515,7 +515,7 @@ struct WeaponRootT { U32T favored_grind_range_table; // {u32 min, u32 max}[6] } __packed_ws_be__(WeaponRootT, 0x20); -const std::array, 0x48> WeaponShopRandomSet::type_defs({ +const std::array, 0x48> WeaponShopRandomSet::type_defs{{ /* 00 */ {0x01, 0x00}, // Saber /* 01 */ {0x01, 0x01}, // Brand /* 02 */ {0x01, 0x02}, // Buster @@ -588,9 +588,9 @@ const std::array, 0x48> WeaponShopRandomSet::type_de /* 45 */ {0x0A, 0x05}, // MACE OF ADAMAN /* 46 */ {0x0C, 0x05}, // ICE STAFF:DAGON /* 47 */ {0x0B, 0x05}, // BRAVE HAMMER -}); +}}; -const std::array, 10> WeaponShopRandomSet::type_defs_39({ +const std::array, 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, 10> WeaponShopRandomSet::type_defs {0x59, 0x00}, // BROOM {0x8A, 0x00}, // SANGE {0x99, 0x00}, // ANGEL HARP -}); +}}; -const std::array, 10> WeaponShopRandomSet::type_defs_3A({ +const std::array, 10> WeaponShopRandomSet::type_defs_3A{{ // Indexed by section_id {0x99, 0x00}, // ANGEL HARP {0x64, 0x00}, // CHAMELEON SCYTHE @@ -616,7 +616,7 @@ const std::array, 10> WeaponShopRandomSet::type_defs {0x2A, 0x00}, // AKIKO'S WOK {0x48, 0x00}, // SAMBA MARACAS {0x35, 0x00}, // CRAZY TUNE -}); +}}; const std::array WeaponShopRandomSet::bonus_values{ -50, -45, -40, -35, -30, -25, -20, -15, -10, -5, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50}; diff --git a/src/StaticGameData.cc b/src/StaticGameData.cc index a703149e..7922b385 100644 --- a/src/StaticGameData.cc +++ b/src/StaticGameData.cc @@ -121,13 +121,13 @@ static const std::array section_id_to_name = { static const std::array section_id_to_abbreviation = { "Vir", "Grn", "Sky", "Blu", "Prp", "Pnk", "Red", "Orn", "Ylw", "Wht"}; -const std::unordered_map name_to_section_id({{"viridia", 0}, +const std::unordered_map 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 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 DEFAULT_MIN_LEVELS_V123({0, 19, 39, 79}); -const std::array DEFAULT_MIN_LEVELS_V4_EP1({0, 19, 39, 79}); -const std::array DEFAULT_MIN_LEVELS_V4_EP2({0, 29, 49, 89}); -const std::array DEFAULT_MIN_LEVELS_V4_EP4({0, 39, 79, 109}); +const std::array DEFAULT_MIN_LEVELS_V123{{0, 19, 39, 79}}; +const std::array DEFAULT_MIN_LEVELS_V4_EP1{{0, 19, 39, 79}}; +const std::array DEFAULT_MIN_LEVELS_V4_EP2{{0, 29, 49, 89}}; +const std::array DEFAULT_MIN_LEVELS_V4_EP4{{0, 39, 79, 109}}; const std::array ALL_GAME_MODES_V1 = {GameMode::NORMAL, GameMode::BATTLE}; const std::array ALL_GAME_MODES_V23 = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE}; diff --git a/system/config.example.json b/system/config.example.json index 1f5cd287..00db2c0a 100644 --- a/system/config.example.json +++ b/system/config.example.json @@ -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 diff --git a/system/quests/retrieval/q058.json b/system/quests/retrieval/q058.json index 68c1c6de..2e71fd98 100644 --- a/system/quests/retrieval/q058.json +++ b/system/quests/retrieval/q058.json @@ -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