Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions Code/client/Games/Skyrim/Interface/ConsoleCommandGuard.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#include <TiltedOnlinePCH.h>

#include <World.h>
#include <Interface/IMenu.h>

// 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 : "<none>");
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);
});
64 changes: 63 additions & 1 deletion Code/client/Games/Skyrim/Interface/IMenu.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<FxDelegateHandler, GStatGroups::kGStat_Default_Mem>
{
using CallbackFn = void(const void*);
using CallbackFn = void(const FxDelegateArgs* apArgs);

class CallbackProcessor
{
Expand Down
4 changes: 3 additions & 1 deletion Code/encoding/Structs/ServerSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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);
}
1 change: 1 addition & 0 deletions Code/encoding/Structs/ServerSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ struct ServerSettings
bool DeathSystemEnabled{};
bool SyncPlayerCalendar{};
bool AutoPartyJoin{};
bool ConsoleCommandsAllowed{true};
};
11 changes: 11 additions & 0 deletions Code/server/GameServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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<int64_t> SetDifficulty(
"SetDifficulty", "Set server difficulty (0 being Novice and 5 being Legendary; default is 4)",
[](Console::ArgStack& aStack)
Expand Down Expand Up @@ -141,6 +151,7 @@ ServerSettings GetSettings()
settings.DeathSystemEnabled = bEnableDeathSystem;
settings.SyncPlayerCalendar = bSyncPlayerCalendar;
settings.AutoPartyJoin = bAutoPartyJoin;
settings.ConsoleCommandsAllowed = bAllowConsoleCommands;
return settings;
}

Expand Down