implement challenge stage unlocks

This commit is contained in:
Martin Michelsen
2024-01-14 09:47:03 -08:00
parent 02e8f8ea8b
commit c15e154846
19 changed files with 162 additions and 25 deletions
+6 -2
View File
@@ -340,9 +340,11 @@ bool Client::can_see_quest(shared_ptr<const Quest> q, uint8_t difficulty, size_t
if (!q->available_expression) { if (!q->available_expression) {
return true; return true;
} }
auto p = this->character();
string expr = q->available_expression->str(); string expr = q->available_expression->str();
QuestAvailabilityExpression::Env env = { 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(), .team = this->team(),
.num_players = num_players, .num_players = num_players,
}; };
@@ -358,9 +360,11 @@ bool Client::can_play_quest(shared_ptr<const Quest> q, uint8_t difficulty, size_
if (!q->enabled_expression) { if (!q->enabled_expression) {
return true; return true;
} }
auto p = this->character();
string expr = q->enabled_expression->str(); string expr = q->enabled_expression->str();
QuestAvailabilityExpression::Env env = { 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(), .team = this->team(),
.num_players = num_players, .num_players = num_players,
}; };
+9
View File
@@ -6736,6 +6736,15 @@ struct G_ServerVersionStrings_GC_Ep3_6xB4x46 {
le_uint32_t unused = 0; le_uint32_t unused = 0;
} __packed__; } __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<TextEncoding::MARKED, 0x40> version_signature;
// "Jun 11 2003 05:02:36"
pstring<TextEncoding::MARKED, 0x40> date_str1;
} __packed__;
// 6xB5x47: Set spectator's CARD level // 6xB5x47: Set spectator's CARD level
// header.sender_client_id is the spectator's client ID. // header.sender_client_id is the spectator's client ID.
+6 -2
View File
@@ -889,15 +889,19 @@ uint32_t encrypt_challenge_time(uint16_t value) {
available_bits.erase(it); 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 decrypt_challenge_time(uint32_t value) {
uint16_t mask = (value >> 0x10); uint16_t mask = (value >> 0x10);
uint8_t mask_one_bits = count_one_bits(mask); 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 ? 0xFFFF
: ((mask ^ value) & 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) { string decrypt_v2_registry_value(const void* data, size_t size) {
+35
View File
@@ -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); uint32_t encrypt_challenge_time(uint16_t value);
uint16_t decrypt_challenge_time(uint32_t value); uint16_t decrypt_challenge_time(uint32_t value);
template <bool IsBigEndian>
class ChallengeTime {
private:
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::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); std::string decrypt_v2_registry_value(const void* data, size_t size);
struct DecryptedPR2 { struct DecryptedPR2 {
+10 -9
View File
@@ -14,6 +14,7 @@
#include "FileContentsCache.hh" #include "FileContentsCache.hh"
#include "ItemData.hh" #include "ItemData.hh"
#include "LevelTable.hh" #include "LevelTable.hh"
#include "PSOEncryption.hh"
#include "Text.hh" #include "Text.hh"
#include "Version.hh" #include "Version.hh"
@@ -332,7 +333,7 @@ template <bool IsBigEndian>
struct ChallengeAwardState { struct ChallengeAwardState {
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type; using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
U32T rank_award_flags = 0; U32T rank_award_flags = 0;
U32T maximum_rank = 0; // Encrypted; see decrypt_challenge_time ChallengeTime<IsBigEndian> maximum_rank;
} __attribute__((packed)); } __attribute__((packed));
template <TextEncoding UnencryptedEncoding, TextEncoding EncryptedEncoding> template <TextEncoding UnencryptedEncoding, TextEncoding EncryptedEncoding>
@@ -340,7 +341,7 @@ struct PlayerRecordsDCPC_Challenge {
/* 00 */ le_uint16_t title_color = 0x7FFF; /* 00 */ le_uint16_t title_color = 0x7FFF;
/* 02 */ parray<uint8_t, 2> unknown_u0; /* 02 */ parray<uint8_t, 2> unknown_u0;
/* 04 */ pstring<EncryptedEncoding, 0x0C> rank_title; /* 04 */ pstring<EncryptedEncoding, 0x0C> rank_title;
/* 10 */ parray<le_uint32_t, 9> times_ep1_online; // Encrypted; see decrypt_challenge_time. TODO: This might be offline times /* 10 */ parray<ChallengeTime<false>, 9> times_ep1_online; // TODO: This might be offline times
/* 34 */ uint8_t grave_stage_num = 0; /* 34 */ uint8_t grave_stage_num = 0;
/* 35 */ uint8_t grave_floor = 0; /* 35 */ uint8_t grave_floor = 0;
/* 36 */ le_uint16_t grave_deaths = 0; /* 36 */ le_uint16_t grave_deaths = 0;
@@ -358,7 +359,7 @@ struct PlayerRecordsDCPC_Challenge {
/* 48 */ le_float grave_z = 0.0f; /* 48 */ le_float grave_z = 0.0f;
/* 4C */ pstring<UnencryptedEncoding, 0x14> grave_team; /* 4C */ pstring<UnencryptedEncoding, 0x14> grave_team;
/* 60 */ pstring<UnencryptedEncoding, 0x18> grave_message; /* 60 */ pstring<UnencryptedEncoding, 0x18> grave_message;
/* 78 */ parray<le_uint32_t, 9> times_ep1_offline; // Encrypted; see decrypt_challenge_time. TODO: This might be online times /* 78 */ parray<ChallengeTime<false>, 9> times_ep1_offline; // TODO: This might be online times
/* 9C */ parray<uint8_t, 4> unknown_l4; /* 9C */ parray<uint8_t, 4> unknown_l4;
/* A0 */ /* A0 */
} __attribute__((packed)); } __attribute__((packed));
@@ -380,9 +381,9 @@ struct PlayerRecordsV3_Challenge {
struct Stats { struct Stats {
/* 00:1C */ U16T title_color = 0x7FFF; // XRGB1555 /* 00:1C */ U16T title_color = 0x7FFF; // XRGB1555
/* 02:1E */ parray<uint8_t, 2> unknown_u0; /* 02:1E */ parray<uint8_t, 2> unknown_u0;
/* 04:20 */ parray<U32T, 9> times_ep1_online; // Encrypted; see decrypt_challenge_time /* 04:20 */ parray<ChallengeTime<IsBigEndian>, 9> times_ep1_online;
/* 28:44 */ parray<U32T, 5> times_ep2_online; // Encrypted; see decrypt_challenge_time /* 28:44 */ parray<ChallengeTime<IsBigEndian>, 5> times_ep2_online;
/* 3C:58 */ parray<U32T, 9> times_ep1_offline; // Encrypted; see decrypt_challenge_time /* 3C:58 */ parray<ChallengeTime<IsBigEndian>, 9> times_ep1_offline;
/* 60:7C */ uint8_t grave_is_ep2 = 0; /* 60:7C */ uint8_t grave_is_ep2 = 0;
/* 61:7D */ uint8_t grave_stage_num = 0; /* 61:7D */ uint8_t grave_stage_num = 0;
/* 62:7E */ uint8_t grave_floor = 0; /* 62:7E */ uint8_t grave_floor = 0;
@@ -419,9 +420,9 @@ struct PlayerRecordsV3_Challenge {
struct PlayerRecordsBB_Challenge { struct PlayerRecordsBB_Challenge {
/* 0000 */ le_uint16_t title_color = 0x7FFF; // XRGB1555 /* 0000 */ le_uint16_t title_color = 0x7FFF; // XRGB1555
/* 0002 */ parray<uint8_t, 2> unknown_u0; /* 0002 */ parray<uint8_t, 2> unknown_u0;
/* 0004 */ parray<le_uint32_t, 9> times_ep1_online; // Encrypted; see decrypt_challenge_time /* 0004 */ parray<ChallengeTime<false>, 9> times_ep1_online;
/* 0028 */ parray<le_uint32_t, 5> times_ep2_online; // Encrypted; see decrypt_challenge_time /* 0028 */ parray<ChallengeTime<false>, 5> times_ep2_online;
/* 003C */ parray<le_uint32_t, 9> times_ep1_offline; // Encrypted; see decrypt_challenge_time /* 003C */ parray<ChallengeTime<false>, 9> times_ep1_offline;
/* 0060 */ uint8_t grave_is_ep2 = 0; /* 0060 */ uint8_t grave_is_ep2 = 0;
/* 0061 */ uint8_t grave_stage_num = 0; /* 0061 */ uint8_t grave_stage_num = 0;
/* 0062 */ uint8_t grave_floor = 0; /* 0062 */ uint8_t grave_floor = 0;
+46
View File
@@ -184,6 +184,33 @@ string QuestAvailabilityExpression::FlagLookupNode::str() const {
return string_printf("F_%04hX", this->flag_index); 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<const ChallengeCompletionLookupNode&>(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<uint8_t>(this->stage_index + 1));
}
QuestAvailabilityExpression::TeamRewardLookupNode::TeamRewardLookupNode(const string& reward_name) QuestAvailabilityExpression::TeamRewardLookupNode::TeamRewardLookupNode(const string& reward_name)
: reward_name(reward_name) {} : reward_name(reward_name) {}
@@ -343,6 +370,25 @@ unique_ptr<const QuestAvailabilityExpression::Node> QuestAvailabilityExpression:
} }
return make_unique<FlagLookupNode>(flag); return make_unique<FlagLookupNode>(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<ChallengeCompletionLookupNode>(episode, stage_index);
}
if (text.starts_with("T_")) { if (text.starts_with("T_")) {
return make_unique<TeamRewardLookupNode>(string(text.substr(2))); return make_unique<TeamRewardLookupNode>(string(text.substr(2)));
} }
+14
View File
@@ -17,6 +17,7 @@ class QuestAvailabilityExpression {
public: public:
struct Env { struct Env {
const QuestFlagsForDifficulty* flags; const QuestFlagsForDifficulty* flags;
const PlayerRecordsBB_Challenge* challenge_records;
std::shared_ptr<const TeamIndex::Team> team; std::shared_ptr<const TeamIndex::Team> team;
size_t num_players; size_t num_players;
}; };
@@ -115,6 +116,19 @@ protected:
uint16_t flag_index; 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 { class TeamRewardLookupNode : public Node {
public: public:
TeamRewardLookupNode(const std::string& reward_name); TeamRewardLookupNode(const std::string& reward_name);
+3 -1
View File
@@ -1,3 +1,5 @@
{ {
"ChallengeTemplateIndex": 1 "ChallengeTemplateIndex": 1,
"AvailableIf": "CC_Ep1_1",
"EnabledIf": "CC_Ep1_1"
} }
+3 -1
View File
@@ -1,3 +1,5 @@
{ {
"ChallengeTemplateIndex": 2 "ChallengeTemplateIndex": 2,
"AvailableIf": "CC_Ep1_2",
"EnabledIf": "CC_Ep1_2"
} }
+3 -1
View File
@@ -1,3 +1,5 @@
{ {
"ChallengeTemplateIndex": 3 "ChallengeTemplateIndex": 3,
"AvailableIf": "CC_Ep1_3",
"EnabledIf": "CC_Ep1_3"
} }
+3 -1
View File
@@ -1,3 +1,5 @@
{ {
"ChallengeTemplateIndex": 4 "ChallengeTemplateIndex": 4,
"AvailableIf": "CC_Ep1_4",
"EnabledIf": "CC_Ep1_4"
} }
+3 -1
View File
@@ -1,3 +1,5 @@
{ {
"ChallengeTemplateIndex": 5 "ChallengeTemplateIndex": 5,
"AvailableIf": "CC_Ep1_5",
"EnabledIf": "CC_Ep1_5"
} }
+3 -1
View File
@@ -1,3 +1,5 @@
{ {
"ChallengeTemplateIndex": 6 "ChallengeTemplateIndex": 6,
"AvailableIf": "CC_Ep1_6",
"EnabledIf": "CC_Ep1_6"
} }
+3 -1
View File
@@ -1,3 +1,5 @@
{ {
"ChallengeTemplateIndex": 7 "ChallengeTemplateIndex": 7,
"AvailableIf": "CC_Ep1_7",
"EnabledIf": "CC_Ep1_7"
} }
+3 -1
View File
@@ -1,3 +1,5 @@
{ {
"ChallengeTemplateIndex": 8 "ChallengeTemplateIndex": 8,
"AvailableIf": "CC_Ep1_8",
"EnabledIf": "CC_Ep1_8"
} }
+3 -1
View File
@@ -1,3 +1,5 @@
{ {
"ChallengeTemplateIndex": 4 "ChallengeTemplateIndex": 4,
"AvailableIf": "CC_Ep2_1",
"EnabledIf": "CC_Ep2_1"
} }
+3 -1
View File
@@ -1,3 +1,5 @@
{ {
"ChallengeTemplateIndex": 5 "ChallengeTemplateIndex": 5,
"AvailableIf": "CC_Ep2_2",
"EnabledIf": "CC_Ep2_2"
} }
+3 -1
View File
@@ -1,3 +1,5 @@
{ {
"ChallengeTemplateIndex": 8 "ChallengeTemplateIndex": 8,
"AvailableIf": "CC_Ep2_3",
"EnabledIf": "CC_Ep2_3"
} }
+3 -1
View File
@@ -1,3 +1,5 @@
{ {
"ChallengeTemplateIndex": 8 "ChallengeTemplateIndex": 8,
"AvailableIf": "CC_Ep2_4",
"EnabledIf": "CC_Ep2_4"
} }