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) {
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<const Quest> 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,
};
+9
View File
@@ -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<TextEncoding::MARKED, 0x40> version_signature;
// "Jun 11 2003 05:02:36"
pstring<TextEncoding::MARKED, 0x40> date_str1;
} __packed__;
// 6xB5x47: Set spectator's CARD level
// 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);
}
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) {
+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);
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);
struct DecryptedPR2 {
+10 -9
View File
@@ -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 <bool IsBigEndian>
struct ChallengeAwardState {
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
U32T rank_award_flags = 0;
U32T maximum_rank = 0; // Encrypted; see decrypt_challenge_time
ChallengeTime<IsBigEndian> maximum_rank;
} __attribute__((packed));
template <TextEncoding UnencryptedEncoding, TextEncoding EncryptedEncoding>
@@ -340,7 +341,7 @@ struct PlayerRecordsDCPC_Challenge {
/* 00 */ le_uint16_t title_color = 0x7FFF;
/* 02 */ parray<uint8_t, 2> unknown_u0;
/* 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;
/* 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<UnencryptedEncoding, 0x14> grave_team;
/* 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;
/* A0 */
} __attribute__((packed));
@@ -380,9 +381,9 @@ struct PlayerRecordsV3_Challenge {
struct Stats {
/* 00:1C */ U16T title_color = 0x7FFF; // XRGB1555
/* 02:1E */ parray<uint8_t, 2> unknown_u0;
/* 04:20 */ parray<U32T, 9> times_ep1_online; // Encrypted; see decrypt_challenge_time
/* 28:44 */ parray<U32T, 5> times_ep2_online; // Encrypted; see decrypt_challenge_time
/* 3C:58 */ parray<U32T, 9> times_ep1_offline; // Encrypted; see decrypt_challenge_time
/* 04:20 */ parray<ChallengeTime<IsBigEndian>, 9> times_ep1_online;
/* 28:44 */ parray<ChallengeTime<IsBigEndian>, 5> times_ep2_online;
/* 3C:58 */ parray<ChallengeTime<IsBigEndian>, 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<uint8_t, 2> unknown_u0;
/* 0004 */ parray<le_uint32_t, 9> times_ep1_online; // Encrypted; see decrypt_challenge_time
/* 0028 */ parray<le_uint32_t, 5> times_ep2_online; // Encrypted; see decrypt_challenge_time
/* 003C */ parray<le_uint32_t, 9> times_ep1_offline; // Encrypted; see decrypt_challenge_time
/* 0004 */ parray<ChallengeTime<false>, 9> times_ep1_online;
/* 0028 */ parray<ChallengeTime<false>, 5> times_ep2_online;
/* 003C */ parray<ChallengeTime<false>, 9> times_ep1_offline;
/* 0060 */ uint8_t grave_is_ep2 = 0;
/* 0061 */ uint8_t grave_stage_num = 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);
}
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)
: reward_name(reward_name) {}
@@ -343,6 +370,25 @@ unique_ptr<const QuestAvailabilityExpression::Node> QuestAvailabilityExpression:
}
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_")) {
return make_unique<TeamRewardLookupNode>(string(text.substr(2)));
}
+14
View File
@@ -17,6 +17,7 @@ class QuestAvailabilityExpression {
public:
struct Env {
const QuestFlagsForDifficulty* flags;
const PlayerRecordsBB_Challenge* challenge_records;
std::shared_ptr<const TeamIndex::Team> 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);
+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"
}