RustDedicated/Facepunch.Console/ConsoleSystem.cs
2025-11-07 15:19:56 +10:30

1246 lines
25 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Facepunch;
using Facepunch.Extend;
using Network;
using Newtonsoft.Json;
using UnityEngine;
public class ConsoleSystem
{
public class Arg
{
public Option Option;
public Command cmd;
public string RawCommand;
public string FullString = "";
public string[] Args;
public bool Invalid = true;
public string Reply = "";
public bool Silent;
public bool IsClientside => Option.IsClient;
public bool IsServerside => Option.IsServer;
public Connection Connection => Option.Connection;
public bool IsConnectionAdmin
{
get
{
if (Option.Connection != null && Option.Connection.connected && Option.Connection.authLevel != 0)
{
return Option.Connection.trusted;
}
return false;
}
}
public bool IsAdmin
{
get
{
if (!IsConnectionAdmin)
{
return IsRcon;
}
return true;
}
}
public bool IsRcon => Option.FromRcon;
internal Arg(Option options, string rconCommand)
{
Option = options;
BuildCommand(rconCommand);
}
internal void BuildCommand(string command)
{
RawCommand = command;
if (string.IsNullOrEmpty(command))
{
Invalid = true;
return;
}
if (command.IndexOf('.') <= 0 || command.IndexOf(' ', 0, command.IndexOf('.')) != -1)
{
command = "global." + command;
}
int num = command.IndexOf('.');
if (num <= 0)
{
return;
}
string text = command.Substring(0, num);
if (text.Length < 1)
{
return;
}
text = text.Trim().ToLower();
string text2 = command.Substring(num + 1);
if (text2.Length >= 1)
{
int num2 = text2.IndexOf(' ');
if (num2 > 0)
{
FullString = text2.Substring(num2 + 1);
FullString = FullString.Trim();
Args = FullString.SplitQuotesStrings(16);
text2 = text2.Substring(0, num2);
}
text2 = text2.Trim().ToLower();
if (cmd == null && Option.IsClient)
{
cmd = Index.Client.Find(text + "." + text2);
}
if (cmd == null && Option.IsServer)
{
cmd = Index.Server.Find(text + "." + text2);
}
Invalid = cmd == null;
}
}
internal bool HasPermission()
{
if (cmd == null)
{
return false;
}
if (Option.IsUnrestricted)
{
return true;
}
if (IsClientside)
{
if (cmd.ClientAdmin)
{
if (ClientCanRunAdminCommands != null)
{
return ClientCanRunAdminCommands();
}
return false;
}
if (Option.IsFromServer && !cmd.AllowRunFromServer)
{
Debug.Log("Server tried to run command \"" + FullString + "\", but we blocked it.");
return false;
}
return cmd.Client;
}
if (cmd.ServerAdmin)
{
if (cmd.RconOnly && (Connection != null || Option.FromCommandBlock))
{
return false;
}
if (IsRcon)
{
return true;
}
if (IsAdmin)
{
return true;
}
}
if (cmd.ServerUser && Connection != null)
{
return true;
}
return false;
}
internal bool CanSeeInFind(Command command)
{
if (command == null)
{
return false;
}
if (Option.IsUnrestricted)
{
return true;
}
if (command.RconOnly && (Connection != null || Option.FromCommandBlock))
{
return false;
}
if (IsClientside)
{
return command.Client;
}
if (IsServerside)
{
return command.Server;
}
return false;
}
public void ReplyWith(string strValue)
{
Reply = strValue;
}
public void ReplyWith(object obj)
{
Reply = JsonConvert.SerializeObject(obj, Formatting.Indented);
}
public bool HasArgs(int iMinimum = 1)
{
if (Args == null)
{
return false;
}
return Args.Length >= iMinimum;
}
public bool HasArg(string value, bool remove = false)
{
if (Args == null)
{
return false;
}
if (Array.IndexOf(Args, value) == -1)
{
return false;
}
if (remove)
{
Args = Args.Where((string x) => x != value).ToArray();
}
return true;
}
public bool TryRemoveKeyBindEventArgs()
{
if (Args == null)
{
return false;
}
int num = Args.Length;
Args = Args.Where((string x) => x != "True" && x != "False").ToArray();
return Args.Length != num;
}
public string GetString(int iArg, string def = "")
{
if (HasArgs(iArg + 1))
{
return Args[iArg];
}
return def;
}
public int GetInt(int iArg, int def = 0)
{
string text = GetString(iArg, null);
if (text == null)
{
return def;
}
if (int.TryParse(text, out var result))
{
return result;
}
return def;
}
public long GetLong(int iArg, long def = 0L)
{
string text = GetString(iArg, null);
if (text == null)
{
return def;
}
if (long.TryParse(text, out var result))
{
return result;
}
return def;
}
public ulong GetULong(int iArg, ulong def = 0uL)
{
string text = GetString(iArg, null);
if (text == null)
{
return def;
}
if (ulong.TryParse(text, out var result))
{
return result;
}
return def;
}
public bool TryGetUInt(int iArg, out uint value)
{
string text = GetString(iArg, null);
if (text == null)
{
value = 0u;
return false;
}
return uint.TryParse(text, out value);
}
public uint GetUInt(int iArg, uint def = 0u)
{
if (!TryGetUInt(iArg, out var value))
{
return def;
}
return value;
}
public ulong GetUInt64(int iArg, ulong def = 0uL)
{
string text = GetString(iArg, null);
if (text == null)
{
return def;
}
if (ulong.TryParse(text, out var result))
{
return result;
}
return def;
}
public float GetFloat(int iArg, float def = 0f)
{
string text = GetString(iArg, null);
if (text == null)
{
return def;
}
if (float.TryParse(text, out var result))
{
return result;
}
return def;
}
public bool GetBool(int iArg, bool def = false)
{
string text = GetString(iArg, null);
if (text == null)
{
return def;
}
if (text == string.Empty || text == "0")
{
return false;
}
if (text.Equals("f", StringComparison.InvariantCultureIgnoreCase))
{
return false;
}
if (text.Equals("false", StringComparison.InvariantCultureIgnoreCase))
{
return false;
}
if (text.Equals("no", StringComparison.InvariantCultureIgnoreCase))
{
return false;
}
if (text.Equals("none", StringComparison.InvariantCultureIgnoreCase))
{
return false;
}
if (text.Equals("null", StringComparison.InvariantCultureIgnoreCase))
{
return false;
}
return true;
}
public long GetTimestamp(int iArg, long def = 0L)
{
string text = GetString(iArg, null);
if (text == null)
{
return def;
}
int num = 3600;
if (text.Length > 1 && char.IsLetter(text[text.Length - 1]))
{
switch (text[text.Length - 1])
{
case 's':
num = 1;
break;
case 'm':
num = 60;
break;
case 'h':
num = 3600;
break;
case 'd':
num = 86400;
break;
case 'w':
num = 604800;
break;
case 'M':
num = 2592000;
break;
case 'Y':
num = 31536000;
break;
}
text = text.Substring(0, text.Length - 1);
}
if (long.TryParse(text, out var result))
{
if (result > 0 && result <= 315360000)
{
return DateTimeOffset.UtcNow.ToUnixTimeSeconds() + result * num;
}
return result;
}
return def;
}
public long GetTicks(int iArg, long def = 0L)
{
string text = GetString(iArg, null);
if (text == null)
{
return def;
}
int num = 3600;
if (text.Length > 1 && char.IsLetter(text[text.Length - 1]))
{
switch (text[text.Length - 1])
{
case 's':
num = 1;
break;
case 'm':
num = 60;
break;
case 'h':
num = 3600;
break;
case 'd':
num = 86400;
break;
case 'w':
num = 604800;
break;
case 'M':
num = 2592000;
break;
case 'Y':
num = 31536000;
break;
}
text = text.Substring(0, text.Length - 1);
}
if (long.TryParse(text, out var result))
{
return result * num * 10000000;
}
return def;
}
public void ReplyWithObject(object rval)
{
if (rval != null)
{
if (rval is string)
{
ReplyWith((string)rval);
return;
}
string strValue = JsonConvert.SerializeObject(rval, Formatting.Indented);
ReplyWith(strValue);
}
}
public Vector3 GetVector3(int iArg, Vector3 def = default(Vector3))
{
return GetString(iArg, null)?.ToVector3() ?? def;
}
public Color GetColor(int iArg, Color def = default(Color))
{
return GetString(iArg, null)?.ToColor() ?? def;
}
}
public class Factory : Attribute
{
public string Name;
public Factory(string systemName)
{
Name = systemName;
}
}
public class Command
{
public string Name;
public string Parent;
public string FullName;
public Func<string> GetOveride;
public Action<string> SetOveride;
public Action<Arg> Call;
public bool Variable;
public string Default;
public string DefaultValue;
public bool Saved;
public bool ServerAdmin;
public bool ServerUser;
public bool RconOnly;
public bool Replicated;
public bool ShowInAdminUI;
public bool ClientAdmin;
public bool Client;
public bool ClientInfo;
public bool AllowRunFromServer;
public string Description = string.Empty;
public string Arguments = string.Empty;
public bool Server
{
get
{
if (!ServerAdmin)
{
return ServerUser;
}
return true;
}
}
public string String => GetOveride?.Invoke() ?? "";
public int AsInt => String.ToInt();
public float AsFloat => String.ToFloat();
public bool AsBool => String.ToBool();
public Vector3 AsVector3 => String.ToVector3();
public event Action<Command> OnValueChanged;
public Command()
{
Call = DefaultCall;
}
private void ValueChanged()
{
if (Saved)
{
HasChanges = true;
}
if (ClientInfo)
{
SendToServer(BuildCommand("setinfo", FullName, String));
}
if (this.OnValueChanged != null)
{
this.OnValueChanged(this);
}
}
private void DefaultCall(Arg arg)
{
if (SetOveride != null && arg.HasArgs())
{
if (arg.IsClientside && Replicated)
{
SendToServer(arg.RawCommand);
arg.Silent = true;
Debug.LogWarning("ConVar '" + Name + "' will be replicated to all other players on the server");
}
else
{
Set(arg.Args[0]);
}
}
}
public void Set(string value)
{
if (SetOveride != null)
{
string text = String;
SetOveride(value);
if (text != String)
{
ValueChanged();
}
}
}
public void Set(float f)
{
string text = f.ToString("0.00");
if (!(String == text))
{
Set(text);
}
}
public void Set(bool val)
{
if (AsBool != val)
{
Set(val ? "1" : "0");
}
}
}
public interface IConsoleCommand
{
void Call(Arg arg);
}
public interface IConsoleButton
{
bool IsPressed { get; set; }
}
public static class Index
{
public static class Server
{
public static Dictionary<string, Command> Dict = new Dictionary<string, Command>(StringComparer.OrdinalIgnoreCase);
public static Dictionary<string, Command> GlobalDict = new Dictionary<string, Command>(StringComparer.OrdinalIgnoreCase);
public static List<Command> Replicated = new List<Command>();
public static Command Find(string strName)
{
if (!strName.Contains("."))
{
strName = "global." + strName;
}
if (Dict.TryGetValue(strName, out var value))
{
return value;
}
GlobalDict.TryGetValue(strName.Replace("global.", ""), out value);
return value;
}
}
public static class Client
{
public static Dictionary<string, Command> Dict = new Dictionary<string, Command>(StringComparer.OrdinalIgnoreCase);
public static Dictionary<string, Command> GlobalDict = new Dictionary<string, Command>(StringComparer.OrdinalIgnoreCase);
public static Command Find(string strName)
{
if (!strName.Contains("."))
{
strName = WithGlobal.Get(strName);
}
if (Dict.TryGetValue(strName, out var value))
{
return value;
}
GlobalDict.TryGetValue(WithoutGlobal.Get(strName), out value);
return value;
}
}
private static readonly Memoized<string, string> WithGlobal = new Memoized<string, string>((string s) => "global." + s);
private static readonly Memoized<string, string> WithoutGlobal = new Memoized<string, string>((string s) => s.Replace("global.", ""));
public static Command[] All { get; private set; }
public static void Initialize(Command[] Commands)
{
Command[] array = Commands;
foreach (Command command in array)
{
if (command.Variable && command.GetOveride != null)
{
try
{
command.DefaultValue = command.GetOveride() ?? "";
}
catch
{
}
}
}
All = Commands;
Server.Dict = new Dictionary<string, Command>();
Client.Dict = new Dictionary<string, Command>();
array = All;
foreach (Command command2 in array)
{
if (command2.Server)
{
if (Server.Dict.ContainsKey(command2.FullName))
{
Debug.LogWarning("Server Vars have multiple entries for " + command2.FullName);
}
else
{
Server.Dict.Add(command2.FullName, command2);
}
if (command2.Parent != "global" && !Server.GlobalDict.ContainsKey(command2.Name))
{
Server.GlobalDict.Add(command2.Name, command2);
}
if (command2.Replicated)
{
if (!command2.Variable || !command2.ServerAdmin)
{
Debug.LogWarning("Replicated server var " + command2.FullName + " has a bad config");
}
else
{
Server.Replicated.Add(command2);
command2.OnValueChanged += delegate(Command command3)
{
ConsoleSystem.OnReplicatedVarChanged?.Invoke(command3.FullName, command3.String);
};
}
}
}
if (command2.Client)
{
if (Client.Dict.ContainsKey(command2.FullName))
{
Debug.LogWarning("Client Vars have multiple entries for " + command2.FullName);
}
else
{
Client.Dict.Add(command2.FullName, command2);
}
if (command2.Parent != "global" && !Client.GlobalDict.ContainsKey(command2.Name))
{
Client.GlobalDict.Add(command2.Name, command2);
}
}
}
Input.RunBind += delegate(string strCommand, bool pressed)
{
Command command3 = Client.Find(strCommand);
if (command3 != null && command3.Variable && !command3.ClientAdmin && !command3.ServerAdmin && !command3.Replicated)
{
command3.Set(pressed);
}
else
{
Run(Option.Client, $"{strCommand} {pressed}");
}
};
}
public static void Reset()
{
if (All == null)
{
return;
}
Command[] all = All;
foreach (Command command in all)
{
if (command.Variable && command.Default != null)
{
try
{
command.Set(command.Default);
}
catch (Exception arg)
{
Debug.LogError($"Exception running {command.FullName} = {command.Default}: {arg}");
}
}
}
}
}
public struct Option
{
public string RconIP;
public string RconName;
public static Option Unrestricted => new Option
{
IsServer = true,
IsClient = true,
ForwardtoServerOnMissing = true,
PrintOutput = true,
IsUnrestricted = true
};
public static Option Client => new Option
{
IsClient = true,
ForwardtoServerOnMissing = true,
PrintOutput = true
};
public static Option Server => new Option
{
IsServer = true,
PrintOutput = true,
FromRcon = true
};
public bool IsServer { get; set; }
public bool IsClient { get; set; }
public bool ForwardtoServerOnMissing { get; set; }
public bool PrintOutput { get; set; }
public bool IsUnrestricted { get; set; }
public bool FromRcon { get; set; }
public bool PrintValueOnly { get; set; }
public bool IsFromServer { get; set; }
public bool FromCommandBlock { get; set; }
public Connection Connection { get; set; }
public bool ServerConsole { get; set; }
public bool ConfigFile { get; set; }
public int RconConnectionId { get; set; }
public Option Quiet()
{
PrintOutput = false;
return this;
}
public Option PrintValue()
{
PrintValueOnly = true;
return this;
}
public Option FromServer()
{
IsFromServer = true;
return this;
}
public Option FromConnection(Connection connection)
{
FromRcon = false;
Connection = connection;
return this;
}
public Option FromRconConnection(int id, string ip, string connectionName)
{
RconConnectionId = id;
RconIP = ip;
RconName = connectionName;
return this;
}
public Option FromServerConsole()
{
ServerConsole = true;
return this;
}
public Option FromConfigFile()
{
ConfigFile = true;
return this;
}
}
public struct CommandResult
{
public CommandResultType Result;
public string Output;
public Command Command;
public CommandResult(CommandResultType result, string output, Command command)
{
Result = result;
Output = output;
Command = command;
}
}
public enum CommandResultType
{
Unknown,
Success,
PermissionDenied,
CommandNotFound
}
public static bool HasChanges = false;
public static Func<bool> ClientCanRunAdminCommands;
public static bool loggingEnabled = false;
public static string IdentityDirectory;
private static StreamWriter _logWriter;
private static DateTime _logTimestamp;
private static ConcurrentQueue<string> _logQueue = new ConcurrentQueue<string>();
private static Task _logThread;
public static Func<string, bool> OnSendToServer;
public static string LastError = null;
public static Arg CurrentArgs = null;
private static List<string> ignoredCommands = new List<string> { "projectpath", "useHub", "hubIPC", "cloudEnvironment", "licensingIpc", "hubSessionId", "accessToken" };
public static event Action<string, string> OnReplicatedVarChanged;
public static void UpdateValuesFromCommandLine()
{
foreach (KeyValuePair<string, string> @switch in CommandLine.GetSwitches())
{
string text = @switch.Value;
if (text == "")
{
text = "1";
}
string strCommand = @switch.Key.Substring(1);
Run(Option.Unrestricted, strCommand, text);
}
}
private static void LogToFile(Arg arg)
{
if (arg.Connection == null || arg.Connection.authLevel != 0 || (arg.cmd.ServerAdmin && !arg.cmd.ServerUser))
{
if (_logThread == null || _logThread.IsCompleted)
{
_logThread = LogThread();
}
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append($"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] ");
if (!string.IsNullOrEmpty(arg.Option.RconIP))
{
stringBuilder.Append("[RCON " + arg.Option.RconIP + " '" + arg.Option.RconName + "'] ");
}
else if (arg.Option.ServerConsole)
{
stringBuilder.Append("[SERVER CONSOLE] ");
}
else if (arg.Option.ConfigFile)
{
stringBuilder.Append("[CONFIG FILE] ");
}
else if (arg.Connection != null)
{
stringBuilder.Append($"[USER {arg.Connection.userid} {arg.Connection.ipaddress}] ");
}
else if (arg.IsClientside && !arg.IsServerside)
{
stringBuilder.Append("[CLIENTSIDE] ");
}
else
{
stringBuilder.Append("[UNKNOWN] ");
}
stringBuilder.Append(arg.RawCommand);
_logQueue.Enqueue(stringBuilder.ToString());
}
}
private static async Task LogThread()
{
while (Application.isPlaying)
{
await Task.Delay(1000);
if (!_logQueue.IsEmpty)
{
StreamWriter writer = await GetLogStream();
string result;
while (_logQueue.TryDequeue(out result))
{
await writer.WriteLineAsync(result);
}
await writer.FlushAsync();
}
}
await (await GetLogStream()).DisposeAsync();
}
private static async Task<StreamWriter> GetLogStream()
{
DateTime today = DateTime.UtcNow.Date;
if (_logWriter == null || today != _logTimestamp)
{
if (_logWriter != null)
{
await _logWriter.DisposeAsync();
}
_logTimestamp = today;
_logWriter = OpenLogFile();
}
return _logWriter;
}
private static StreamWriter OpenLogFile()
{
string text = IdentityDirectory;
if (string.IsNullOrEmpty(text))
{
text = "Logs";
}
text = Path.Combine(text, "command_history");
Directory.CreateDirectory(text);
string text2 = DateTime.UtcNow.Date.ToString("yyyy-MM-dd");
string path = "commands_" + text2 + ".log";
FileStream fileStream = new FileStream(Path.Combine(text, path), FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read);
fileStream.Position = fileStream.Length;
return new StreamWriter(fileStream, Encoding.UTF8);
}
internal static bool SendToServer(string command)
{
if (OnSendToServer != null)
{
return OnSendToServer(command);
}
return false;
}
public static void RunFile(Option options, string strFile)
{
string[] array = strFile.Split(new char[1] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < array.Length; i++)
{
string text = array[i].Trim();
if (!string.IsNullOrWhiteSpace(text) && text[0] != '#')
{
Run(options.FromConfigFile(), text);
}
}
HasChanges = false;
}
public static string Run(Option options, string strCommand, params object[] args)
{
return RunWithResult(options, strCommand, args).Output;
}
public static CommandResult RunWithResult(Option options, string strCommand, params object[] args)
{
LastError = null;
string text = BuildCommand(strCommand, args);
Arg arg = new Arg(options, text);
bool flag = arg.HasPermission();
if (!arg.Invalid && flag)
{
Arg currentArgs = CurrentArgs;
CurrentArgs = arg;
bool flag2 = Internal(arg);
CurrentArgs = currentArgs;
if (options.PrintOutput && flag2 && arg.Reply != null && arg.Reply.Length > 0)
{
DebugEx.Log(arg.Reply);
}
if (loggingEnabled)
{
LogToFile(arg);
}
return new CommandResult(CommandResultType.Success, arg.Reply, arg.cmd);
}
LastError = "Command not found";
CommandResultType result = CommandResultType.CommandNotFound;
if (!flag)
{
LastError = "Permission denied";
result = CommandResultType.PermissionDenied;
}
if (!options.IsServer && (!options.ForwardtoServerOnMissing || !SendToServer(text)))
{
LastError = "Command '" + strCommand + "' not found";
if (options.PrintOutput && !ignoredCommands.Contains(strCommand))
{
DebugEx.Log(LastError);
}
return new CommandResult(CommandResultType.CommandNotFound, null, null);
}
if (options.IsServer && options.PrintOutput)
{
LastError = "Command '" + strCommand + "' not found";
if (!ignoredCommands.Contains(strCommand))
{
DebugEx.Log(LastError);
}
}
return new CommandResult(result, null, null);
}
private static bool Internal(Arg arg)
{
if (arg.Invalid)
{
return false;
}
if (!arg.HasPermission())
{
arg.ReplyWith("You cannot run this command");
return false;
}
try
{
using (TimeWarning.New("ConsoleSystem: " + arg.cmd.FullName))
{
arg.cmd.Call(arg);
}
}
catch (Exception ex)
{
arg.ReplyWith("Error: " + arg.cmd.FullName + " - " + ex.Message + " (" + ex.Source + ")");
Debug.LogException(ex);
return false;
}
if (arg.cmd.Variable && arg.cmd.GetOveride != null && string.IsNullOrWhiteSpace(arg.Reply))
{
string text = arg.cmd.String;
string text2 = (arg.cmd.Variable ? arg.cmd.String : "");
if (!arg.Silent)
{
if (arg.Option.PrintValueOnly)
{
arg.ReplyWith(text);
}
else if (text2 != text)
{
arg.ReplyWith($"{arg.cmd.FullName}: changed from {text2.QuoteSafe()} to {text.QuoteSafe()}");
}
else
{
arg.ReplyWith($"{arg.cmd.FullName}: {text.QuoteSafe()}");
}
}
}
return true;
}
public static string BuildCommand(string strCommand, params object[] args)
{
if (args == null || args.Length == 0)
{
return strCommand;
}
StringBuilder obj = Pool.Get<StringBuilder>();
obj.Clear();
obj.Append(strCommand);
foreach (object obj2 in args)
{
if (obj2 == null)
{
obj.Append(" \"\"");
}
else if (obj2 is Color color)
{
obj.Append(" \"").Append(color.r).Append(',')
.Append(color.g)
.Append(',')
.Append(color.b)
.Append(',')
.Append(color.a)
.Append('"');
}
else if (obj2 is Vector3 vector)
{
obj.Append(" \"").Append(vector.x).Append(',')
.Append(vector.y)
.Append(',')
.Append(vector.z)
.Append('"');
}
else if (obj2 is IEnumerable<string> enumerable)
{
foreach (string item in enumerable)
{
obj.Append(' ').QuoteSafe(item);
}
}
else
{
obj.Append(' ').QuoteSafe(obj2.ToString());
}
}
string result = obj.ToString();
obj.Clear();
Pool.FreeUnmanaged(ref obj);
return result;
}
public static string SaveToConfigString(bool bServer)
{
string text = "";
IEnumerable<Command> enumerable = ((!bServer) ? Index.All.Where((Command x) => x.Saved && x.Client && !x.Replicated) : Index.All.Where((Command x) => x.Saved && x.ServerAdmin));
foreach (Command item in enumerable)
{
if (item.GetOveride != null)
{
text = text + item.FullName + " " + item.String.QuoteSafe();
text += Environment.NewLine;
}
}
return text;
}
}