From 29f200b83e136dbcc287e2381929243ac0e2852e Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sun, 28 Apr 2024 00:23:21 -0700 Subject: [PATCH] add a way for joinable quests to lock themselves --- src/ChatCommands.cc | 4 ++-- src/CommandFormats.hh | 6 +++--- src/HTTPServer.cc | 1 + src/Quest.cc | 25 ++++++++++++++++--------- src/Quest.hh | 5 ++++- src/ReceiveSubcommands.cc | 32 +++++++++++++++++++++++++++++++- system/quests/battle/b88001.json | 6 ++++++ 7 files changed, 63 insertions(+), 16 deletions(-) diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index 189d2835..cf63c87c 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -577,7 +577,7 @@ static void server_command_qsync_qsyncall(shared_ptr c, const std::strin return; } - G_SyncQuestData_6x77 cmd; + G_SyncQuestRegister_6x77 cmd; cmd.header = {0x77, 0x03, 0x0000}; cmd.register_number = stoul(tokens[0].substr(1), nullptr, 0); cmd.unused = 0; @@ -616,7 +616,7 @@ static void proxy_command_qsync_qsyncall(shared_ptr return; } - G_SyncQuestData_6x77 cmd; + G_SyncQuestRegister_6x77 cmd; cmd.header = {0x77, 0x03, 0x0000}; cmd.register_number = stoul(tokens[0].substr(1), nullptr, 0); cmd.unused = 0; diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 394cc4c9..73c9d85e 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -5034,10 +5034,10 @@ struct G_SetEntitySetFlags_6x76 { le_uint16_t flags = 0; } __packed_ws__(G_SetEntitySetFlags_6x76, 8); -// 6x77: Sync quest data +// 6x77: Sync quest register // This is sent by the client when an opcode D9 is executed within a quest. -struct G_SyncQuestData_6x77 { +struct G_SyncQuestRegister_6x77 { G_UnusedHeader header; le_uint16_t register_number = 0; // Must be < 0x100 le_uint16_t unused = 0; @@ -5045,7 +5045,7 @@ struct G_SyncQuestData_6x77 { le_uint32_t as_int; le_float as_float; } __packed__ value; -} __packed_ws__(G_SyncQuestData_6x77, 0x0C); +} __packed_ws__(G_SyncQuestRegister_6x77, 0x0C); // 6x78: Unknown diff --git a/src/HTTPServer.cc b/src/HTTPServer.cc index a8e683a6..059c16fe 100644 --- a/src/HTTPServer.cc +++ b/src/HTTPServer.cc @@ -220,6 +220,7 @@ JSON HTTPServer::generate_quest_json_st(shared_ptr q) { {"Number", q->quest_number}, {"Episode", name_for_episode(q->episode)}, {"Joinable", q->joinable}, + {"LockStatusRegister", (q->lock_status_register >= 0) ? q->lock_status_register : JSON(nullptr)}, {"Name", q->name}, {"BattleRules", std::move(battle_rules_json)}, {"ChallengeTemplateIndex", std::move(challenge_template_index_json)}, diff --git a/src/Quest.cc b/src/Quest.cc index 30d4dba5..4deaec08 100644 --- a/src/Quest.cc +++ b/src/Quest.cc @@ -206,11 +206,13 @@ VersionedQuest::VersionedQuest( ssize_t challenge_template_index, std::shared_ptr available_expression, std::shared_ptr enabled_expression, - bool force_joinable) + bool force_joinable, + int16_t lock_status_register) : quest_number(quest_number), category_id(category_id), episode(Episode::NONE), - joinable(false), + joinable(force_joinable), + lock_status_register(lock_status_register), version(version), language(language), is_dlq_encoded(false), @@ -234,7 +236,6 @@ VersionedQuest::VersionedQuest( throw invalid_argument("file is too small for header"); } auto* header = reinterpret_cast(bin_decompressed.data()); - this->joinable = force_joinable; this->episode = Episode::EP1; if (this->quest_number == 0xFFFFFFFF) { this->quest_number = fnv1a32(header, sizeof(header)) & 0xFFFF; @@ -250,7 +251,6 @@ VersionedQuest::VersionedQuest( throw invalid_argument("file is too small for header"); } auto* header = reinterpret_cast(bin_decompressed.data()); - this->joinable = force_joinable; this->episode = Episode::EP1; if (this->quest_number == 0xFFFFFFFF) { this->quest_number = header->quest_number; @@ -267,7 +267,6 @@ VersionedQuest::VersionedQuest( throw invalid_argument("file is too small for header"); } auto* header = reinterpret_cast(bin_decompressed.data()); - this->joinable = force_joinable; this->episode = Episode::EP1; if (this->quest_number == 0xFFFFFFFF) { this->quest_number = header->quest_number; @@ -290,7 +289,6 @@ VersionedQuest::VersionedQuest( throw invalid_argument("file is incorrect size"); } auto* map = reinterpret_cast(bin_decompressed.data()); - this->joinable = force_joinable; this->episode = Episode::EP3; if (this->quest_number == 0xFFFFFFFF) { this->quest_number = map->map_number; @@ -308,7 +306,6 @@ VersionedQuest::VersionedQuest( throw invalid_argument("file is too small for header"); } auto* header = reinterpret_cast(bin_decompressed.data()); - this->joinable = force_joinable; this->episode = find_quest_episode_from_script(bin_decompressed.data(), bin_decompressed.size(), this->version); if (this->quest_number == 0xFFFFFFFF) { this->quest_number = header->quest_number; @@ -324,7 +321,7 @@ VersionedQuest::VersionedQuest( throw invalid_argument("file is too small for header"); } auto* header = reinterpret_cast(bin_decompressed.data()); - this->joinable = header->joinable || force_joinable; + this->joinable |= header->joinable; this->episode = find_quest_episode_from_script(bin_decompressed.data(), bin_decompressed.size(), this->version); if (this->quest_number == 0xFFFFFFFF) { this->quest_number = header->quest_number; @@ -388,6 +385,7 @@ Quest::Quest(shared_ptr initial_version) category_id(initial_version->category_id), episode(initial_version->episode), joinable(initial_version->joinable), + lock_status_register(initial_version->lock_status_register), name(initial_version->name), battle_rules(initial_version->battle_rules), challenge_template_index(initial_version->challenge_template_index), @@ -413,6 +411,9 @@ void Quest::add_version(shared_ptr vq) { if (this->joinable != vq->joinable) { throw runtime_error("quest version has a different joinability state"); } + if (this->lock_status_register != vq->lock_status_register) { + throw runtime_error("quest version has a different lock status register"); + } if (!this->battle_rules != !vq->battle_rules) { throw runtime_error("quest version has a different battle rules presence state"); } @@ -676,6 +677,7 @@ QuestIndex::QuestIndex( shared_ptr available_expression; shared_ptr enabled_expression; bool force_joinable = false; + int16_t lock_status_register = -1; try { json_filedata = &json_files.at(basename); } catch (const out_of_range&) { @@ -710,6 +712,10 @@ QuestIndex::QuestIndex( force_joinable = metadata_json.get_bool("Joinable"); } catch (const out_of_range&) { } + try { + lock_status_register = metadata_json.get_bool("LockStatusRegister"); + } catch (const out_of_range&) { + } } auto vq = make_shared( @@ -724,7 +730,8 @@ QuestIndex::QuestIndex( challenge_template_index, available_expression, enabled_expression, - force_joinable); + force_joinable, + lock_status_register); auto category_name = this->category_index->at(vq->category_id)->name; string filenames_str = bin_filedata->filename; diff --git a/src/Quest.hh b/src/Quest.hh index 724711af..00d6296d 100644 --- a/src/Quest.hh +++ b/src/Quest.hh @@ -64,6 +64,7 @@ struct VersionedQuest { uint32_t category_id; Episode episode; bool joinable; + int16_t lock_status_register; std::string name; Version version; uint8_t language; @@ -91,7 +92,8 @@ struct VersionedQuest { ssize_t challenge_template_index = -1, std::shared_ptr available_expression = nullptr, std::shared_ptr enabled_expression = nullptr, - bool force_joinable = false); + bool force_joinable = false, + int16_t lock_status_register = -1); std::string bin_filename() const; std::string dat_filename() const; @@ -122,6 +124,7 @@ public: uint32_t category_id; Episode episode; bool joinable; + int16_t lock_status_register; std::string name; std::shared_ptr battle_rules; ssize_t challenge_template_index; diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index bd59f469..aec37375 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -2803,6 +2803,36 @@ static void on_set_quest_flag(shared_ptr c, uint8_t command, uint8_t fla } } +static void on_sync_quest_register(shared_ptr c, uint8_t command, uint8_t flag, void* data, size_t size) { + auto l = c->require_lobby(); + if (!l->is_game()) { + return; + } + + const auto& cmd = check_size_t(data, size); + if (cmd.register_number >= 0x100) { + throw runtime_error("invalid register number"); + } + + // If the lock status register is being written, change the game's flags to + // allow or forbid joining + if (l->quest && + l->quest->joinable && + (l->quest->lock_status_register >= 0) && + (cmd.register_number == l->quest->lock_status_register)) { + // Lock if value is nonzero; unlock if value is zero + if (cmd.value.as_int) { + l->set_flag(Lobby::Flag::QUEST_IN_PROGRESS); + l->clear_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS); + } else { + l->clear_flag(Lobby::Flag::QUEST_IN_PROGRESS); + l->set_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS); + } + } + + forward_subcommand(c, command, flag, data, size); +} + static void on_set_entity_set_flag(shared_ptr c, uint8_t command, uint8_t flag, void* data, size_t size) { auto l = c->require_lobby(); if (!l->is_game()) { @@ -4480,7 +4510,7 @@ const SubcommandDefinition subcommand_definitions[0x100] = { /* 6x74 */ {0x62, 0x69, 0x74, on_word_select, SDF::ALWAYS_FORWARD_TO_WATCHERS}, /* 6x75 */ {0x00, 0x00, 0x75, on_set_quest_flag}, /* 6x76 */ {0x00, 0x00, 0x76, on_set_entity_set_flag}, - /* 6x77 */ {0x00, 0x00, 0x77, on_forward_check_game}, + /* 6x77 */ {0x00, 0x00, 0x77, on_sync_quest_register}, /* 6x78 */ {0x00, 0x00, 0x78, forward_subcommand_m}, /* 6x79 */ {0x00, 0x00, 0x79, on_forward_check_lobby}, /* 6x7A */ {0x00, 0x00, 0x7A, on_forward_check_game_client}, diff --git a/system/quests/battle/b88001.json b/system/quests/battle/b88001.json index 0f667cd0..d0c94dc2 100644 --- a/system/quests/battle/b88001.json +++ b/system/quests/battle/b88001.json @@ -61,4 +61,10 @@ // progress. Note that this will likely not work properly unless the quest // script is designed to support joining players. // "Joinable": true, + // If a quest is joinable and this field is specified, then setting this + // register to a nonzero value via the sync_register opcode will cause the + // game to be locked and not allow any further joins. The register may be + // set to to zero to allow joins again. This field is ignored for quests + // that do not have the Joinable otion set above. + // "LockStatusRegister": 249, }