diff --git a/src/Client.cc b/src/Client.cc index c519a8ef..d3b4eb60 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -340,9 +340,11 @@ bool Client::can_see_quest(shared_ptr q, uint8_t difficulty, size_t if (!q->available_expression) { return true; } + auto p = this->character(); string expr = q->available_expression->str(); QuestAvailabilityExpression::Env env = { - .flags = &this->character()->quest_flags.data.at(difficulty), + .flags = &p->quest_flags.data.at(difficulty), + .challenge_records = &p->challenge_records, .team = this->team(), .num_players = num_players, }; @@ -358,9 +360,11 @@ bool Client::can_play_quest(shared_ptr q, uint8_t difficulty, size_ if (!q->enabled_expression) { return true; } + auto p = this->character(); string expr = q->enabled_expression->str(); QuestAvailabilityExpression::Env env = { - .flags = &this->character()->quest_flags.data.at(difficulty), + .flags = &p->quest_flags.data.at(difficulty), + .challenge_records = &p->challenge_records, .team = this->team(), .num_players = num_players, }; diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 9bc31993..7a3b9cf2 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -6736,6 +6736,15 @@ struct G_ServerVersionStrings_GC_Ep3_6xB4x46 { le_uint32_t unused = 0; } __packed__; +struct G_ServerVersionStrings_GC_Ep3_NTE_6xB4x46 { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_ServerVersionStrings_GC_Ep3_NTE_6xB4x46) / 4, 0, 0x46, 0, 0, 0}; + // Ep3 NTE uses the following strings: + // "03/05/29 18:00 by K.Toya" + pstring version_signature; + // "Jun 11 2003 05:02:36" + pstring date_str1; +} __packed__; + // 6xB5x47: Set spectator's CARD level // header.sender_client_id is the spectator's client ID. diff --git a/src/PSOEncryption.cc b/src/PSOEncryption.cc index a741445a..704d5e0f 100644 --- a/src/PSOEncryption.cc +++ b/src/PSOEncryption.cc @@ -889,15 +889,19 @@ uint32_t encrypt_challenge_time(uint16_t value) { available_bits.erase(it); } - return (mask << 16) | (value ^ mask); + uint32_t ret = (mask << 16) | (value ^ mask); + fprintf(stderr, "encrypt_challenge_time %04hX => %08" PRIX32 "\n", value, ret); + return ret; } uint16_t decrypt_challenge_time(uint32_t value) { uint16_t mask = (value >> 0x10); uint8_t mask_one_bits = count_one_bits(mask); - return ((mask_one_bits < 4) || (mask_one_bits > 12)) + uint16_t ret = ((mask_one_bits < 4) || (mask_one_bits > 12)) ? 0xFFFF : ((mask ^ value) & 0xFFFF); + fprintf(stderr, "decrypt_challenge_time %08" PRIX32 " => %04hX\n", value, ret); + return ret; } string decrypt_v2_registry_value(const void* data, size_t size) { diff --git a/src/PSOEncryption.hh b/src/PSOEncryption.hh index 28af7474..c0f13a9d 100644 --- a/src/PSOEncryption.hh +++ b/src/PSOEncryption.hh @@ -252,6 +252,41 @@ void decrypt_trivial_gci_data(void* data, size_t size, uint8_t basis); uint32_t encrypt_challenge_time(uint16_t value); uint16_t decrypt_challenge_time(uint32_t value); +template +class ChallengeTime { +private: + using U32T = typename std::conditional::type; + U32T value; + +public: + ChallengeTime() = default; + ChallengeTime(uint16_t v) { + this->store(v); + } + ChallengeTime(const ChallengeTime& other) = default; + ChallengeTime(ChallengeTime&& other) = default; + ChallengeTime& operator=(const ChallengeTime& other) = default; + ChallengeTime& operator=(ChallengeTime&& other) = default; + + bool has_value() const { + return this->value != 0; + } + + uint16_t load() const { + return decrypt_challenge_time(this->value); + } + operator uint16_t() const { + return this->load(); + } + void store(uint16_t v) { + this->value = ((v == 0) || (v == 0xFFFF)) ? 0 : encrypt_challenge_time(v); + } + ChallengeTime& operator=(uint16_t v) { + this->store(v); + return *this; + } +} __attribute__((packed)); + std::string decrypt_v2_registry_value(const void* data, size_t size); struct DecryptedPR2 { diff --git a/src/PlayerSubordinates.hh b/src/PlayerSubordinates.hh index 88224cec..11bf4a44 100644 --- a/src/PlayerSubordinates.hh +++ b/src/PlayerSubordinates.hh @@ -14,6 +14,7 @@ #include "FileContentsCache.hh" #include "ItemData.hh" #include "LevelTable.hh" +#include "PSOEncryption.hh" #include "Text.hh" #include "Version.hh" @@ -332,7 +333,7 @@ template struct ChallengeAwardState { using U32T = typename std::conditional::type; U32T rank_award_flags = 0; - U32T maximum_rank = 0; // Encrypted; see decrypt_challenge_time + ChallengeTime maximum_rank; } __attribute__((packed)); template @@ -340,7 +341,7 @@ struct PlayerRecordsDCPC_Challenge { /* 00 */ le_uint16_t title_color = 0x7FFF; /* 02 */ parray unknown_u0; /* 04 */ pstring rank_title; - /* 10 */ parray times_ep1_online; // Encrypted; see decrypt_challenge_time. TODO: This might be offline times + /* 10 */ parray, 9> times_ep1_online; // TODO: This might be offline times /* 34 */ uint8_t grave_stage_num = 0; /* 35 */ uint8_t grave_floor = 0; /* 36 */ le_uint16_t grave_deaths = 0; @@ -358,7 +359,7 @@ struct PlayerRecordsDCPC_Challenge { /* 48 */ le_float grave_z = 0.0f; /* 4C */ pstring grave_team; /* 60 */ pstring grave_message; - /* 78 */ parray times_ep1_offline; // Encrypted; see decrypt_challenge_time. TODO: This might be online times + /* 78 */ parray, 9> times_ep1_offline; // TODO: This might be online times /* 9C */ parray unknown_l4; /* A0 */ } __attribute__((packed)); @@ -380,9 +381,9 @@ struct PlayerRecordsV3_Challenge { struct Stats { /* 00:1C */ U16T title_color = 0x7FFF; // XRGB1555 /* 02:1E */ parray unknown_u0; - /* 04:20 */ parray times_ep1_online; // Encrypted; see decrypt_challenge_time - /* 28:44 */ parray times_ep2_online; // Encrypted; see decrypt_challenge_time - /* 3C:58 */ parray times_ep1_offline; // Encrypted; see decrypt_challenge_time + /* 04:20 */ parray, 9> times_ep1_online; + /* 28:44 */ parray, 5> times_ep2_online; + /* 3C:58 */ parray, 9> times_ep1_offline; /* 60:7C */ uint8_t grave_is_ep2 = 0; /* 61:7D */ uint8_t grave_stage_num = 0; /* 62:7E */ uint8_t grave_floor = 0; @@ -419,9 +420,9 @@ struct PlayerRecordsV3_Challenge { struct PlayerRecordsBB_Challenge { /* 0000 */ le_uint16_t title_color = 0x7FFF; // XRGB1555 /* 0002 */ parray unknown_u0; - /* 0004 */ parray times_ep1_online; // Encrypted; see decrypt_challenge_time - /* 0028 */ parray times_ep2_online; // Encrypted; see decrypt_challenge_time - /* 003C */ parray times_ep1_offline; // Encrypted; see decrypt_challenge_time + /* 0004 */ parray, 9> times_ep1_online; + /* 0028 */ parray, 5> times_ep2_online; + /* 003C */ parray, 9> times_ep1_offline; /* 0060 */ uint8_t grave_is_ep2 = 0; /* 0061 */ uint8_t grave_stage_num = 0; /* 0062 */ uint8_t grave_floor = 0; diff --git a/src/QuestAvailabilityExpression.cc b/src/QuestAvailabilityExpression.cc index 43dd2cb0..e5ca65c1 100644 --- a/src/QuestAvailabilityExpression.cc +++ b/src/QuestAvailabilityExpression.cc @@ -184,6 +184,33 @@ string QuestAvailabilityExpression::FlagLookupNode::str() const { return string_printf("F_%04hX", this->flag_index); } +QuestAvailabilityExpression::ChallengeCompletionLookupNode::ChallengeCompletionLookupNode( + Episode episode, uint8_t stage_index) + : episode(episode), + stage_index(stage_index) {} + +bool QuestAvailabilityExpression::ChallengeCompletionLookupNode::operator==(const Node& other) const { + try { + const ChallengeCompletionLookupNode& other_cc = dynamic_cast(other); + return other_cc.episode == this->episode && other_cc.stage_index == this->stage_index; + } catch (const bad_cast&) { + return false; + } +} + +int64_t QuestAvailabilityExpression::ChallengeCompletionLookupNode::evaluate(const Env& env) const { + if (this->episode == Episode::EP1) { + return env.challenge_records->times_ep1_online.at(this->stage_index).has_value(); + } else if (this->episode == Episode::EP2) { + return env.challenge_records->times_ep2_online.at(this->stage_index).has_value(); + } + return false; +} + +string QuestAvailabilityExpression::ChallengeCompletionLookupNode::str() const { + return string_printf("CC_%s_%hhu", abbreviation_for_episode(this->episode), static_cast(this->stage_index + 1)); +} + QuestAvailabilityExpression::TeamRewardLookupNode::TeamRewardLookupNode(const string& reward_name) : reward_name(reward_name) {} @@ -343,6 +370,25 @@ unique_ptr QuestAvailabilityExpression: } return make_unique(flag); } + if (text.starts_with("CC_")) { + Episode episode; + if (text.starts_with("CC_Ep1_")) { + episode = Episode::EP1; + } else if (text.starts_with("CC_Ep2_")) { + episode = Episode::EP2; + } else { + throw runtime_error("invalid challenge episode"); + } + char* endptr = nullptr; + uint64_t stage_index = strtoul(text.data() + 7, &endptr, 0) - 1; + if (endptr != text.data() + text.size()) { + throw runtime_error("invalid challenge completion lookup token"); + } + if ((episode == Episode::EP1 && stage_index > 8) || (episode == Episode::EP2 && stage_index > 4)) { + throw runtime_error("invalid challenge stage index"); + } + return make_unique(episode, stage_index); + } if (text.starts_with("T_")) { return make_unique(string(text.substr(2))); } diff --git a/src/QuestAvailabilityExpression.hh b/src/QuestAvailabilityExpression.hh index 796381dc..405d5b60 100644 --- a/src/QuestAvailabilityExpression.hh +++ b/src/QuestAvailabilityExpression.hh @@ -17,6 +17,7 @@ class QuestAvailabilityExpression { public: struct Env { const QuestFlagsForDifficulty* flags; + const PlayerRecordsBB_Challenge* challenge_records; std::shared_ptr team; size_t num_players; }; @@ -115,6 +116,19 @@ protected: uint16_t flag_index; }; + class ChallengeCompletionLookupNode : public Node { + public: + ChallengeCompletionLookupNode(Episode episode, uint8_t stage_index); + virtual ~ChallengeCompletionLookupNode() = default; + virtual bool operator==(const Node& other) const; + virtual int64_t evaluate(const Env& env) const; + virtual std::string str() const; + + protected: + Episode episode; + uint8_t stage_index; + }; + class TeamRewardLookupNode : public Node { public: TeamRewardLookupNode(const std::string& reward_name); diff --git a/system/quests/challenge-ep1/c88102.json b/system/quests/challenge-ep1/c88102.json index 2faae4ca..0d485ed6 100644 --- a/system/quests/challenge-ep1/c88102.json +++ b/system/quests/challenge-ep1/c88102.json @@ -1,3 +1,5 @@ { - "ChallengeTemplateIndex": 1 + "ChallengeTemplateIndex": 1, + "AvailableIf": "CC_Ep1_1", + "EnabledIf": "CC_Ep1_1" } diff --git a/system/quests/challenge-ep1/c88103.json b/system/quests/challenge-ep1/c88103.json index c9833bf5..5b88afff 100644 --- a/system/quests/challenge-ep1/c88103.json +++ b/system/quests/challenge-ep1/c88103.json @@ -1,3 +1,5 @@ { - "ChallengeTemplateIndex": 2 + "ChallengeTemplateIndex": 2, + "AvailableIf": "CC_Ep1_2", + "EnabledIf": "CC_Ep1_2" } diff --git a/system/quests/challenge-ep1/c88104.json b/system/quests/challenge-ep1/c88104.json index 5fa3f804..2ff5f03e 100644 --- a/system/quests/challenge-ep1/c88104.json +++ b/system/quests/challenge-ep1/c88104.json @@ -1,3 +1,5 @@ { - "ChallengeTemplateIndex": 3 + "ChallengeTemplateIndex": 3, + "AvailableIf": "CC_Ep1_3", + "EnabledIf": "CC_Ep1_3" } diff --git a/system/quests/challenge-ep1/c88105.json b/system/quests/challenge-ep1/c88105.json index 45a7511d..8921a6c3 100644 --- a/system/quests/challenge-ep1/c88105.json +++ b/system/quests/challenge-ep1/c88105.json @@ -1,3 +1,5 @@ { - "ChallengeTemplateIndex": 4 + "ChallengeTemplateIndex": 4, + "AvailableIf": "CC_Ep1_4", + "EnabledIf": "CC_Ep1_4" } diff --git a/system/quests/challenge-ep1/c88106.json b/system/quests/challenge-ep1/c88106.json index bfb434cf..1349ff2c 100644 --- a/system/quests/challenge-ep1/c88106.json +++ b/system/quests/challenge-ep1/c88106.json @@ -1,3 +1,5 @@ { - "ChallengeTemplateIndex": 5 + "ChallengeTemplateIndex": 5, + "AvailableIf": "CC_Ep1_5", + "EnabledIf": "CC_Ep1_5" } diff --git a/system/quests/challenge-ep1/c88107.json b/system/quests/challenge-ep1/c88107.json index 30b0532f..c27c415f 100644 --- a/system/quests/challenge-ep1/c88107.json +++ b/system/quests/challenge-ep1/c88107.json @@ -1,3 +1,5 @@ { - "ChallengeTemplateIndex": 6 + "ChallengeTemplateIndex": 6, + "AvailableIf": "CC_Ep1_6", + "EnabledIf": "CC_Ep1_6" } diff --git a/system/quests/challenge-ep1/c88108.json b/system/quests/challenge-ep1/c88108.json index 5b750df8..ef401d5e 100644 --- a/system/quests/challenge-ep1/c88108.json +++ b/system/quests/challenge-ep1/c88108.json @@ -1,3 +1,5 @@ { - "ChallengeTemplateIndex": 7 + "ChallengeTemplateIndex": 7, + "AvailableIf": "CC_Ep1_7", + "EnabledIf": "CC_Ep1_7" } diff --git a/system/quests/challenge-ep1/c88109.json b/system/quests/challenge-ep1/c88109.json index e7f35692..b0a58fe6 100644 --- a/system/quests/challenge-ep1/c88109.json +++ b/system/quests/challenge-ep1/c88109.json @@ -1,3 +1,5 @@ { - "ChallengeTemplateIndex": 8 + "ChallengeTemplateIndex": 8, + "AvailableIf": "CC_Ep1_8", + "EnabledIf": "CC_Ep1_8" } diff --git a/system/quests/challenge-ep2/d88202.json b/system/quests/challenge-ep2/d88202.json index 45a7511d..7bc5ee08 100644 --- a/system/quests/challenge-ep2/d88202.json +++ b/system/quests/challenge-ep2/d88202.json @@ -1,3 +1,5 @@ { - "ChallengeTemplateIndex": 4 + "ChallengeTemplateIndex": 4, + "AvailableIf": "CC_Ep2_1", + "EnabledIf": "CC_Ep2_1" } diff --git a/system/quests/challenge-ep2/d88203.json b/system/quests/challenge-ep2/d88203.json index bfb434cf..79c303d7 100644 --- a/system/quests/challenge-ep2/d88203.json +++ b/system/quests/challenge-ep2/d88203.json @@ -1,3 +1,5 @@ { - "ChallengeTemplateIndex": 5 + "ChallengeTemplateIndex": 5, + "AvailableIf": "CC_Ep2_2", + "EnabledIf": "CC_Ep2_2" } diff --git a/system/quests/challenge-ep2/d88204.json b/system/quests/challenge-ep2/d88204.json index e7f35692..b6375928 100644 --- a/system/quests/challenge-ep2/d88204.json +++ b/system/quests/challenge-ep2/d88204.json @@ -1,3 +1,5 @@ { - "ChallengeTemplateIndex": 8 + "ChallengeTemplateIndex": 8, + "AvailableIf": "CC_Ep2_3", + "EnabledIf": "CC_Ep2_3" } diff --git a/system/quests/challenge-ep2/d88205.json b/system/quests/challenge-ep2/d88205.json index e7f35692..a528f650 100644 --- a/system/quests/challenge-ep2/d88205.json +++ b/system/quests/challenge-ep2/d88205.json @@ -1,3 +1,5 @@ { - "ChallengeTemplateIndex": 8 + "ChallengeTemplateIndex": 8, + "AvailableIf": "CC_Ep2_4", + "EnabledIf": "CC_Ep2_4" }