From 21f1c40408d900c7c51fe861b19a22d4460dbed6 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Thu, 17 Oct 2024 21:19:25 -0700 Subject: [PATCH] allow specifying what counts as cheating; closes #572 --- src/ChatCommands.cc | 88 +++++++++++++++++++++----------------- src/ServerState.cc | 27 ++++++++++++ src/ServerState.hh | 24 +++++++++++ system/config.example.json | 19 ++++++++ tests/config.json | 15 +++++++ 5 files changed, 134 insertions(+), 39 deletions(-) diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index 8f15b72a..159bb919 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -71,22 +71,25 @@ static void check_debug_enabled(shared_ptr c) { } } -static void check_cheats_enabled(shared_ptr l, shared_ptr c) { - if (!l->check_flag(Lobby::Flag::CHEATS_ENABLED) && +static void check_cheats_enabled(shared_ptr l, shared_ptr c, bool behavior_is_cheating) { + if (behavior_is_cheating && + !l->check_flag(Lobby::Flag::CHEATS_ENABLED) && !c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE)) { throw precondition_failed("$C6This command can\nonly be used in\ncheat mode."); } } -static void check_cheats_allowed(shared_ptr s, shared_ptr c) { - if ((s->cheat_mode_behavior == ServerState::BehaviorSwitch::OFF) && +static void check_cheats_allowed(shared_ptr s, shared_ptr c, bool behavior_is_cheating) { + if (behavior_is_cheating && + (s->cheat_mode_behavior == ServerState::BehaviorSwitch::OFF) && !c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE)) { throw precondition_failed("$C6Cheats are disabled\non this server."); } } -static void check_cheats_allowed(shared_ptr s, shared_ptr ses) { - if ((s->cheat_mode_behavior == ServerState::BehaviorSwitch::OFF) && +static void check_cheats_allowed(shared_ptr s, shared_ptr ses, bool behavior_is_cheating) { + if (behavior_is_cheating && + (s->cheat_mode_behavior == ServerState::BehaviorSwitch::OFF) && (!ses->login || !ses->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE))) { throw precondition_failed("$C6Cheats are disabled\non this proxy."); } @@ -972,7 +975,9 @@ static void server_command_cheat(shared_ptr c, const std::string&) { l->toggle_flag(Lobby::Flag::CHEATS_ENABLED); send_text_message_printf(l, "Cheat mode %s", l->check_flag(Lobby::Flag::CHEATS_ENABLED) ? "enabled" : "disabled"); - if (!l->check_flag(Lobby::Flag::CHEATS_ENABLED) && !c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE)) { + if (!l->check_flag(Lobby::Flag::CHEATS_ENABLED) && + !c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE) && + s->cheat_flags.insufficient_minimum_level) { size_t default_min_level = s->default_min_level_for_game(l->base_version, l->episode, l->difficulty); if (l->min_level < default_min_level) { l->min_level = default_min_level; @@ -1147,7 +1152,8 @@ static void server_command_meseta(shared_ptr c, const std::string& args) static void server_command_secid(shared_ptr c, const std::string& args) { auto l = c->require_lobby(); - check_cheats_allowed(c->require_server_state(), c); + auto s = c->require_server_state(); + check_cheats_allowed(s, c, s->cheat_flags.override_section_id); uint8_t new_override_section_id; @@ -1172,7 +1178,9 @@ static void server_command_secid(shared_ptr c, const std::string& args) } static void proxy_command_secid(shared_ptr ses, const std::string& args) { - check_cheats_allowed(ses->require_server_state(), ses); + auto s = ses->require_server_state(); + check_cheats_allowed(s, ses, s->cheat_flags.override_section_id); + if (!args[0]) { ses->config.override_section_id = 0xFF; send_text_message(ses->client_channel, "$C6Override section ID\nremoved"); @@ -1194,7 +1202,7 @@ static void server_command_variations(shared_ptr c, const std::string& a auto s = c->require_server_state(); auto l = c->require_lobby(); check_is_game(l, false); - check_cheats_allowed(s, c); + check_cheats_allowed(s, c, s->cheat_flags.override_variations); c->override_variations = make_unique>(); c->override_variations->clear(0); @@ -1207,7 +1215,7 @@ static void server_command_rand(shared_ptr c, const std::string& args) { auto s = c->require_server_state(); auto l = c->require_lobby(); check_is_game(l, false); - check_cheats_allowed(s, c); + check_cheats_allowed(s, c, s->cheat_flags.override_random_seed); if (!args[0]) { c->config.clear_flag(Client::Flag::USE_OVERRIDE_RANDOM_SEED); @@ -1221,7 +1229,8 @@ static void server_command_rand(shared_ptr c, const std::string& args) { } static void proxy_command_rand(shared_ptr ses, const std::string& args) { - check_cheats_allowed(ses->require_server_state(), ses); + auto s = ses->require_server_state(); + check_cheats_allowed(s, ses, s->cheat_flags.override_random_seed); if (!args[0]) { ses->config.clear_flag(Client::Flag::USE_OVERRIDE_RANDOM_SEED); ses->config.override_random_seed = 0; @@ -1292,7 +1301,7 @@ static void server_command_min_level(shared_ptr c, const std::string& ar auto s = c->require_server_state(); bool cheats_allowed = (l->check_flag(Lobby::Flag::CHEATS_ENABLED) || c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE)); - if (!cheats_allowed) { + if (!cheats_allowed && s->cheat_flags.insufficient_minimum_level) { size_t default_min_level = s->default_min_level_for_game(l->base_version, l->episode, l->difficulty); if (new_min_level < default_min_level) { send_text_message_printf(c, "$C6Cannot set minimum\nlevel below %zu", default_min_level + 1); @@ -1342,28 +1351,28 @@ static void server_command_edit(shared_ptr c, const std::string& args) { try { auto p = c->character(); - if (tokens.at(0) == "atp" && cheats_allowed) { + if (tokens.at(0) == "atp" && (cheats_allowed || !s->cheat_flags.edit_stats)) { p->disp.stats.char_stats.atp = stoul(tokens.at(1)); - } else if (tokens.at(0) == "mst" && cheats_allowed) { + } else if (tokens.at(0) == "mst" && (cheats_allowed || !s->cheat_flags.edit_stats)) { p->disp.stats.char_stats.mst = stoul(tokens.at(1)); - } else if (tokens.at(0) == "evp" && cheats_allowed) { + } else if (tokens.at(0) == "evp" && (cheats_allowed || !s->cheat_flags.edit_stats)) { p->disp.stats.char_stats.evp = stoul(tokens.at(1)); - } else if (tokens.at(0) == "hp" && cheats_allowed) { + } else if (tokens.at(0) == "hp" && (cheats_allowed || !s->cheat_flags.edit_stats)) { p->disp.stats.char_stats.hp = stoul(tokens.at(1)); - } else if (tokens.at(0) == "dfp" && cheats_allowed) { + } else if (tokens.at(0) == "dfp" && (cheats_allowed || !s->cheat_flags.edit_stats)) { p->disp.stats.char_stats.dfp = stoul(tokens.at(1)); - } else if (tokens.at(0) == "ata" && cheats_allowed) { + } else if (tokens.at(0) == "ata" && (cheats_allowed || !s->cheat_flags.edit_stats)) { p->disp.stats.char_stats.ata = stoul(tokens.at(1)); - } else if (tokens.at(0) == "lck" && cheats_allowed) { + } else if (tokens.at(0) == "lck" && (cheats_allowed || !s->cheat_flags.edit_stats)) { p->disp.stats.char_stats.lck = stoul(tokens.at(1)); - } else if (tokens.at(0) == "meseta" && cheats_allowed) { + } else if (tokens.at(0) == "meseta" && (cheats_allowed || !s->cheat_flags.edit_stats)) { p->disp.stats.meseta = stoul(tokens.at(1)); - } else if (tokens.at(0) == "exp" && cheats_allowed) { + } else if (tokens.at(0) == "exp" && (cheats_allowed || !s->cheat_flags.edit_stats)) { p->disp.stats.experience = stoul(tokens.at(1)); - } else if (tokens.at(0) == "level" && cheats_allowed) { + } else if (tokens.at(0) == "level" && (cheats_allowed || !s->cheat_flags.edit_stats)) { p->disp.stats.level = stoul(tokens.at(1)) - 1; p->recompute_stats(s->level_table(c->version())); - } else if (((tokens.at(0) == "material") || (tokens.at(0) == "mat")) && !is_v1_or_v2(c->version())) { + } else if (((tokens.at(0) == "material") || (tokens.at(0) == "mat")) && !is_v1_or_v2(c->version()) && (cheats_allowed || !s->cheat_flags.reset_materials)) { if (tokens.at(1) == "reset") { const auto& which = tokens.at(2); if (which == "power") { @@ -1420,7 +1429,7 @@ static void server_command_edit(shared_ptr c, const std::string& args) { sys->language = new_language; } } else if (tokens.at(0) == "secid") { - if (!cheats_allowed && (p->disp.stats.level > 0)) { + if (!cheats_allowed && (p->disp.stats.level > 0) && s->cheat_flags.edit_section_id) { send_text_message(c, "$C6You cannot change\nyour Section ID\nafter level 1"); return; } @@ -1482,7 +1491,7 @@ static void server_command_edit(shared_ptr c, const std::string& args) { p->disp.visual.extra_model = npc; p->disp.visual.validation_flags |= 0x02; } - } else if (tokens.at(0) == "tech" && cheats_allowed) { + } else if (tokens.at(0) == "tech" && (cheats_allowed || !s->cheat_flags.edit_stats)) { uint8_t level = stoul(tokens.at(2)) - 1; if (tokens.at(1) == "all") { for (size_t x = 0; x < 0x14; x++) { @@ -1853,7 +1862,7 @@ static void server_command_warp(shared_ptr c, const std::string& args, b auto s = c->require_server_state(); auto l = c->require_lobby(); check_is_game(l, true); - check_cheats_enabled(l, c); + check_cheats_enabled(l, c, s->cheat_flags.warp); uint32_t floor = stoul(args, nullptr, 0); if (!is_warpall && (c->floor == floor)) { @@ -1885,7 +1894,7 @@ static void server_command_warpall(shared_ptr c, const std::string& args static void proxy_command_warp(shared_ptr ses, const std::string& args, bool is_warpall) { auto s = ses->require_server_state(); - check_cheats_allowed(s, ses); + check_cheats_allowed(s, ses, s->cheat_flags.warp); if (!ses->is_in_game) { send_text_message(ses->client_channel, "$C6You must be in a\ngame to use this\ncommand"); return; @@ -1910,7 +1919,7 @@ static void server_command_next(shared_ptr c, const std::string&) { auto s = c->require_server_state(); auto l = c->require_lobby(); check_is_game(l, true); - check_cheats_enabled(l, c); + check_cheats_enabled(l, c, s->cheat_flags.warp); size_t limit = floor_limit_for_episode(l->episode); if (limit == 0) { @@ -1921,7 +1930,7 @@ static void server_command_next(shared_ptr c, const std::string&) { static void proxy_command_next(shared_ptr ses, const std::string&) { auto s = ses->require_server_state(); - check_cheats_allowed(s, ses); + check_cheats_allowed(s, ses, s->cheat_flags.warp); if (!ses->is_in_game) { send_text_message(ses->client_channel, "$C6You must be in a\ngame to use this\ncommand"); return; @@ -2022,7 +2031,7 @@ static void server_command_infinite_hp(shared_ptr c, const std::string&) auto s = c->require_server_state(); auto l = c->require_lobby(); check_is_game(l, true); - check_cheats_enabled(l, c); + check_cheats_enabled(l, c, s->cheat_flags.infinite_hp_tp); c->config.toggle_flag(Client::Flag::INFINITE_HP_ENABLED); bool enabled = c->config.check_flag(Client::Flag::INFINITE_HP_ENABLED); @@ -2034,7 +2043,7 @@ static void server_command_infinite_hp(shared_ptr c, const std::string&) static void proxy_command_infinite_hp(shared_ptr ses, const std::string&) { auto s = ses->require_server_state(); - check_cheats_allowed(s, ses); + check_cheats_allowed(s, ses, s->cheat_flags.infinite_hp_tp); ses->config.toggle_flag(Client::Flag::INFINITE_HP_ENABLED); bool enabled = ses->config.check_flag(Client::Flag::INFINITE_HP_ENABLED); send_text_message_printf(ses->client_channel, "$C6Infinite HP %s", enabled ? "enabled" : "disabled"); @@ -2048,7 +2057,7 @@ static void server_command_infinite_tp(shared_ptr c, const std::string&) auto s = c->require_server_state(); auto l = c->require_lobby(); check_is_game(l, true); - check_cheats_enabled(l, c); + check_cheats_enabled(l, c, s->cheat_flags.infinite_hp_tp); c->config.toggle_flag(Client::Flag::INFINITE_TP_ENABLED); send_text_message_printf(c, "$C6Infinite TP %s", c->config.check_flag(Client::Flag::INFINITE_TP_ENABLED) ? "enabled" : "disabled"); @@ -2056,7 +2065,7 @@ static void server_command_infinite_tp(shared_ptr c, const std::string&) static void proxy_command_infinite_tp(shared_ptr ses, const std::string&) { auto s = ses->require_server_state(); - check_cheats_allowed(s, ses); + check_cheats_allowed(s, ses, s->cheat_flags.infinite_hp_tp); ses->config.toggle_flag(Client::Flag::INFINITE_TP_ENABLED); send_text_message_printf(ses->client_channel, "$C6Infinite TP %s", ses->config.check_flag(Client::Flag::INFINITE_TP_ENABLED) ? "enabled" : "disabled"); @@ -2146,7 +2155,8 @@ static void server_command_dropmode(shared_ptr c, const std::string& arg } static void proxy_command_dropmode(shared_ptr ses, const std::string& args) { - check_cheats_allowed(ses->require_server_state(), ses); + auto s = ses->require_server_state(); + check_cheats_allowed(ses->require_server_state(), ses, s->cheat_flags.proxy_override_drops); using DropMode = ProxyServer::LinkedSession::DropMode; if (args.empty()) { @@ -2194,7 +2204,7 @@ static void server_command_item(shared_ptr c, const std::string& args) { auto s = c->require_server_state(); auto l = c->require_lobby(); check_is_game(l, true); - check_cheats_enabled(l, c); + check_cheats_enabled(l, c, s->cheat_flags.create_items); ItemData item = s->parse_item_description(c->version(), args); item.id = l->generate_item_id(c->lobby_client_id); @@ -2213,7 +2223,7 @@ static void server_command_item(shared_ptr c, const std::string& args) { static void proxy_command_item(shared_ptr ses, const std::string& args) { auto s = ses->require_server_state(); - check_cheats_allowed(s, ses); + check_cheats_allowed(s, ses, s->cheat_flags.create_items); if (ses->version() == Version::BB_V4) { send_text_message(ses->client_channel, "$C6This command cannot\nbe used on the proxy\nserver in BB games"); return; @@ -2379,7 +2389,7 @@ static void server_command_ep3_replace_assist_card(shared_ptr c, const s auto l = c->require_lobby(); check_is_game(l, true); check_is_ep3(c, true); - check_cheats_enabled(l, c); + check_cheats_enabled(l, c, s->cheat_flags.ep3_replace_assist); if (l->episode != Episode::EP3) { throw logic_error("non-Ep3 client in Ep3 game"); @@ -2431,7 +2441,7 @@ static void server_command_ep3_unset_field_character(shared_ptr c, const auto l = c->require_lobby(); check_is_game(l, true); check_is_ep3(c, true); - check_cheats_enabled(l, c); + check_cheats_enabled(l, c, s->cheat_flags.ep3_unset_field_character); if (l->episode != Episode::EP3) { throw logic_error("non-Ep3 client in Ep3 game"); diff --git a/src/ServerState.cc b/src/ServerState.cc index 39442aa8..daa615a8 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -19,6 +19,27 @@ using namespace std; +CheatFlags::CheatFlags(const phosg::JSON& json) : CheatFlags() { + unordered_set enabled_keys; + for (const auto& it : json.as_list()) { + enabled_keys.emplace(it->as_string()); + } + + this->create_items = enabled_keys.count("CreateItems"); + this->edit_section_id = enabled_keys.count("EditSectionID"); + this->edit_stats = enabled_keys.count("EditStats"); + this->ep3_replace_assist = enabled_keys.count("Ep3ReplaceAssist"); + this->ep3_unset_field_character = enabled_keys.count("Ep3UnsetFieldCharacter"); + this->infinite_hp_tp = enabled_keys.count("InfiniteHPTP"); + this->insufficient_minimum_level = enabled_keys.count("InsufficientMinimumLevel"); + this->override_random_seed = enabled_keys.count("OverrideRandomSeed"); + this->override_section_id = enabled_keys.count("OverrideSectionID"); + this->override_variations = enabled_keys.count("OverrideVariations"); + this->proxy_override_drops = enabled_keys.count("ProxyOverrideDrops"); + this->reset_materials = enabled_keys.count("ResetMaterials"); + this->warp = enabled_keys.count("Warp"); +} + ServerState::QuestF960Result::QuestF960Result(const phosg::JSON& json, shared_ptr name_index) { static const array day_names = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; this->meseta_cost = json.get_int("MesetaCost", 0); @@ -1207,6 +1228,12 @@ void ServerState::load_config_early() { } catch (const out_of_range&) { } + try { + this->cheat_flags = CheatFlags(this->config_json->at("CheatingBehaviors")); + } catch (const out_of_range&) { + this->cheat_flags = CheatFlags(); + } + this->update_dependent_server_configs(); } diff --git a/src/ServerState.hh b/src/ServerState.hh index 5b3ff91c..d7240551 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -46,6 +46,29 @@ struct PortConfiguration { ServerBehavior behavior; }; +struct CheatFlags { + // This structure describes which behaviors are considered cheating (that is, + // require cheat mode to be enabled or the user to have the CHEAT_ANYWHERE + // account flag). A false value here means that that particular behavior is + // NOT cheating, so cheat mode is NOT required. + bool create_items = true; + bool edit_section_id = true; + bool edit_stats = true; + bool ep3_replace_assist = true; + bool ep3_unset_field_character = true; + bool infinite_hp_tp = true; + bool insufficient_minimum_level = true; + bool override_random_seed = true; + bool override_section_id = true; + bool override_variations = true; + bool proxy_override_drops = true; + bool reset_materials = false; + bool warp = true; + + CheatFlags() = default; + explicit CheatFlags(const phosg::JSON& json); +}; + struct ServerState : public std::enable_shared_from_this { enum class RunShellBehavior { DEFAULT = 0, @@ -191,6 +214,7 @@ struct ServerState : public std::enable_shared_from_this { std::shared_ptr rare_enemy_rates_challenge; std::array, 3> min_levels_v4; // Indexed as [episode][difficulty] std::vector bb_required_patches; + CheatFlags cheat_flags; struct QuestF960Result { uint32_t meseta_cost = 0; diff --git a/system/config.example.json b/system/config.example.json index 0b0ba1ea..399e7b0b 100644 --- a/system/config.example.json +++ b/system/config.example.json @@ -1009,6 +1009,25 @@ // available on the proxy server. "CheatModeBehavior": "OnByDefault", + // Cheat mode behaviors. The keys present in this list determine what the + // server considers to be cheating. If you delete or comment out an item + // here, the server will allow that action even when cheat mode is off. + "CheatingBehaviors": [ + "CreateItems", // Use of the $item command + "EditSectionID", // Use of $edit secid + "EditStats", // Use of $edit atp, etc. + "Ep3ReplaceAssist", // Use of $setassist + "Ep3UnsetFieldCharacter", // Use of $unset + "InfiniteHPTP", // Use of $infhp and $inftp + "InsufficientMinimumLevel", // Setting a $minlevel below the default + "OverrideRandomSeed", // Use of $rand + "OverrideSectionID", // Use of $secid + "OverrideVariations", // Use of $variations + "ProxyOverrideDrops", // Use of $dropmode on proxy server + // "ResetMaterials", // Use of $edit mat reset (by default, not cheating) + "Warp", // Use of $warp + ], + // Default switch assist behavior. Players can always toggle switch assist // with the $swa command; this only controls whether it's enabled by default // for all players or not. diff --git a/tests/config.json b/tests/config.json index 6a3c5c5a..0027690e 100644 --- a/tests/config.json +++ b/tests/config.json @@ -30,6 +30,21 @@ "DefaultDropModeV4Battle": "SERVER_SHARED", "DefaultDropModeV4Challenge": "SERVER_SHARED", "CheatModeBehavior": "OnByDefault", + "CheatingBehaviors": [ + "CreateItems", + "EditSectionID", + "EditStats", + "Ep3ReplaceAssist", + "Ep3UnsetFieldCharacter", + "InfiniteHPTP", + "InsufficientMinimumLevel", + "OverrideRandomSeed", + "OverrideSectionID", + "OverrideVariations", + "ProxyOverrideDrops", + // "ResetMaterials", + "Warp", + ], "EnableSwitchAssistByDefault": false, "RareNotificationsEnabledByDefault": false, "NotifyGameForItemPrimaryIdentifiersV1V2": [],