diff --git a/Code/client/Games/Skyrim/Interface/ConsoleCommandGuard.cpp b/Code/client/Games/Skyrim/Interface/ConsoleCommandGuard.cpp new file mode 100644 index 0000000000..986b6ba3b2 --- /dev/null +++ b/Code/client/Games/Skyrim/Interface/ConsoleCommandGuard.cpp @@ -0,0 +1,118 @@ +#include + +#include +#include + +// Refuses typed console commands while connected to a server with Gameplay:bAllowConsoleCommands off. +// Commands run by code (ConsoleUtil, Papyrus ExecuteCommand) don't go through this callback. +// +// 51084 is the Console menu's "ExecuteCommand" callback (registered by 51083). It queues the typed line as a +// Script to be compiled and run later. 51109 is ConsoleLog::Print, 401203 the ConsoleLog singleton. + +namespace +{ +// Commands that stay allowed while typed commands are turned off, by short and full name. They help players +// get past things that block a playthrough without affecting other players. +// +// tgm and tcl stay blocked: tgm makes a player invulnerable in PvP, and tcl lets a player walk through walls +// and breaks enemy pathing. The game already offers fair alternatives: fast travel, teleporting to a party +// member, and the Become Ethereal shout. +constexpr std::string_view kAllowedCommands[]{ + "tfc", "ToggleFreeCamera", + "tm", "ToggleMenus", + "coc", "CenterOnCell", +}; + +bool EqualsIgnoreCase(std::string_view aLhs, std::string_view aRhs) noexcept +{ + return aLhs.size() == aRhs.size() && _strnicmp(aLhs.data(), aRhs.data(), aLhs.size()) == 0; +} + +bool IsSpace(char aChar) noexcept +{ + return aChar == ' ' || aChar == '\t'; +} + +// Only the command name is checked. A reference prefix ("player.tcl") isn't allowed, so the command can't be +// aimed at another reference. +bool IsAllowedCommand(std::string_view aLine) noexcept +{ + if (aLine.find_first_of("\r\n") != std::string_view::npos) + return false; + + while (!aLine.empty() && IsSpace(aLine.front())) + aLine.remove_prefix(1); + + size_t nameLength = 0; + while (nameLength < aLine.size() && !IsSpace(aLine[nameLength])) + ++nameLength; + + const std::string_view cName = aLine.substr(0, nameLength); + for (const std::string_view cAllowed : kAllowedCommands) + { + if (EqualsIgnoreCase(cName, cAllowed)) + return true; + } + + return false; +} + +const char* GetTypedLine(const FxDelegateArgs* apArgs) noexcept +{ + if (!apArgs || !apArgs->pArgs || apArgs->GetArgCount() == 0) + return nullptr; + + const GFxValue& cValue = (*apArgs)[0]; + if (!cValue.IsString() || !cValue.value.pObject) + return nullptr; + + return cValue.GetString(); +} + +void PrintToConsole(const char* acpText) noexcept +{ + using TPrint = void(void*, const char*, ...); + POINTER_SKYRIMSE(TPrint, s_print, 51109); + POINTER_SKYRIMSE(void*, s_consoleLog, 401203); + + void* pConsoleLog = *s_consoleLog.Get(); + if (pConsoleLog) + s_print.Get()(pConsoleLog, "%s", acpText); +} + +bool AreTypedCommandsBlocked() noexcept +{ + World& world = World::Get(); + return world.GetTransport().IsConnected() && !world.GetServerSettings().ConsoleCommandsAllowed; +} +} // namespace + +using TExecuteCommand = void(const FxDelegateArgs* apArgs); +static TExecuteCommand* RealExecuteCommand = nullptr; + +static void HookExecuteCommand(const FxDelegateArgs* apArgs) +{ + if (!AreTypedCommandsBlocked()) + { + RealExecuteCommand(apArgs); + return; + } + + const char* cpLine = GetTypedLine(apArgs); + if (cpLine && IsAllowedCommand(cpLine)) + { + RealExecuteCommand(apArgs); + return; + } + + spdlog::info("Console command refused, the server has turned console commands off: {}", cpLine ? cpLine : ""); + PrintToConsole("Console commands are turned off on this server."); +} + +static TiltedPhoques::Initializer s_consoleCommandGuardHooks( + []() + { + POINTER_SKYRIMSE(TExecuteCommand, s_executeCommand, 51084); + RealExecuteCommand = s_executeCommand.Get(); + TP_HOOK(&RealExecuteCommand, HookExecuteCommand); + }); diff --git a/Code/client/Games/Skyrim/Interface/IMenu.h b/Code/client/Games/Skyrim/Interface/IMenu.h index d472b4f73b..5fa90b767c 100644 --- a/Code/client/Games/Skyrim/Interface/IMenu.h +++ b/Code/client/Games/Skyrim/Interface/IMenu.h @@ -76,9 +76,71 @@ struct GStatGroups }; }; +// Layout from powerof3's CommonLibSSE (RE/G/GFxValue.h). +struct GFxValue +{ + enum ValueType : uint32_t + { + kUndefined = 0x00, + kNull = 0x01, + kBoolean = 0x02, + kNumber = 0x03, + kString = 0x04, + kStringW = 0x05, + kObject = 0x06, + kArray = 0x07, + kDisplayObject = 0x08, + + kManagedBit = 1 << 6, + kConvertBit = 1 << 7, + kValueMask = 0x0F, + kTypeMask = kConvertBit | kValueMask, + }; + + [[nodiscard]] uint32_t GetType() const noexcept { return type & kTypeMask; } + [[nodiscard]] bool IsString() const noexcept { return GetType() == kString; } + [[nodiscard]] const char* GetString() const noexcept { return (type & kManagedBit) ? *value.pManagedString : value.pString; } + + union ValueUnion + { + double number; + bool boolean; + const char* pString; + const char** pManagedString; + const wchar_t* pWideString; + const wchar_t** pManagedWideString; + void* pObject; + }; + static_assert(sizeof(ValueUnion) == 0x8); + + void* pObjectInterface; // 00 + uint32_t type; // 08 + uint32_t pad0C; // 0C + ValueUnion value; // 10 +}; +static_assert(sizeof(GFxValue) == 0x18); + +struct FxDelegateHandler; + +// Layout from powerof3's CommonLibSSE (RE/F/FxDelegateArgs.h). +struct FxDelegateArgs +{ + [[nodiscard]] uint32_t GetArgCount() const noexcept { return numArgs; } + [[nodiscard]] const GFxValue& operator[](uint32_t aIndex) const noexcept { return pArgs[aIndex]; } + + GFxValue responseId; // 00 + FxDelegateHandler* pHandler; // 18 + void* pMovieView; // 20 + const GFxValue* pArgs; // 28 + uint32_t numArgs; // 30 + uint32_t pad34; // 34 +}; +static_assert(offsetof(FxDelegateArgs, pArgs) == 0x28); +static_assert(sizeof(FxDelegateArgs) == 0x38); + struct FxDelegateHandler : GRefCountBase { - using CallbackFn = void(const void*); + using CallbackFn = void(const FxDelegateArgs* apArgs); class CallbackProcessor { diff --git a/Code/encoding/Structs/ServerSettings.cpp b/Code/encoding/Structs/ServerSettings.cpp index bdf8f77adc..8748465b70 100644 --- a/Code/encoding/Structs/ServerSettings.cpp +++ b/Code/encoding/Structs/ServerSettings.cpp @@ -5,7 +5,7 @@ using TiltedPhoques::Serialization; bool ServerSettings::operator==(const ServerSettings& acRhs) const noexcept { - return Difficulty == acRhs.Difficulty && GreetingsEnabled == acRhs.GreetingsEnabled && PvpEnabled == acRhs.PvpEnabled && SyncPlayerHomes == acRhs.SyncPlayerHomes && DeathSystemEnabled == acRhs.DeathSystemEnabled && AutoPartyJoin == acRhs.AutoPartyJoin; + return Difficulty == acRhs.Difficulty && GreetingsEnabled == acRhs.GreetingsEnabled && PvpEnabled == acRhs.PvpEnabled && SyncPlayerHomes == acRhs.SyncPlayerHomes && DeathSystemEnabled == acRhs.DeathSystemEnabled && AutoPartyJoin == acRhs.AutoPartyJoin && ConsoleCommandsAllowed == acRhs.ConsoleCommandsAllowed; } bool ServerSettings::operator!=(const ServerSettings& acRhs) const noexcept @@ -22,6 +22,7 @@ void ServerSettings::Serialize(TiltedPhoques::Buffer::Writer& aWriter) const noe Serialization::WriteBool(aWriter, DeathSystemEnabled); Serialization::WriteBool(aWriter, SyncPlayerCalendar); Serialization::WriteBool(aWriter, AutoPartyJoin); + Serialization::WriteBool(aWriter, ConsoleCommandsAllowed); } void ServerSettings::Deserialize(TiltedPhoques::Buffer::Reader& aReader) noexcept @@ -33,4 +34,5 @@ void ServerSettings::Deserialize(TiltedPhoques::Buffer::Reader& aReader) noexcep DeathSystemEnabled = Serialization::ReadBool(aReader); SyncPlayerCalendar = Serialization::ReadBool(aReader); AutoPartyJoin = Serialization::ReadBool(aReader); + ConsoleCommandsAllowed = Serialization::ReadBool(aReader); } diff --git a/Code/encoding/Structs/ServerSettings.h b/Code/encoding/Structs/ServerSettings.h index 02a49302fe..90bc5a8540 100644 --- a/Code/encoding/Structs/ServerSettings.h +++ b/Code/encoding/Structs/ServerSettings.h @@ -17,4 +17,5 @@ struct ServerSettings bool DeathSystemEnabled{}; bool SyncPlayerCalendar{}; bool AutoPartyJoin{}; + bool ConsoleCommandsAllowed{true}; }; diff --git a/Code/server/GameServer.cpp b/Code/server/GameServer.cpp index de5e917934..35b57805b0 100644 --- a/Code/server/GameServer.cpp +++ b/Code/server/GameServer.cpp @@ -41,6 +41,7 @@ Console::Setting bEnableDeathSystem{"Gameplay:bEnableDeathSystem", "Enables the Console::Setting uTimeScale{"Gameplay:uTimeScale", "How many seconds pass ingame for every real second (0 to 1000). Changing this can make the game unstable", 20u}; Console::Setting bSyncPlayerCalendar{"Gameplay:bSyncPlayerCalendar", "Syncs up all player calendars to be the same day, month, and year. This uses the date of the player with the furthest ahead date at connection.", false}; Console::Setting bAutoPartyJoin{"Gameplay:bAutoPartyJoin", "Join parties automatically, as long as there is only one party in the server", true}; +Console::Setting bAllowConsoleCommands{"Gameplay:bAllowConsoleCommands", "Lets players run console commands they type while connected. Turn it off to stop console trolling such as spawning enemies or killing NPCs. The console still opens for reading ids, and mods can still run commands", true}; // ModPolicy Stuff Console::Setting bEnableModCheck{"ModPolicy:bEnableModCheck", "Refuse clients whose mods do not match Data/loadorder.txt", false, Console::SettingsFlags::kLocked}; Console::Setting bAllowSKSE{"ModPolicy:bAllowSKSE", "Allow players using SKSE to join. Applies even when bEnableModCheck is off", true, Console::SettingsFlags::kLocked}; @@ -64,6 +65,15 @@ Console::Command<> TogglePvp( GameServer::Get()->UpdateSettings(); }); +Console::Command<> ToggleConsoleCommands( + "ToggleConsoleCommands", "Toggle whether players can run console commands they type", + [](Console::ArgStack&) + { + bAllowConsoleCommands = !bAllowConsoleCommands; + spdlog::get("ConOut")->info("Typed console commands have been {}.", bAllowConsoleCommands == true ? "allowed" : "turned off"); + GameServer::Get()->UpdateSettings(); + }); + Console::Command SetDifficulty( "SetDifficulty", "Set server difficulty (0 being Novice and 5 being Legendary; default is 4)", [](Console::ArgStack& aStack) @@ -141,6 +151,7 @@ ServerSettings GetSettings() settings.DeathSystemEnabled = bEnableDeathSystem; settings.SyncPlayerCalendar = bSyncPlayerCalendar; settings.AutoPartyJoin = bAutoPartyJoin; + settings.ConsoleCommandsAllowed = bAllowConsoleCommands; return settings; }