implement challenge stage unlocks
This commit is contained in:
+6
-2
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"ChallengeTemplateIndex": 1
|
"ChallengeTemplateIndex": 1,
|
||||||
|
"AvailableIf": "CC_Ep1_1",
|
||||||
|
"EnabledIf": "CC_Ep1_1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"ChallengeTemplateIndex": 2
|
"ChallengeTemplateIndex": 2,
|
||||||
|
"AvailableIf": "CC_Ep1_2",
|
||||||
|
"EnabledIf": "CC_Ep1_2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"ChallengeTemplateIndex": 3
|
"ChallengeTemplateIndex": 3,
|
||||||
|
"AvailableIf": "CC_Ep1_3",
|
||||||
|
"EnabledIf": "CC_Ep1_3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"ChallengeTemplateIndex": 4
|
"ChallengeTemplateIndex": 4,
|
||||||
|
"AvailableIf": "CC_Ep1_4",
|
||||||
|
"EnabledIf": "CC_Ep1_4"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"ChallengeTemplateIndex": 5
|
"ChallengeTemplateIndex": 5,
|
||||||
|
"AvailableIf": "CC_Ep1_5",
|
||||||
|
"EnabledIf": "CC_Ep1_5"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"ChallengeTemplateIndex": 6
|
"ChallengeTemplateIndex": 6,
|
||||||
|
"AvailableIf": "CC_Ep1_6",
|
||||||
|
"EnabledIf": "CC_Ep1_6"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"ChallengeTemplateIndex": 7
|
"ChallengeTemplateIndex": 7,
|
||||||
|
"AvailableIf": "CC_Ep1_7",
|
||||||
|
"EnabledIf": "CC_Ep1_7"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"ChallengeTemplateIndex": 8
|
"ChallengeTemplateIndex": 8,
|
||||||
|
"AvailableIf": "CC_Ep1_8",
|
||||||
|
"EnabledIf": "CC_Ep1_8"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"ChallengeTemplateIndex": 4
|
"ChallengeTemplateIndex": 4,
|
||||||
|
"AvailableIf": "CC_Ep2_1",
|
||||||
|
"EnabledIf": "CC_Ep2_1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"ChallengeTemplateIndex": 5
|
"ChallengeTemplateIndex": 5,
|
||||||
|
"AvailableIf": "CC_Ep2_2",
|
||||||
|
"EnabledIf": "CC_Ep2_2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"ChallengeTemplateIndex": 8
|
"ChallengeTemplateIndex": 8,
|
||||||
|
"AvailableIf": "CC_Ep2_3",
|
||||||
|
"EnabledIf": "CC_Ep2_3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"ChallengeTemplateIndex": 8
|
"ChallengeTemplateIndex": 8,
|
||||||
|
"AvailableIf": "CC_Ep2_4",
|
||||||
|
"EnabledIf": "CC_Ep2_4"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user