Some mods target the Mono version of the game, which is available by opting into the Steam beta branch "alternate"
Decompiled source of Hash v1.0.2
Hash.dll
Decompiled 11 hours ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Reflection; using System.Resources; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using HarmonyLib; using Hash; using Hash.Game; using Hash.Terminal; using Il2CppFishNet; using Il2CppFishNet.Object; using Il2CppInterop.Runtime; using Il2CppInterop.Runtime.Injection; using Il2CppInterop.Runtime.InteropTypes; using Il2CppScheduleOne; using Il2CppScheduleOne.AvatarFramework; using Il2CppScheduleOne.AvatarFramework.Emotions; using Il2CppScheduleOne.Core.Items.Framework; using Il2CppScheduleOne.Cutscenes; using Il2CppScheduleOne.DevUtilities; using Il2CppScheduleOne.Employees; using Il2CppScheduleOne.Interaction; using Il2CppScheduleOne.ItemFramework; using Il2CppScheduleOne.Map; using Il2CppScheduleOne.NPCs; using Il2CppScheduleOne.Persistence; using Il2CppScheduleOne.PlayerScripts; using Il2CppScheduleOne.Product; using Il2CppScheduleOne.Product.Packaging; using Il2CppScheduleOne.Property; using Il2CppScheduleOne.Quests; using Il2CppScheduleOne.UI; using Il2CppScheduleOne.Variables; using Il2CppScheduleOne.Vehicles; using Il2CppSystem; using Il2CppSystem.Collections.Generic; using MelonLoader; using MelonLoader.Preferences; using MelonLoader.Utils; using Microsoft.CodeAnalysis; using Sideload.Api; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: MelonInfo(typeof(Core), "hash", "1.0.2", "DooDesch", "https://github.com/DooDesch-Mods/ScheduleOne-Hash")] [assembly: MelonGame("TVGS", "Schedule I")] [assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")] [assembly: AssemblyCompany("Hash")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.2.0")] [assembly: AssemblyInformationalVersion("1.0.2+eb6683bcfee3f2a30b362296fea41161889c5776")] [assembly: AssemblyProduct("Hash")] [assembly: AssemblyTitle("Hash")] [assembly: NeutralResourcesLanguage("en-US")] [assembly: AssemblyVersion("1.0.2.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace Sideload.Api { public static class Apps { private static bool _bound; private static int _probeAttempts; private static readonly List<Action> _pending = new List<Action>(); private static Action<string, string, string, string, Assembly> _registerApp; private static Action<string, string, Func<string, string, string>> _handle; private static Action<string, string, string> _emit; private static Action<string, string> _allowHost; private static Action<string, string> _declareOrientations; private static Action<string, int> _setBadge; private static Action<string, string, string> _notify; private static Action<string, string, string, float> _notifyFor; private static Func<string, bool> _isOnScreen; private static Action<string, string, byte[]> _setImage; private static Action<string, bool> _setIconHidden; private static Action<string, bool> _setAppOpen; private static Func<string, bool> _isAppOpen; private static Func<bool, bool> _setPhoneRaised; private static Func<bool> _isPhoneRaised; public static bool Available { get { EnsureBound(); return _bound; } } internal static bool HasPhone { get { EnsureBound(); return _setPhoneRaised != null; } } internal static bool HasOpen { get { EnsureBound(); if (_setAppOpen != null) { return _setIconHidden != null; } return false; } } public static AppHandle Register(string id, string bundlePrefix, string title = null, string iconLabel = null, Assembly hostAssembly = null) { AppHandle result = new AppHandle(id); if (string.IsNullOrEmpty(id)) { return result; } Assembly asm = hostAssembly ?? Assembly.GetCallingAssembly(); string t = title; string il = iconLabel; string prefix = bundlePrefix; EnsureBound(); if (_registerApp != null) { _registerApp(id, t, il, prefix, asm); } else { _pending.Add(delegate { _registerApp?.Invoke(id, t, il, prefix, asm); }); } return result; } internal static void WhenBound(Action work) { if (work != null) { EnsureBound(); if (_bound) { work(); } else { _pending.Add(work); } } } internal static void HandleCall(string appId, string name, Func<string, string> handler) { if (_handle != null && handler != null) { _handle(appId, name, (string app, string argument) => handler(argument)); } } internal static void EmitEvent(string appId, string name, string payload) { _emit?.Invoke(appId, name, payload); } internal static void AllowNetHost(string appId, string host) { _allowHost?.Invoke(appId, host); } internal static void Orient(string appId, string orientations) { _declareOrientations?.Invoke(appId, orientations); } internal static void Badge(string appId, int count) { _setBadge?.Invoke(appId, count); } internal static void Notify(string appId, string title, string subtitle, float seconds) { if (_notifyFor != null) { _notifyFor(appId, title, subtitle, seconds); } else if (_notify != null) { _notify(appId, title, subtitle); } } internal static bool OnScreen(string appId) { if (_isOnScreen != null) { return _isOnScreen(appId); } return false; } internal static void SetImage(string appId, string name, byte[] png) { _setImage?.Invoke(appId, name, png); } internal static void HideIcon(string appId, bool hidden) { _setIconHidden?.Invoke(appId, hidden); } internal static void OpenApp(string appId, bool open) { _setAppOpen?.Invoke(appId, open); } internal static bool AppIsOpen(string appId) { if (_isAppOpen != null) { return _isAppOpen(appId); } return false; } internal static bool RaisePhone(bool raised) { if (_setPhoneRaised != null) { return _setPhoneRaised(raised); } return false; } internal static bool PhoneIsRaised() { if (_isPhoneRaised != null) { return _isPhoneRaised(); } return false; } private static void EnsureBound() { if (_bound) { return; } try { Type type = FindBridge(_probeAttempts++ % 30 == 0); if (type == null || (type.GetField("AbiVersion", BindingFlags.Static | BindingFlags.Public)?.GetValue(null) is int num && num < 1)) { return; } _registerApp = Get<Action<string, string, string, string, Assembly>>(type, "RegisterApp"); if (_registerApp == null) { return; } _handle = Get<Action<string, string, Func<string, string, string>>>(type, "Handle"); _emit = Get<Action<string, string, string>>(type, "Emit"); _allowHost = Get<Action<string, string>>(type, "AllowHost"); _declareOrientations = Get<Action<string, string>>(type, "DeclareOrientations"); _setBadge = Get<Action<string, int>>(type, "SetBadge"); _notify = Get<Action<string, string, string>>(type, "Notify"); _notifyFor = Get<Action<string, string, string, float>>(type, "NotifyFor"); _isOnScreen = Get<Func<string, bool>>(type, "IsAppOnScreen"); _setImage = Get<Action<string, string, byte[]>>(type, "SetImage"); _setIconHidden = Get<Action<string, bool>>(type, "SetIconHidden"); _setAppOpen = Get<Action<string, bool>>(type, "SetAppOpen"); _isAppOpen = Get<Func<string, bool>>(type, "IsAppOpen"); _setPhoneRaised = Get<Func<bool, bool>>(type, "SetPhoneRaised"); _isPhoneRaised = Get<Func<bool>>(type, "IsPhoneRaised"); _bound = true; for (int i = 0; i < _pending.Count; i++) { try { _pending[i](); } catch { } } _pending.Clear(); } catch { } } private static T Get<T>(Type t, string field) where T : class { return t.GetField(field, BindingFlags.Static | BindingFlags.Public)?.GetValue(null) as T; } private static Type FindBridge(bool scan) { Type type = Type.GetType("Sideload.Bridge.SideloadBridge, Sideload", throwOnError: false); if (type != null || !scan) { return type; } Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { try { type = assembly.GetType("Sideload.Bridge.SideloadBridge", throwOnError: false); if (type != null) { return type; } } catch { } } return null; } } public sealed class AppHandle { private readonly string _id; public string Id => _id; public bool IsOnScreen { get { if (Apps.Available) { return Apps.OnScreen(_id); } return false; } } public bool IsOpen { get { if (Apps.Available) { return Apps.AppIsOpen(_id); } return false; } } public static bool CanOpenProgrammatically { get { if (Apps.Available) { return Apps.HasOpen; } return false; } } internal AppHandle(string id) { _id = id ?? ""; } public AppHandle OnCall(string name, Func<string, string> handler) { string id = _id; Apps.WhenBound(delegate { Apps.HandleCall(id, name, handler); }); return this; } public void Emit(string name, string payload = "") { string id = _id; Apps.WhenBound(delegate { Apps.EmitEvent(id, name, payload); }); } public AppHandle AllowHost(string host) { string id = _id; Apps.WhenBound(delegate { Apps.AllowNetHost(id, host); }); return this; } public AppHandle Orientation(params string[] supported) { string id = _id; string list = ((supported == null) ? "" : string.Join(",", supported)); Apps.WhenBound(delegate { Apps.Orient(id, list); }); return this; } public AppHandle Badge(int count) { string id = _id; Apps.WhenBound(delegate { Apps.Badge(id, count); }); return this; } public AppHandle Notify(string title, string subtitle = "", float seconds = 0f) { string id = _id; Apps.WhenBound(delegate { Apps.Notify(id, title, subtitle, seconds); }); return this; } public AppHandle Image(string name, byte[] png) { string id = _id; Apps.WhenBound(delegate { Apps.SetImage(id, name, png); }); return this; } public AppHandle Icon(bool visible) { Apps.HideIcon(_id, !visible); return this; } public AppHandle NoIcon() { string id = _id; Apps.WhenBound(delegate { Apps.HideIcon(id, hidden: true); }); return this; } public AppHandle Open() { string id = _id; Apps.WhenBound(delegate { Apps.OpenApp(id, open: true); }); return this; } public AppHandle Close() { string id = _id; Apps.WhenBound(delegate { Apps.OpenApp(id, open: false); }); return this; } public bool Show() { if (!Apps.RaisePhone(raised: true)) { return false; } Apps.OpenApp(_id, open: true); return true; } public AppHandle Hide() { string id = _id; Apps.WhenBound(delegate { Apps.OpenApp(id, open: false); Apps.RaisePhone(raised: false); }); return this; } } public static class PhoneScreen { public static bool IsRaised { get { if (Apps.Available) { return Apps.PhoneIsRaised(); } return false; } } public static bool Available { get { if (Apps.Available) { return Apps.HasPhone; } return false; } } public static bool Raise() { return Apps.RaisePhone(raised: true); } public static bool Lower() { return Apps.RaisePhone(raised: false); } } } namespace Hash { public class Core : MelonMod { internal const string AppId = "hash"; internal static Instance Log; private static MelonPreferences_Entry<bool> _hijack; private AppHandle _app; private Session _session; private CommandIndex _index; private ArgProviders _providers; private LogCapture _log; private CommandRunner _runner; private Store _store; private History _history; private Aliases _aliases; private Usage _usage; private WorldMarks _marks; private bool _wasOnScreen; private int _logsDrawnUpTo; private const float IconInterval = 0.5f; private float _iconCheckedAt; private int _toggledOnFrame = -1; public override void OnInitializeMelon() { Log = ((MelonBase)this).LoggerInstance; _hijack = MelonPreferences.CreateCategory("Hash", "hash").CreateEntry<bool>("HijackConsoleKey", true, "Console key opens the terminal", "ON (default): the key that opened the console now takes the phone out with hash on it. OFF: the vanilla console bar comes back and hash stays reachable only from code. Turn it off if another mod needs the vanilla bar.", false, false, (ValueValidator)null, (string)null); if (!PhoneScreen.Available) { Log.Error("[hash] needs Sideload 1.5.0 or newer - this one cannot take the phone out of the player's pocket, so the terminal could never be reached. Nothing was registered."); return; } Build(); RegisterApp(); Patch(); Log.Msg("[hash] ready. Press the console key."); } private void Build() { _store = new Store(); _log = new LogCapture(); _providers = new ArgProviders(); _index = new CommandIndex(_providers); _history = new History(); _aliases = new Aliases(); _usage = new Usage(); _history.Load(_store); _aliases.Load(_store); _marks = new WorldMarks(); _runner = new CommandRunner(_log); _session = new Session(_index, _runner, _usage, _history, _aliases, _marks); _runner.LogViewOpen = () => _session.Builtins.LogsOpen; _session.Builtins.UseFace(_store.Read(StoreScope.Global, "font")); } private void RegisterApp() { _app = Apps.Register("hash", "Hash.Assets.hash", "hash", "hash").Orientation("landscape").NoIcon() .OnCall("boot", (string _) => Boot()) .OnCall("nav", Nav) .OnCall("run", Run) .OnCall("drain", (string _) => Drain()) .OnCall("close", delegate { Toggle(); return ""; }); } private void Patch() { ItemSourcePatch.Providers = _providers; ConsoleAwakePatch.OnAwake = delegate { DeclaredCommands.Apply(); _index.MarkDirty(); }; ConsoleKeyPatch.OnOpen = Toggle; ConsoleKeyPatch.Enabled = _hijack.Value; try { ((MelonBase)this).HarmonyInstance.PatchAll(); } catch (Exception ex) { Log.Error("[hash] patching failed - the console key will open the vanilla bar: " + ex); ConsoleKeyPatch.Enabled = false; } } private bool Toggle() { if (_app == null) { return false; } int frameCount = Time.frameCount; if (frameCount == _toggledOnFrame) { return true; } _toggledOnFrame = frameCount; if (_app.IsOpen && PhoneScreen.IsRaised) { _app.Hide(); _wasOnScreen = false; Persist(); return true; } _index.MarkDirty(); _providers.Invalidate(); if (!_app.Show()) { Log.Warning("[hash] the game would not take the phone out right now, so the terminal stayed shut."); return false; } _wasOnScreen = true; _app.Emit("shown"); return true; } public override void OnUpdate() { _marks?.Tick(); IconFollowsTheConsole(); if (_wasOnScreen && _app != null && (!_app.IsOpen || !PhoneScreen.IsRaised)) { _wasOnScreen = false; Persist(); } } private void IconFollowsTheConsole() { if (_app != null && _runner != null) { float unscaledTime = Time.unscaledTime; if (!(unscaledTime - _iconCheckedAt < 0.5f)) { _iconCheckedAt = unscaledTime; _app.Icon(_runner.CanRun); } } } public override void OnDeinitializeMelon() { Persist(); _log?.Dispose(); } private void Persist() { _history.Save(_store); _aliases.Save(_store); _usage.Save(_store); _store.Write(StoreScope.Global, "font", _session.Builtins.Face); } private string Boot() { _usage.Load(_store); _logsDrawnUpTo = _log.Ring.Count; if (_session.Transcript.Count == 0) { _session.Transcript.Add(_session.Banner(Identity())); } Json json = new Json(); json.Str("session", _session.Locked ? "session:client" : "session:host"); json.Num("commands", _session.CommandCount); json.Str("prompt", "hash $"); MelonInfoAttribute info = ((MelonBase)this).Info; json.Str("version", ((info != null) ? info.Version : null) ?? ""); json.Str("mark", Mark()); json.Bool("locked", _session.Locked); json.Bool("live", _session.Builtins.LogsOpen); json.Str("font", _session.Builtins.Face); json.Raw("banner", Lines(_session.Transcript.Window())); return json.Done(); } private string Identity() { MelonInfoAttribute info = ((MelonBase)this).Info; string text = ((info != null) ? info.Version : null) ?? ""; string text2 = ""; try { text2 = Application.version ?? ""; } catch { } return "hash" + ((text.Length > 0) ? (" v" + text) : "") + ((text2.Length > 0) ? (" on Schedule I " + text2) : ""); } private string Nav(string argument) { string line = Json.Field(argument, "line"); string action = Json.Field(argument, "action"); NavResult navResult = _session.Navigate(line, action); Json json = new Json(); if (navResult.Line != null) { json.Str("line", navResult.Line); } json.Str("suggest", navResult.Suggest); json.Str("mark", Mark()); json.Str("ghost", navResult.Ghost); return json.Done(); } private string Drain() { bool flag = _wasOnScreen && _session.Builtins.LogsOpen; Json json = new Json(); IReadOnlyList<OutputLine> lines; if (!flag) { IReadOnlyList<OutputLine> readOnlyList = Array.Empty<OutputLine>(); lines = readOnlyList; } else { IReadOnlyList<OutputLine> readOnlyList = Fresh(); lines = readOnlyList; } json.Raw("lines", Lines(lines)); json.Bool("live", _session.Builtins.LogsOpen); json.Str("font", _session.Builtins.Face); return json.Done(); } private string Mark() { Mark mark = _session.Marks.Resolve("#"); if (!mark.Exists) { return ""; } return "# " + mark.Id; } private string Run(string line) { RunResult runResult = _session.Run(line); if (!string.IsNullOrEmpty(runResult.Clipboard)) { Clipboard.Put(runResult.Clipboard); } Json json = new Json(); json.Raw("lines", Lines(WithLogs(runResult.Lines))); json.Num("commands", _session.CommandCount); json.Bool("cleared", runResult.Cleared); json.Str("mark", Mark()); json.Bool("live", _session.Builtins.LogsOpen); json.Str("font", _session.Builtins.Face); return json.Done(); } private IReadOnlyList<OutputLine> WithLogs(IReadOnlyList<OutputLine> ran) { if (!_session.Builtins.LogsOpen) { _logsDrawnUpTo = _log.Ring.Count; return ran; } List<OutputLine> list = new List<OutputLine>(ran); list.AddRange(Fresh()); return list; } private List<OutputLine> Fresh() { List<OutputLine> list = new List<OutputLine>(); string logsFilter = _session.Builtins.LogsFilter; for (int i = Math.Max(0, _logsDrawnUpTo); i < _log.Ring.Count; i++) { OutputLine outputLine = _log.Ring[i]; if (logsFilter.Length <= 0 || outputLine.Text.IndexOf(logsFilter, StringComparison.OrdinalIgnoreCase) >= 0 || logsFilter.Equals(Kind(outputLine), StringComparison.OrdinalIgnoreCase)) { _session.Push(outputLine); list.Add(outputLine); } } _logsDrawnUpTo = _log.Ring.Count; return list; } private static string Kind(OutputLine line) { return line.Kind switch { LineKind.Warn => "warn", LineKind.Error => "error", _ => "log", }; } private static string Lines(IReadOnlyList<OutputLine> lines) { StringBuilder stringBuilder = new StringBuilder("["); for (int i = 0; i < lines.Count; i++) { if (i > 0) { stringBuilder.Append(','); } Json json = new Json(); json.Str("cls", Css(lines[i].Kind)); json.Str("text", lines[i].Text); stringBuilder.Append(json.Done()); } return stringBuilder.Append(']').ToString(); } private static string Css(LineKind kind) { return kind switch { LineKind.Echo => "echo", LineKind.Warn => "warn", LineKind.Error => "err", LineKind.Dim => "dim", _ => "", }; } } internal sealed class Json { private readonly StringBuilder _sb = new StringBuilder("{"); internal Json Str(string name, string value) { return Put(name, Quote(value)); } internal Json Num(string name, int value) { return Put(name, value.ToString(CultureInfo.InvariantCulture)); } internal Json Bool(string name, bool value) { return Put(name, value ? "true" : "false"); } internal Json Raw(string name, string json) { return Put(name, json ?? "null"); } internal string Done() { return _sb.Append('}').ToString(); } private Json Put(string name, string value) { if (_sb.Length > 1) { _sb.Append(','); } _sb.Append(Quote(name)).Append(':').Append(value); return this; } internal static string Quote(string value) { if (value == null) { return "\"\""; } StringBuilder stringBuilder = new StringBuilder(value.Length + 2).Append('"'); foreach (char c in value) { switch (c) { case '"': stringBuilder.Append("\\\""); continue; case '\\': stringBuilder.Append("\\\\"); continue; case '\n': stringBuilder.Append("\\n"); continue; case '\r': stringBuilder.Append("\\r"); continue; case '\t': stringBuilder.Append("\\t"); continue; } if (c < ' ') { StringBuilder stringBuilder2 = stringBuilder.Append("\\u"); int num = c; stringBuilder2.Append(num.ToString("x4", CultureInfo.InvariantCulture)); } else { stringBuilder.Append(c); } } return stringBuilder.Append('"').ToString(); } internal static string Field(string json, string name) { if (string.IsNullOrEmpty(json) || string.IsNullOrEmpty(name)) { return ""; } string text = "\"" + name + "\""; int num = json.IndexOf(text, StringComparison.Ordinal); if (num < 0) { return ""; } int num2 = json.IndexOf(':', num + text.Length); if (num2 < 0) { return ""; } int i; for (i = num2 + 1; i < json.Length && char.IsWhiteSpace(json[i]); i++) { } if (i >= json.Length || json[i] != '"') { return ""; } StringBuilder stringBuilder = new StringBuilder(); for (i++; i < json.Length; i++) { char c = json[i]; if (c == '\\' && i + 1 < json.Length) { char c2 = json[++i]; switch (c2) { case 'n': stringBuilder.Append('\n'); continue; case 'r': stringBuilder.Append('\r'); continue; case 't': stringBuilder.Append('\t'); continue; case 'u': if (i + 4 < json.Length) { if (int.TryParse(json.Substring(i + 1, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var result)) { stringBuilder.Append((char)result); i += 4; } continue; } break; } stringBuilder.Append(c2); } else { if (c == '"') { break; } stringBuilder.Append(c); } } return stringBuilder.ToString(); } } } namespace Hash.Terminal { public sealed class Aliases { private const string FileName = "aliases.txt"; private readonly Dictionary<string, string> _byName = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); private bool _dirty; public int Count => _byName.Count; public IEnumerable<KeyValuePair<string, string>> All { get { List<string> list = new List<string>(_byName.Keys); list.Sort(StringComparer.OrdinalIgnoreCase); foreach (string item in list) { yield return new KeyValuePair<string, string>(item, _byName[item]); } } } public string Expand(string name) { if (name == null || !_byName.TryGetValue(name, out var value)) { return null; } return value; } public bool TrySet(string name, string expansion, Func<string, bool> isRealCommand, out string error) { error = null; string text = name?.Trim(); string value = expansion?.Trim(); if (string.IsNullOrEmpty(text)) { error = "an alias needs a name."; return false; } if (string.IsNullOrEmpty(value)) { error = "an alias needs something to stand for."; return false; } if (text.IndexOf(' ') >= 0) { error = "an alias name cannot contain a space."; return false; } if (isRealCommand != null && isRealCommand(text)) { error = "'" + text + "' is a real command; an alias would hide it."; return false; } _byName[text] = value; _dirty = true; return true; } public bool Remove(string name) { if (name == null || !_byName.Remove(name)) { return false; } _dirty = true; return true; } public void Load(IStore store) { _byName.Clear(); _dirty = false; string text = store?.Read(StoreScope.Global, "aliases.txt"); if (string.IsNullOrEmpty(text)) { return; } string[] array = text.Split('\n'); for (int i = 0; i < array.Length; i++) { string text2 = array[i].Trim('\r'); if (text2.Length != 0) { int num = text2.IndexOf('\t'); if (num > 0 && num != text2.Length - 1) { _byName[text2.Substring(0, num)] = text2.Substring(num + 1); } } } } public void Save(IStore store) { if (!_dirty || store == null) { return; } StringBuilder stringBuilder = new StringBuilder(); foreach (KeyValuePair<string, string> item in All) { stringBuilder.Append(item.Key).Append('\t').Append(item.Value) .Append('\n'); } store.Write(StoreScope.Global, "aliases.txt", stringBuilder.ToString()); _dirty = false; } } public sealed class Builtins { private readonly Suggestions _suggestions; private readonly ICommandCatalogue _catalogue; private readonly History _history; private readonly Aliases _aliases; private readonly Transcript _transcript; public const string Source = "hash"; public static readonly IReadOnlyList<CommandInfo> Catalogue = new CommandInfo[11] { Own("help", "help [word|topic|all] [page]", "explain one command, list a topic, or search", "help all 2"), Own("clear", "clear", "empty the screen", "clear"), Own("history", "history [text]", "what you have run before, oldest first", "history give"), Own("alias", "alias [name] [command]", "make a short name for a command, or list them", "alias gk \"give ogkush 5\""), Own("unalias", "unalias <name>", "remove an alias", "unalias gk"), Own("grep", "grep <text>", "show only the lines on screen containing something", "grep ogkush"), Own("copy", "copy [text]", "put a line on the system clipboard - the last one, unless you name it", "copy ogkushseed"), Own("logs", "logs [filter|off]", "show what the game logs as it happens", "logs warn"), Own("raw", "raw <command>", "run the rest of the line as one command, semicolons and all", "raw bind t 'settime 1200'"), Own("repeat", "repeat <count> <command>", "run a command several times", "repeat 5 give ogkush 1"), Own("font", "font [mono|pixel]", "switch the typeface, or say which one is on", "font pixel") }; public static readonly string[] Words = Vocabulary(); public string PendingClipboard { get; private set; } public string Face { get; private set; } = "mono"; public bool LogsOpen { get; private set; } public string LogsFilter { get; private set; } = ""; public Builtins(Suggestions suggestions, ICommandCatalogue catalogue, History history, Aliases aliases, Transcript transcript) { _suggestions = suggestions; _catalogue = catalogue; _history = history; _aliases = aliases; _transcript = transcript; } private static CommandInfo Own(string word, string signature, string description, string usage) { return new CommandInfo(word, description, usage, signature, "hash", isVanilla: false); } private static string[] Vocabulary() { string[] array = new string[Catalogue.Count]; for (int i = 0; i < Catalogue.Count; i++) { array[i] = Catalogue[i].Word; } return array; } public string TakeClipboard() { string pendingClipboard = PendingClipboard; PendingClipboard = null; return pendingClipboard; } public void UseFace(string face) { if (string.Equals(face, "pixel", StringComparison.OrdinalIgnoreCase)) { Face = "pixel"; } else if (string.Equals(face, "mono", StringComparison.OrdinalIgnoreCase)) { Face = "mono"; } } public bool TryRun(string line, out IReadOnlyList<OutputLine> output) { output = null; List<string> list = CommandLine.Tokenise(line); if (list.Count == 0) { return false; } string text = list[0].ToLowerInvariant(); if (Array.IndexOf(Words, text) < 0) { return false; } if (_suggestions.IsCommand(text)) { return false; } string text2 = ((line.Length > list[0].Length) ? line.Substring(list[0].Length).Trim() : ""); List<OutputLine> list2 = new List<OutputLine>(); switch (text) { case "help": Help(text2, list2); break; case "clear": _transcript.Clear(); break; case "history": HistoryList(text2, list2); break; case "alias": Alias(text2, list, list2); break; case "unalias": Unalias(text2, list2); break; case "grep": Grep(text2, list2); break; case "copy": Copy(text2, list2); break; case "logs": Logs(text2, list2); break; case "font": Typeface(text2, list2); break; case "raw": list2.Add(OutputLine.Error("raw: no command after it")); UsageLine("raw", list2); break; case "repeat": list2.Add(OutputLine.Error("repeat: no count and no command")); UsageLine("repeat", list2); break; } output = list2; return true; } private void Help(string query, List<OutputLine> lines) { if (query.Length == 0) { Brief(lines); return; } if (query.StartsWith("all", StringComparison.OrdinalIgnoreCase)) { int.TryParse(query.Substring(3).Trim(), out var result); Everything(Math.Max(1, result), lines); return; } if (HelpTopics.IsTopic(query)) { OneTopic(query, lines); return; } CommandInfo commandInfo = _suggestions.Find(query); if (commandInfo != null) { Detail(commandInfo, lines); return; } List<CommandInfo> list = new List<CommandInfo>(); foreach (CommandInfo command in _catalogue.Commands) { if (FuzzyMatcher.IsMatch(command.Word, query) || command.Description.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0) { list.Add(command); } } if (list.Count == 0) { lines.Add(OutputLine.Warn("help: nothing matches '" + query + "'")); return; } lines.Add(OutputLine.Dim($"{list.Count} match '{query}':")); foreach (CommandInfo item in list) { lines.Add(OutputLine.Out(Row(item))); } } private void Brief(List<OutputLine> lines) { string[] common = HelpTopics.Common; foreach (string word in common) { CommandInfo commandInfo = _suggestions.Find(word); if (commandInfo != null) { lines.Add(OutputLine.Out(Row(commandInfo))); } } lines.Add(OutputLine.Out("")); Dictionary<string, List<string>> dictionary = Sort(); List<string> list = new List<string>(); (string, string[])[] groups = HelpTopics.Groups; for (int i = 0; i < groups.Length; i++) { string item = groups[i].Item1; if (dictionary.TryGetValue(item, out var value)) { list.Add($"{item} {value.Count}"); } } foreach (KeyValuePair<string, List<string>> item2 in dictionary) { if (!HelpTopics.IsTopic(item2.Key)) { list.Add($"{item2.Key} {item2.Value.Count}"); } } if (dictionary.TryGetValue("terminal", out var value2)) { list.Add($"{"terminal"} {value2.Count}"); } Wrap("topics", list, lines); lines.Add(OutputLine.Dim($"{Total()} commands. 'help <topic>' lists one, 'help <command>' explains one, " + "'help all' lists every one.")); } private void OneTopic(string topic, List<OutputLine> lines) { if (!Sort().TryGetValue(topic, out var value) || value.Count == 0) { lines.Add(OutputLine.Warn("help: nothing is filed under '" + topic + "' in this game")); return; } value.Sort(StringComparer.OrdinalIgnoreCase); lines.Add(OutputLine.Dim($"{topic} - {value.Count} command(s)")); foreach (string item in value) { CommandInfo commandInfo = _suggestions.Find(item); if (commandInfo != null) { lines.Add(OutputLine.Out(Row(commandInfo))); } } } private void Everything(int page, List<OutputLine> lines) { Dictionary<string, List<string>> dictionary = Sort(); List<List<OutputLine>> list = new List<List<OutputLine>>(); (string, string[])[] groups = HelpTopics.Groups; for (int i = 0; i < groups.Length; i++) { string item = groups[i].Item1; if (dictionary.TryGetValue(item, out var value)) { list.Add(TopicBlock(item, value)); } } foreach (KeyValuePair<string, List<string>> item2 in dictionary) { if (!HelpTopics.IsTopic(item2.Key) && !(item2.Key == "other")) { list.Add(TopicBlock(item2.Key, item2.Value)); } } if (dictionary.TryGetValue("terminal", out var value2)) { list.Add(TopicBlock("terminal", value2)); } if (dictionary.TryGetValue("other", out var value3)) { list.Add(TopicBlock("other", value3)); } List<List<OutputLine>> list2 = new List<List<OutputLine>> { new List<OutputLine>() }; foreach (List<OutputLine> item3 in list) { List<OutputLine> list3 = list2[list2.Count - 1]; if (list3.Count > 0 && list3.Count + item3.Count > 16) { list3 = new List<OutputLine>(); list2.Add(list3); } list3.AddRange(item3); } if (page > list2.Count) { page = list2.Count; } lines.AddRange(list2[page - 1]); lines.Add(OutputLine.Dim((page < list2.Count) ? $"page {page} of {list2.Count} - 'help all {page + 1}' for the next." : $"{Total()} commands in all.")); } private List<OutputLine> TopicBlock(string topic, List<string> words) { List<OutputLine> list = new List<OutputLine> { OutputLine.Dim(topic) }; words.Sort(StringComparer.OrdinalIgnoreCase); foreach (string word in words) { CommandInfo commandInfo = _suggestions.Find(word); if (commandInfo != null) { list.Add(OutputLine.Out(" " + Row(commandInfo))); } } return list; } private static void Wrap(string label, List<string> words, List<OutputLine> lines) { if (words.Count == 0) { return; } StringBuilder stringBuilder = new StringBuilder(Pad(label, 10)); int num = 10; foreach (string word in words) { if (num > 10 && num + 3 + word.Length > 96) { lines.Add(OutputLine.Out(stringBuilder.ToString())); stringBuilder = new StringBuilder(new string(' ', 10)); num = 10; } if (num > 10) { stringBuilder.Append(" "); num += 3; } stringBuilder.Append(word); num += word.Length; } lines.Add(OutputLine.Out(stringBuilder.ToString())); } private Dictionary<string, List<string>> Sort() { Dictionary<string, List<string>> dictionary = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase); foreach (CommandInfo command in _catalogue.Commands) { Put(dictionary, command.IsVanilla ? HelpTopics.TopicOf(command.Word) : Mod(command.Source), command.Word); } foreach (CommandInfo item in Catalogue) { if (!_suggestions.IsCommand(item.Word)) { Put(dictionary, "terminal", item.Word); } } return dictionary; } private static string Mod(string source) { if (string.IsNullOrEmpty(source)) { return "other"; } int num = source.LastIndexOf(" v", StringComparison.Ordinal); if (num <= 0) { return source; } return source.Substring(0, num); } private static void Put(Dictionary<string, List<string>> byTopic, string topic, string word) { if (!byTopic.TryGetValue(topic, out var value)) { value = (byTopic[topic] = new List<string>()); } value.Add(word); } private int Total() { int num = _catalogue.Commands.Count; foreach (CommandInfo item in Catalogue) { if (!_suggestions.IsCommand(item.Word)) { num++; } } return num; } private static void UsageLine(string word, List<OutputLine> lines) { foreach (CommandInfo item in Catalogue) { if (string.Equals(item.Word, word, StringComparison.OrdinalIgnoreCase)) { lines.Add(OutputLine.Dim("usage: " + item.Signature)); break; } } } private static string Fragment(string description) { if (string.IsNullOrEmpty(description)) { return ""; } string text = description.TrimEnd(); if (text.EndsWith(".", StringComparison.Ordinal) && !text.EndsWith("..", StringComparison.Ordinal)) { text = text.Substring(0, text.Length - 1); } if (text.Length > 1 && char.IsUpper(text[0]) && !char.IsUpper(text[1])) { text = char.ToLowerInvariant(text[0]) + text.Substring(1); } return text; } private static void Detail(CommandInfo command, List<OutputLine> lines) { lines.Add(OutputLine.Out(command.Signature)); if (command.Description.Length > 0) { lines.Add(OutputLine.Out(" " + Fragment(command.Description))); } if (command.Usage.Length > 0) { lines.Add(OutputLine.Dim(" e.g. " + command.Usage)); } lines.Add(OutputLine.Dim(" " + command.Source)); } private static string Row(CommandInfo command) { string value = ((command.Description.Length > 0) ? Fragment(command.Description) : command.Signature); return Pad(command.Word, 24) + Markup.Clip(value, 72); } private static IEnumerable<string> Grid(IReadOnlyList<CommandInfo> commands, IReadOnlyList<string> builtins) { List<string> words = new List<string>(); foreach (CommandInfo command in commands) { words.Add(command.Word); } List<string> list = new List<string>(builtins); list.Sort(StringComparer.OrdinalIgnoreCase); words.AddRange(list); for (int i = 0; i < words.Count; i += 5) { StringBuilder stringBuilder = new StringBuilder(); for (int j = 0; j < 5 && i + j < words.Count; j++) { stringBuilder.Append(Pad(words[i + j], 19)); } yield return stringBuilder.ToString().TrimEnd(); } } private void HistoryList(string query, List<OutputLine> lines) { List<string> list = _history.Search(query); if (list.Count == 0) { lines.Add(OutputLine.Dim((query.Length == 0) ? "history: nothing yet" : ("history: nothing matches '" + query + "'"))); return; } for (int num = list.Count - 1; num >= 0; num--) { lines.Add(OutputLine.Out(Pad((list.Count - num).ToString(), 6) + list[num])); } } private void Alias(string rest, List<string> tokens, List<OutputLine> lines) { if (rest.Length == 0) { if (_aliases.Count != 0) { foreach (KeyValuePair<string, string> item in _aliases.All) { lines.Add(OutputLine.Out(Pad(item.Key, 16) + item.Value)); } return; } lines.Add(OutputLine.Dim("no aliases yet - alias <name> <command> makes one")); } else if (tokens.Count < 3) { lines.Add(OutputLine.Error("alias: needs a name and a command")); UsageLine("alias", lines); lines.Add(OutputLine.Dim(" quote the command if it has spaces")); } else { string text = tokens[1]; string text2 = rest.Substring(text.Length).Trim(); if (!_aliases.TrySet(text, text2, _suggestions.IsKnown, out var error)) { lines.Add(OutputLine.Error(error)); } else { lines.Add(OutputLine.Out(text + " -> " + text2)); } } } private void Unalias(string rest, List<OutputLine> lines) { if (rest.Length == 0) { lines.Add(OutputLine.Error("unalias: needs a name")); UsageLine("unalias", lines); } else { lines.Add(_aliases.Remove(rest) ? OutputLine.Out(rest + " removed.") : OutputLine.Warn("unalias: no alias called '" + rest + "'")); } } private void Grep(string pattern, List<OutputLine> lines) { if (pattern.Length == 0) { lines.Add(OutputLine.Error("grep: needs something to look for")); UsageLine("grep", lines); return; } List<OutputLine> list = new List<OutputLine>(); foreach (OutputLine line in _transcript.Lines) { if (line.Text.IndexOf(pattern, StringComparison.OrdinalIgnoreCase) >= 0) { list.Add(line); } } if (list.Count == 0) { lines.Add(OutputLine.Dim("grep: nothing on screen matches '" + pattern + "'")); return; } lines.Add(OutputLine.Dim($"{list.Count} line(s) match '{pattern}':")); lines.AddRange(list); } private void Copy(string rest, List<OutputLine> lines) { string text = rest; if (text.Length == 0) { foreach (OutputLine item in _transcript.Recent(40)) { if (item.Kind != LineKind.Dim && item.Kind != LineKind.Echo) { text = item.Text.Trim(); break; } } } if (string.IsNullOrEmpty(text)) { lines.Add(OutputLine.Warn("copy: nothing on screen to copy")); return; } PendingClipboard = text; lines.Add(OutputLine.Dim("Copied: " + text)); } private void Typeface(string rest, List<OutputLine> lines) { string text = (rest ?? "").Trim().ToLowerInvariant(); if (text.Length == 0) { lines.Add(OutputLine.Out("font: " + Face)); lines.Add(OutputLine.Dim("mono the machine's own monospaced font, easier to read")); lines.Add(OutputLine.Dim("pixel the game's own face, smaller and denser")); } else if (text != "mono" && text != "pixel") { lines.Add(OutputLine.Error("font: '" + rest.Trim() + "' is not a face")); lines.Add(OutputLine.Dim("try 'mono' or 'pixel'")); } else { Face = text; lines.Add(OutputLine.Dim("font: " + Face)); } } private void Logs(string rest, List<OutputLine> lines) { if (string.Equals(rest, "off", StringComparison.OrdinalIgnoreCase)) { LogsOpen = false; LogsFilter = ""; lines.Add(OutputLine.Dim("Log view off.")); } else { LogsOpen = true; LogsFilter = rest; lines.Add(OutputLine.Dim((rest.Length == 0) ? "Log view on - everything the game logs. 'logs warn' filters, 'logs off' stops." : ("Log view on, filtered to '" + rest + "'. 'logs off' stops."))); } } internal static string Pad(string value, int width) { if (value == null) { value = ""; } if (value.Length < width) { return value.PadRight(width); } return value + " "; } } public static class CommandLine { public readonly struct Plan { public IReadOnlyList<string> Commands { get; } public string Error { get; } public bool Failed => Error != null; internal Plan(IReadOnlyList<string> commands, string error) { Commands = commands ?? Array.Empty<string>(); Error = error; } } public const string RawWord = "raw"; public const string RepeatWord = "repeat"; public const int MaxRepeat = 100; private static readonly (string Word, int OwnArguments, bool RunsNow)[] TakeACommand = new(string, int, bool)[4] { ("raw", 0, true), ("repeat", 1, true), ("alias", 1, false), ("bind", 1, false) }; public static Plan Parse(string line, Func<string, string> expandAlias = null) { if (string.IsNullOrWhiteSpace(line)) { return new Plan(Array.Empty<string>(), null); } string text = line.Trim(); if (StartsWithWord(text, "raw", out var rest)) { if (rest.Length != 0) { return new Plan(new string[1] { rest }, null); } return new Plan(null, "raw: no command after it\nusage: raw <command>"); } if (!NeedsShell(text)) { string text2 = ExpandFirstWord(text, expandAlias); return Split(text2, expandAlias, (object)text2 != text); } return Split(text, expandAlias, alreadyExpanded: false); } public static bool NeedsShell(string line) { if (line != null) { if (line.IndexOf('"') < 0) { return line.IndexOf(';') >= 0; } return true; } return false; } private static Plan Split(string line, Func<string, string> expandAlias, bool alreadyExpanded) { if (!TrySplitStatements(line, out var statements, out var error)) { return new Plan(null, error); } List<string> list = new List<string>(); foreach (string item in statements) { string text = item.Trim(); if (text.Length == 0) { continue; } if (StartsWithWord(text, "raw", out var rest)) { if (rest.Length == 0) { return new Plan(null, "raw: no command after it\nusage: raw <command>"); } list.Add(rest); continue; } if (!alreadyExpanded) { text = ExpandFirstWord(text, expandAlias); } if (Expand(text, list, out error)) { continue; } return new Plan(null, error); } return new Plan(list, null); } private static bool Expand(string statement, List<string> into, out string error) { error = null; if (!StartsWithWord(statement, "repeat", out var rest)) { into.Add(statement); return true; } int num = rest.IndexOf(' '); string text = ((num < 0) ? rest : rest.Substring(0, num)); string text2 = ((num < 0) ? "" : rest.Substring(num + 1).Trim()); if (!int.TryParse(text, out var result)) { error = "repeat: '" + text + "' is not a number\nusage: repeat <count> <command>"; return false; } if (result < 1) { error = "repeat: the count has to be 1 or more\nusage: repeat <count> <command>"; return false; } if (result > 100) { error = $"repeat: {100} is the most that can run in one frame"; return false; } if (text2.Length == 0) { error = "repeat: no command after the count\nusage: repeat <count> <command>"; return false; } if (StartsWithWord(text2, "raw", out var rest2)) { if (rest2.Length == 0) { error = "raw: no command after it\nusage: raw <command>"; return false; } text2 = rest2; } List<string> list = new List<string>(); for (int i = 0; i < result; i++) { if (!Expand(text2, list, out error)) { return false; } if (list.Count > 100) { error = $"repeat: that comes to more than {100} commands, and they all run in one frame"; return false; } } into.AddRange(list); return true; } private static bool TrySplitStatements(string line, out List<string> statements, out string error) { statements = new List<string>(); error = null; StringBuilder stringBuilder = new StringBuilder(); bool flag = false; bool flag2 = true; for (int i = 0; i < line.Length; i++) { char c = line[i]; if (flag2 && c != ' ') { flag2 = false; if (SuspendsSplitting(line.Substring(i))) { statements.Add(stringBuilder.Append(line.Substring(i)).ToString()); return true; } } switch (c) { case '"': if (flag && i + 1 < line.Length && line[i + 1] == '"') { stringBuilder.Append('"'); i++; } else { flag = !flag; } continue; case ';': if (!flag) { statements.Add(stringBuilder.ToString()); stringBuilder.Clear(); flag2 = true; continue; } break; } stringBuilder.Append(c); } if (flag) { error = "a quote was opened and never closed."; statements = null; return false; } statements.Add(stringBuilder.ToString()); return true; } private static bool SuspendsSplitting(string rest) { if (StartsWithWord(rest, "raw", out var rest2)) { return true; } if (!StartsWithWord(rest, "repeat", out var rest3)) { return false; } int num = rest3.IndexOf(' '); if (num <= 0) { return false; } if (!int.TryParse(rest3.Substring(0, num), out var _)) { return false; } return StartsWithWord(rest3.Substring(num + 1).TrimStart(), "raw", out rest2); } private static string ExpandFirstWord(string statement, Func<string, string> expandAlias) { if (expandAlias == null) { return statement; } int num = statement.IndexOf(' '); string arg = ((num < 0) ? statement : statement.Substring(0, num)); string text = expandAlias(arg); if (string.IsNullOrEmpty(text)) { return statement; } if (num >= 0) { return text + statement.Substring(num); } return text; } public static string Unwrap(string statement) { if (string.IsNullOrEmpty(statement)) { return statement ?? ""; } for (int i = 0; i < 4; i++) { string text = statement.TrimStart(); int num = WrapperWidth(text); if (num <= 0) { return statement; } statement = text.Substring(num); } return statement; } public static int StoredCommandAt(string statement) { if (string.IsNullOrEmpty(statement)) { return -1; } string line = statement.TrimStart(); int at = 0; string a = ReadWord(line, ref at); (string, int, bool)[] takeACommand = TakeACommand; for (int i = 0; i < takeACommand.Length; i++) { (string, int, bool) tuple = takeACommand[i]; var (b, num, _) = tuple; if (!tuple.Item3 && string.Equals(a, b, StringComparison.OrdinalIgnoreCase)) { return num + 1; } } return -1; } private static int WrapperWidth(string statement) { int i = 0; string a = ReadWord(statement, ref i); int num = -1; (string, int, bool)[] takeACommand = TakeACommand; for (int j = 0; j < takeACommand.Length; j++) { var (b, num2, _) = takeACommand[j]; if (string.Equals(a, b, StringComparison.OrdinalIgnoreCase)) { num = num2; } } if (num < 0) { return 0; } for (int k = 0; k <= num; k++) { if (i >= statement.Length || statement[i] != ' ') { return 0; } for (; i < statement.Length && statement[i] == ' '; i++) { } if (k < num && ReadWord(statement, ref i).Length == 0) { return 0; } } if (i < statement.Length && (statement[i] == '"' || statement[i] == '\'')) { i++; } return i; } private static string ReadWord(string line, ref int at) { int num = at; while (at < line.Length && line[at] != ' ') { at++; } return line.Substring(num, at - num); } public static bool StartsWithWord(string line, string word, out string rest) { rest = null; if (line == null || word == null) { return false; } if (!line.StartsWith(word, StringComparison.OrdinalIgnoreCase)) { return false; } if (line.Length == word.Length) { rest = ""; return true; } if (line[word.Length] != ' ') { return false; } rest = line.Substring(word.Length + 1).Trim(); return true; } public static List<string> Tokenise(string command) { List<string> list = new List<string>(); if (string.IsNullOrEmpty(command)) { return list; } StringBuilder stringBuilder = new StringBuilder(); bool flag = false; bool flag2 = false; foreach (char c in command) { switch (c) { case '"': flag = !flag; flag2 = true; continue; case ' ': if (!flag) { if (flag2) { list.Add(stringBuilder.ToString()); stringBuilder.Clear(); flag2 = false; } continue; } break; } stringBuilder.Append(c); flag2 = true; } if (flag2) { list.Add(stringBuilder.ToString()); } return list; } public static void TokenAtCaret(string line, out int index, out string prefix) { index = 0; prefix = ""; if (string.IsNullOrEmpty(line)) { return; } bool flag = false; StringBuilder stringBuilder = new StringBuilder(); bool flag2 = false; foreach (char c in line) { switch (c) { case '"': flag = !flag; flag2 = true; continue; case ' ': if (!flag) { if (flag2) { index++; stringBuilder.Clear(); flag2 = false; } continue; } break; } stringBuilder.Append(c); flag2 = true; } prefix = stringBuilder.ToString(); } public static string ReplaceTokenAtCaret(string line, string value, bool trailingSpace) { string text = ((value != null && value.IndexOf(' ') >= 0) ? ("\"" + value + "\"") : (value ?? "")); int length = line?.Length ?? 0; if (line != null) { bool flag = false; for (int num = line.Length - 1; num >= 0; num--) { if (line[num] == '"') { flag = !flag; } if (line[num] == ' ' && !flag) { length = num + 1; break; } if (num == 0) { length = 0; } } } return ((line == null) ? "" : line.Substring(0, length)) + text + (trailingSpace ? " " : ""); } } public enum MatchKind { None, Subsequence, Substring, WordStart, Prefix, Exact } public readonly struct MatchResult { public static readonly MatchResult NoMatch = new MatchResult(MatchKind.None, int.MaxValue); public MatchKind Kind { get; } public int Offset { get; } public bool IsMatch => Kind != MatchKind.None; public int Score { get { if (Kind != MatchKind.None) { return (int)Kind * 1000 - Math.Min(Offset, 999); } return 0; } } internal MatchResult(MatchKind kind, int offset) { Kind = kind; Offset = offset; } } public static class FuzzyMatcher { private const int MinSubsequenceQuery = 2; private static readonly char[] WordSeparators = new char[6] { '_', '-', '.', ' ', '/', ':' }; public static MatchResult Match(string candidate, string query) { if (string.IsNullOrEmpty(candidate)) { return MatchResult.NoMatch; } if (string.IsNullOrEmpty(query)) { return new MatchResult(MatchKind.Prefix, 0); } if (candidate.Equals(query, StringComparison.OrdinalIgnoreCase)) { return new MatchResult(MatchKind.Exact, 0); } if (candidate.StartsWith(query, StringComparison.OrdinalIgnoreCase)) { return new MatchResult(MatchKind.Prefix, 0); } int num = candidate.IndexOf(query, StringComparison.OrdinalIgnoreCase); if (num > 0) { return new MatchResult(IsWordStart(candidate, num) ? MatchKind.WordStart : MatchKind.Substring, num); } if (query.Length >= 2 && TryMatchSubsequence(candidate, query, out var firstIndex)) { return new MatchResult(MatchKind.Subsequence, firstIndex); } return MatchResult.NoMatch; } public static bool IsMatch(string candidate, string query) { return Match(candidate, query).IsMatch; } private static bool IsWordStart(string candidate, int index) { if (index <= 0) { return true; } char c = candidate[index - 1]; for (int i = 0; i < WordSeparators.Length; i++) { if (c == WordSeparators[i]) { return true; } } if (char.IsLower(c)) { return char.IsUpper(candidate[index]); } return false; } private static bool TryMatchSubsequence(string candidate, string query, out int firstIndex) { firstIndex = -1; int num = 0; for (int i = 0; i < candidate.Length; i++) { if (num >= query.Length) { break; } if (char.ToLowerInvariant(candidate[i]) == char.ToLowerInvariant(query[num])) { if (num == 0) { firstIndex = i; } num++; } } if (num < query.Length) { firstIndex = -1; return false; } return true; } } public static class HelpTopics { public const string Other = "other"; public const string Terminal = "terminal"; public static readonly (string Topic, string[] Words)[] Groups = new(string, string[])[13] { ("items", new string[7] { "give", "setdiscovered", "setquality", "setquantity", "packageproduct", "clearinventory", "growplants" }), ("money", new string[3] { "changecash", "changebalance", "addxp" }), ("world", new string[8] { "settime", "setdayduration", "settimescale", "setweather", "triggerlightning", "triggerdistantthunder", "cleartrash", "forcesleep" }), ("places", new string[4] { "teleport", "setowned", "setregionunlocked", "spawnvehicle" }), ("people", new string[7] { "setunlocked", "setrelationship", "addemployee", "setemotion", "destroynpcs", "disablenpcs", "disablenpcasset" }), ("police", new string[5] { "raisewanted", "lowerwanted", "clearwanted", "setlawintensity", "setpoliceignoreplayers" }), ("player", new string[6] { "sethealth", "setstaminareserve", "setmovespeed", "setjumpforce", "setgravitymultiplier", "freecam" }), ("quests", new string[5] { "setqueststate", "setquestentrystate", "setvar", "endtutorial", "playcutscene" }), ("keys", new string[3] { "bind", "unbind", "clearbinds" }), ("display", new string[5] { "hideui", "showfps", "hidefps", "enable", "disable" }), ("graphics", new string[9] { "enableinstancing", "disableinstancing", "enableocclusionculling", "disableocclusionculling", "enablephysics", "disablephysics", "enableterrain", "disableterrain", "disablemeshes" }), ("game", new string[2] { "save", "quit" }), ("dev", new string[2] { "npcworkbench", "presentationworkbench" }) }; public static readonly string[] Common = new string[6] { "give", "teleport", "settime", "changecash", "spawnvehicle", "setweather" }; private static readonly Dictionary<string, string> ByWord = Index(); public static bool IsTopic(string name) { if (string.IsNullOrEmpty(name)) { return false; } (string, string[])[] groups = Groups; for (int i = 0; i < groups.Length; i++) { if (string.Equals(groups[i].Item1, name, StringComparison.OrdinalIgnoreCase)) { return true; } } return string.Equals(name, "terminal", StringComparison.OrdinalIgnoreCase); } public static string TopicOf(string word) { if (word == null || !ByWord.TryGetValue(word, out var value)) { return "other"; } return value; } private static Dictionary<string, string> Index() { Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); (string, string[])[] groups = Groups; for (int i = 0; i < groups.Length; i++) { (string, string[]) tuple = groups[i]; string item = tuple.Item1; string[] item2 = tuple.Item2; foreach (string key in item2) { dictionary[key] = item; } } return dictionary; } } public sealed class History { private const string FileName = "history.txt"; public const int Capacity = 500; private readonly List<string> _lines = new List<string>(); private bool _dirty; public IReadOnlyList<string> Lines => _lines; public int Count => _lines.Count; public void Add(string line) { string one = line?.Trim(); if (!string.IsNullOrEmpty(one)) { _lines.RemoveAll((string existing) => string.Equals(existing, one, StringComparison.Ordinal)); _lines.Insert(0, one); if (_lines.Count > 500) { _lines.RemoveRange(500, _lines.Count - 500); } _dirty = true; } } public List<string> Search(string query) { List<string> list = new List<string>(); if (string.IsNullOrEmpty(query)) { list.AddRange(_lines); return list; } foreach (string line in _lines) { if (line.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0) { list.Add(line); } } return list; } public string ReverseSearch(string query, int skip) { if (skip < 0) { return null; } List<string> list = Search(query); if (skip >= list.Count) { return null; } return list[skip]; } public void Clear() { if (_lines.Count != 0) { _lines.Clear(); _dirty = true; } } public void Load(IStore store) { _lines.Clear(); _dirty = false; string text = store?.Read(StoreScope.Global, "history.txt"); if (string.IsNullOrEmpty(text)) { return; } string[] array = text.Split('\n'); for (int i = 0; i < array.Length; i++) { string text2 = array[i].Trim('\r'); if (text2.Length != 0) { _lines.Add(text2); if (_lines.Count >= 500) { break; } } } } public void Save(IStore store) { if (!_dirty || store == null) { return; } StringBuilder stringBuilder = new StringBuilder(); foreach (string line in _lines) { stringBuilder.Append(line).Append('\n'); } store.Write(StoreScope.Global, "history.txt", stringBuilder.ToString()); _dirty = false; } } public readonly struct Expansion { public string Line { get; } public string Error { get; } public bool Failed => Error != null; internal Expansion(string line, string error) { Line = line ?? ""; Error = error; } } public sealed class MarkExpansion { private readonly Marks _marks; private readonly ICommandCatalogue _catalogue; public MarkExpansion(Marks marks, ICommandCatalogue catalogue) { _marks = marks; _catalogue = catalogue; } public Expansion Apply(string statement) { if (string.IsNullOrEmpty(statement) || statement.IndexOf('#') < 0) { return new Expansion(statement, null); } List<string> list = CommandLine.Tokenise(statement); if (list.Count == 0) { return new Expansion(statement, null); } string text = list[0]; StringBuilder stringBuilder = new StringBuilder(text); int num = CommandLine.StoredCommandAt(statement); for (int i = 1; i < list.Count; i++) { string text2 = list[i]; stringBuilder.Append(' '); if (num >= 0 && i >= num) { stringBuilder.Append(Quote(text2)); continue; } if (!Marks.IsWord(text2)) { stringBuilder.Append(Quote(text2)); continue; } Mark mark = _marks.Resolve(text2); int argIndex = i - 1; if (!mark.Exists) { return new Expansion(null, Unmarked(text2)); } MarkKind wanted = _catalogue.KindOf(text, argIndex); if (!Fits(mark.Kind, wanted)) { return new Expansion(null, Mismatch(text, text2, mark, wanted)); } stringBuilder.Append(Quote(mark.Id)); } return new Expansion(stringBuilder.ToString(), null); } private static bool Fits(MarkKind have, MarkKind wanted) { if (wanted == MarkKind.None) { return false; } if (have == MarkKind.Any || wanted == MarkKind.Any) { return true; } return have == wanted; } private static string Unmarked(string word) { if (!(word == "#")) { return word + ": nothing there right now"; } return "#: nothing marked - look at something before opening the terminal"; } private static string Mismatch(string command, string word, Mark mark, MarkKind wanted) { string text = $"{word}: marked {Mark.Word(mark.Kind)} '{mark.Id}'"; if (wanted != MarkKind.None) { return text + $"\n{command} needs {AnA(Mark.Word(wanted))} there"; } return text + "\n" + command + " does not take one there"; } private static string AnA(string word) { if (word.Length == 0) { return word; } if (string.Equals(word, "npc", StringComparison.OrdinalIgnoreCase)) { return "an " + word; } if ("aeiou".IndexOf(char.ToLowerInvariant(word[0])) < 0) { return "a " + word; } return "an " + word; } private static string Quote(string value) { if (value.IndexOf(' ') < 0) { return value; } return "\"" + value + "\""; } } public enum MarkKind { None, Npc, Vehicle, Property, Item, Any } public readonly struct Mark { public static readonly Mark None = new Mark(MarkKind.None, "", ""); public MarkKind Kind { get; } public string Id { get; } public string Label { get; } public bool Exists { get { if (Kind != MarkKind.None) { return Id.Length > 0; } return false; } } public Mark(MarkKind kind, string id, string label) { Kind = kind; Id = id ?? ""; Label = label ?? ""; } public override string ToString() { if (!Exists) { return "nothing"; } return Id + " (" + Word(Kind) + ")"; } internal static string Word(MarkKind kind) { return kind switch { MarkKind.Npc => "npc", MarkKind.Vehicle => "vehicle", MarkKind.Property => "property", MarkKind.Item => "item", MarkKind.Any => "id", _ => "nothing", }; } } public interface IMarks { Mark Looked { get; } Mark Hand { get; } Mark Here { get; } Mark Car { get; } Mark Home { get; } Mark Near { get; } } public sealed class Marks { public const char Sigil = '#'; private readonly IMarks _world; private string _lastArgument = ""; private string _lastPrinted = ""; public static readonly string[] Words = new string[8] { "#", "#hand", "#here", "#last", "#it", "#car", "#home", "#near" }; public Marks(IMarks world) { _world = world; } public Mark Resolve(string word) { if (string.IsNullOrEmpty(word) || word[0] != '#') { return Mark.None; } return word.ToLowerInvariant() switch { "#" => _world?.Looked ?? Mark.None, "#hand" => _world?.Hand ?? Mark.None, "#here" => _world?.Here ?? Mark.None, "#car" => _world?.Car ?? Mark.None, "#home" => _world?.Home ?? Mark.None, "#near" => _world?.Near ?? Mark.None, "#last" => Free(_lastArgument), "#it" => Free(_lastPrinted), _ => Mark.None, }; } public static bool IsWord(string word) { if (string.IsNullOrEmpty(word) || word[0] != '#') { return false; } string[] words = Words; for (int i = 0; i < words.Length; i++) { if (string.Equals(words[i], word, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } public void Ran(string line) { List<string> list = CommandLine.Tokenise(line ?? ""); _lastArgument = ((list.Count > 1) ? list[list.Count - 1] : ""); } public void Printed(string text) { string text2 = (text ?? "").Trim(); if (text2.Length != 0 && text2.IndexOf(' ') < 0) { _lastPrinted = text2; } } private static Mark Free(string id) { if (!string.IsNullOrEmpty(id)) { return new Mark(MarkKind.Any, id, id); } return Mark.None; } } public static class Markup { private const int ValueColumn = 24; private const int SourceColumn = 30; private const int LineWidth = 88; public static string Suggestions(SuggestionSet set, int selected, int window, bool expanded) { if (set == null) { return ""; } if (set.Command == null && !set.Any) { return ""; } StringBuilder stringBuilder = new StringBuilder(); if (set.Command != null) { Signature(stringBuilder, set.Command.Signature, set.ArgIndex); string value = ((set.Command.Description.Length > 0) ? set.Command.Description : "No description."); Line(stringBuilder, "desc", Clip(value, 88 - set.Command.Source.Length - 2) + " " + set.Command.Source); } if (!expanded) { if (set.Any) { Line(stringBuilder, "ghost", Pad("tab " + set.Rows[Math.Min(Math.Max(selected, 0), set.Rows.Count - 1)].Value, 32) + ((set.Rows.Count > 1) ? $"up/down browse {set.Rows.Count}" : "")); } return stringBuilder.ToString(); } if (!set.Any) { return stringBuilder.ToString(); } int count = set.Rows.Count; int num = Math.Min(8, count); int num2 = Math.Max(0, Math.Min(window, count - num)); int num3 = num2 + num; Line(stringBuilder, "rule", Rule(num2, num3, count)); for (int i = num2; i < num3; i++) { Suggestion suggestion = set.Rows[i]; bool flag = i == selected; StringBuilder stringBuilder2 = new StringBuilder(); stringBuilder2.Append(flag ? "> " : " "); stringBuilder2.Append(Pad(Clip(suggestion.Value, 24), 28)); if (stringBuilder.Length > 0) { stringBuilder.Append("<br>"); } Span(stringBuilder, flag ? "pick" : null, stringBuilder2.ToString()); Span(stringBuilder, (suggestion.Kind == SuggestionKind.History) ? "src-history" : (suggestion.IsVanilla ? "src-game" : "src-mod"), suggestion.Source); } return stringBuilder.ToString(); } private static void Signature(StringBuilder sb, string signature, int argIndex) { if (string.IsNullOrEmpty(signature)) { return; } List<string> list = new List<string>(signature.Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries)); int num = argIndex + 1; if (num <= 0 || num >= list.Count) { Span(sb, "sig", signature); return; } Span(sb, "sig", string.Join(" ", list.GetRange(0, num)) + " "); Span(sb, "cur", list[num]); if (num + 1 < list.Count) { Span(sb, "sig", " " + string.Join(" ", list.GetRange(num + 1, list.Count - num - 1))); } } private static string Rule(int first, int last, int count) { if (count <= 8) { return new string('-', 88); } string text = $" {first + 1}-{last} of {count} "; int count2 = Math.Max(4, 88 - text.Length); return new string('-', count2) + text; } public static string Transcript(IReadOnlyList<OutputLine> lines) { if (lines == null || lines.Count == 0) { return ""; } StringBuilder stringBuilder = new StringBuilder(); foreach (OutputLine line in lines) { if (stringBuilder.Length > 0) { stringBuilder.Append("<br>"); } Span(stringBuilder, Class(line.Kind), line.Text); } return stringBuilder.ToString(); } private static string Class(LineKind kind) { return kind switch { LineKind.Echo => "echo", LineKind.Warn => "warn", LineKind.Error => "err", LineKind.Dim => "dim", _ => null, }; } private static void Line(StringBuilder sb, string cls, string text) { if (sb.Length > 0) { sb.Append("<br>"); } Span(sb, cls, text); } private static void Span(StringBuilder sb, string cls, string text) { if (!string.IsNullOrEmpty(text)) { if (cls == null) { sb.Append(Escape(text)); } else { sb.Append("<span class=\"").Append(cls).Append("\">") .Append(Escape(text)) .Append("</span>"); } } } public static string Escape(string text) { if (string.IsNullOrEmpty(text)) { return ""; } if (text.IndexOf('&') < 0 && text.IndexOf('<') < 0 && text.IndexOf('>') < 0) { return text; } return text.Replace("&", "&").Replace("<", "<").Replace(">", ">"); } internal static string Pad(string value, int width) { if (value == null) { value = ""; } if (value.Length < width) { return value.PadRight(width); } return value + " "; } internal static string Clip(string value, int width) { if (value == null) { return ""; } if (width < 4 || value.Length <= width) { return value; } return value.Substring(0, width - 2) + ".."; } } public sealed class CommandInfo { public string Word { get; } public string Description { get; } public string Usage { get; } public string Signature { get; } public string Source { get; } public bool IsVanilla { get; } public CommandInfo(string word, string description, string usage, string signature, string source, bool isVanilla) { Word = word ?? ""; Description = description ?? ""; Usage = usage ?? ""; Signature = signature ?? ""; Source = source ?? ""; IsVanilla = isVanilla; } } public readonly struct ArgValue { public string Value { get; } public string Source { get; } public bool IsVanilla { get; } public ArgValue(string value, string source, bool isVanilla) { Value = value ?? ""; Source = source ?? ""; IsVanilla = isVanilla; } } public enum LineKind { Echo, Out, Warn, Error, Dim } public readonly struct OutputLine { public LineKind Kind { get; } public string Text { get; } public OutputLine(LineKind kind, string text) { Kind = kind; Text = text ?? ""; } public static OutputLine Echo(string text) { return new OutputLine(LineKind.Echo, text); } public static OutputLine Out(string text) { return new OutputLine(LineKind.Out, text); } public static OutputLine Warn(string text) { return new OutputLine(LineKind.Warn, text); } public static OutputLine Error(string text) { return new OutputLine(LineKind.Error, text); } public static OutputLine Dim(string text) { return new OutputLine(LineKind.Dim, text); } } public enum SuggestionKind { Command, Argument, History } public readonly struct Suggestion { public SuggestionKind Kind { get; } public string Value { get; } public string Source { get; } public bool IsVanilla { get; } public int Score { get; } public Suggestion(SuggestionKind kind, string value, string source, bool isVanilla, int score) { Kind = kind; Value = value ?? ""; Source = source ?? ""; IsVanilla = isVanilla; Score = score; } } public interface ICommandCatalogue { IReadOnlyList<CommandInfo> Commands { get; } IReadOnlyList<ArgValue> ValuesFor(string command, int argIndex); MarkKind KindOf(string command, int argIndex); bool Owns(string command, int argIndex); } public interface ICommandRunner { bool CanRun { get; } string RefusalReason { get; } IReadOnlyList<OutputLine> Run(string line); } public interface IStore { string Read(StoreScope scope, string name); void Write(StoreScope scope, string name, string content); } public enum StoreScope { Save, Global } public interface IClock { string Now { get; } } public sealed class NavResult { public string Line { get; internal set; } public string Suggest { get; internal set; } = ""; public string Ghost { get; internal set; } = ""; } public sealed class RunResult { public IReadOnlyList<OutputLine> Lines { get; internal set; } = Array.Empty<OutputLine>(); public string Clipboard { get; internal set; } public bool Cleared { get; internal set; } } public sealed class Session { private readonly ICommandCatalogue _catalogue; private readonly ICommandRunner _runner; private readonly Suggestions _suggestions; private readonly Builtins _builtins; private readonly Transcript _transcript; private readonly History _history; private readonly Aliases _aliases; private readonly Usage _usage; private readonly Marks _marks; private readonly MarkExpansion _expansion; private SuggestionSet _current = SuggestionSet.Empty; private int _selected; private int _window; private bool _expanded; private int _searchSkip; private string _searchQuery = ""; public Transcript Transcript => _transcript; public Marks Marks => _marks; public Builtins Builtins => _builtins; public bool Locked => !_runner.CanRun; public int CommandCount { get { int num = _catalogue.Commands.Count; foreach (CommandInfo item in Builtins.Catalogue) { if (!_suggestions.IsCommand(item.Word)) { num++; } } return num; } } public Session(ICommandCatalogue catalogue, ICommandRunner runner, Usage usage, History history, Aliases aliases, IMarks marks = null) { _catalogue = catalogue; _runner = runner; _usage = usage; _history = history; _aliases = aliases; _marks = new Marks(marks); _expansion = new MarkExpansion(_marks, catalogue); _transcript = new Transcript(); _suggestions = new Suggestions(catalogue, usage, history, aliases, _marks); _builtins = new Builtins(_suggestions, catalogue, history, aliases, _transcript); } public IReadOnlyList<OutputLine> Banner(string identity) { List<OutputLine> list = new List<OutputLine>(); if (!string.IsNullOrEmpty(identity)) { list.Add(OutputLine.Out(identity)); } list.Add(OutputLine.Dim($"{CommandCount} commands loaded. Type 'help' for the list.")); if (Locked) { list.Add(OutputLine.Error(_runner.RefusalReason + " Lookups still work.")); } return list; } public NavResult Typed(string line) { _searchSkip = 0; if (string.IsNullOrWhiteSpace(line)) { _expanded = false; } return Offer(line, resetSelection: true); } public NavResult Navigate(string line, string action) { return (action ?? "").ToLowerInvariant() switch { "typed" => Typed(line), "accept" => Accept(line), "up" => Move(line, -1), "down" => Move(line, 1), "pageup" => Move(line, -8), "pagedown" => Move(line, 8), "search" => ReverseSearch(line), _ => Offer(line, resetSelection: false), }; } private NavResult Move(string line, int by) { if (_current.Rows.Count == 0) { _current = _suggestions.For(line); _selected = 0; _window = 0; } if (!_current.Any) { return Draw(line); } if (!_expanded) { _expanded = true; _selected = ((by < 0) ? (_current.Rows.Count - 1) : 0); ScrollToSelection(); return Draw(line); } int count = _current.Rows.Count; _selected = ((_selected + by) % count + count) % count; ScrollToSelection(); return Draw(line); } private NavResult Recall(string line) { _current = SuggestionSet.Empty; _selected = 0; _window = 0; _expanded = false; return new NavResult { Line = line }; } private NavResult ReverseSearch(string line) { if (_searchSkip == 0) { _searchQuery = (line ?? "").Trim(); } string text = _history.ReverseSearch(_searchQuery, _searchSkip); if (text == null) { _searchSkip = 0; return Draw(line); } _searchSkip++; return Recall(text); } private NavResult Accept(string line) { if (_current.Rows.Count == 0) { _current = _suggestions.For(line); _selected = 0; _window = 0; } if (!_current.Any) { return Draw(line); } Suggestion suggestion = _current.Rows[Math.Min(_selected, _current.Rows.Count - 1)]; if (suggestion.Kind == SuggestionKind.History) { return Offer(suggestion.Value, resetSelection: true, suggestion.Value); } bool trailingSpace = suggestion.Kind == SuggestionKind.Command || MoreArgumentsAfter(); string text = CommandLine.ReplaceTokenAtCaret(line, suggestion.Value, trailingSpace); return Offer(text, resetSelection: true, text); } private bool MoreArgumentsAfter() { if (_current.Command == null || _current.ArgIndex < 0) { return false; } return _current.ArgIndex + 1 < UsageExample.ArgumentCount(_current.Command.Signature); } public RunResult Run(string line) { RunResult runResult = new RunResult(); List<OutputLine> list = new List<OutputLine>(); string line2 = (line ?? "").Trim(); if (line2.Length == 0) { runResult.Lines = list; return runResult; } _current = SuggestionSet.Empty; _searchSkip = 0; _expanded = false; if (!Expand(ref line2, list)) { runResult.Lines = list; return runResult; } Echo(line2, list); _history.Add(line2); CommandLine.Plan plan = CommandLine.Parse(line2, _aliases.Expand); if (plan.Failed) { string[] array = plan.Error.Split('\n'); Emit(OutputLine.Error(array[0]), list); for (int i = 1; i < array.Length; i++) { Emit(OutputLine.Dim(array[i]), list); } runResult.Lines = list; return runResult; } int count = _transcript.Count; List<string> list2 = new List<string>(); foreach (string command in plan.Commands) { Expansion expansion = _expansion.Apply(command); if (expansion.Failed) { string[] array2 = expansion.Error.Split('\n'); foreach (string text in array2) { Emit(OutputLine.Error(text), list); } runResult.Lines = list; return runResult; } list2.Add(expansion.Line); } _marks.Ran(line2); foreach (string item in list2) { RunOne(item, list); } runResult.Cleared = _transcript.Count < count; runResult.Lines = list; runResult.Clipboard = _builtins.TakeClipboard(); return runResult; } private bool Expand(ref string line, List<OutputLine> lines) { if (line.Length < 2 || line[0] != '!') { return true; } string text = line.Substring(1); string text2; if (text == "!") { text2 = ((_history.Count > 0) ? _history.Lines[0] : null); if (text2 == null) { Emit(OutputLine.Warn("No previous command."), lines); return false; } } else { text2 = null; foreach (string line2 in _history.Lines) { if (line2.StartsWith(text, StringComparison.OrdinalIgnoreCase)) { text2 = line2; break; } } if (text2 == null) { Emit(OutputLine.Warn("Nothing in history starts with '" + text + "'."), lines); return false; } } line = text2; return true; } private void RunOne(string command, List<OutputLine> lines) { if (_builtins.TryRun(command, out var output)) { foreach (OutputLine item in output) { Emit(item, lines); } return; } if (Locked) { Emit(OutputLine.Error(_runner.RefusalReason), lines); return; } _usage.Record(CommandLine.Tokenise(command)); foreach (OutputLine item2 in _runner.Run(command)) { Emit(item2, lines); } } private void Emit(OutputLine line, List<OutputLine> into) { _transcript.Add(line); into.Add(line); if (line.Kind == LineKind.Out) { _marks.Printed(line.Text); } } private void Echo(string typed, List<OutputLine> lines) { Emit(OutputLine.Echo("$ " + typed), lines); } public void Push(OutputLine line) { _transcript.Add(line); } private NavResult Offer(string line, bool resetSelection, string forceLine = null) { _current = _suggestions.For(line); if (resetSelection) { _selected = 0; } if (_selected >= _current.Rows.Count) { _selected = 0; } if (resetSelection) { _window = 0; } ScrollToSelection(); NavResult navResult = Draw(line); navResult.Line = forceLine; return navResult; } private void ScrollToSelection() { int count = _current.Rows.Count; int num = 8; if (count <= num) { _window = 0; return; } if (_selected < _window) { _window = _selected; } else if (_selected >= _window + num) { _window = _selected - num + 1; } if (_window > count - num) { _window = count - num; } if (_window < 0) { _window = 0; } } private NavResult Draw(string line) { return new NavResult { Suggest = Markup.Suggestions(_current, _selected, _window, _expanded), Ghost = Ghost(line) }; } private string Ghost(string line) { if (!_current.Any) { return ""; } Suggestion suggestion = _current.Rows[Math.Min(_selected, _current.Rows.Count - 1)]; if (suggestion.Kind == SuggestionKind.History) { string text = line ?? ""; if (text.Length == 0) { if (!_expanded) { return ""; } return suggestion.Value; } if (!suggestion.Value.StartsWith(text, StringComparison.OrdinalIgnoreCase)) { return ""; } return suggestion.Value.Substring(text.Length); } string prefix = _current.Prefix; if (prefix.Length == 0 && !_expanded) { return ""; } if (!suggestion.Value.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { return ""; } return suggestion.Value.Substring(prefix.Length); } public string SuggestMarkup() { return Markup.Suggestions(_current, _selected, _window, _expanded); } } public sealed class SuggestionSet { public static readonly SuggestionSet Empty = new SuggestionSet(null, null, -1, ""); public IReadOnlyList<Suggestion> Rows { get; } public CommandInfo Command { get; } public int ArgIndex { get; } public string Prefix { get; } public bool Any => Rows.Count > 0; internal SuggestionSet(IReadOnlyList<Suggestion> rows, CommandInfo command, int argIndex, string prefix) { Rows = rows ?? Array.Empty<Suggestion>(); Command = command; ArgIndex = argIndex; Prefix = prefix ?? ""; } } public sealed class Suggestions { public const int MaxRows = 8; public const int MaxMatches = 300; public const int MaxHistoryRows = 3; public const int MaxHistoryRowsAlone = 8; private readonly ICommandCatalogue _catalogue; private readonly Usage _usage; private readonly History _history; private readonly Aliases _aliases; private readonly Marks _marks; public Suggestions(ICommandCatalogue catalogue, Usage usage, History history, Aliases aliases, Marks marks = null) { _catalogue = catalogue; _usage = usage; _history = history; _aliases = aliases; _marks = marks; } public SuggestionSet For(string line) { if (line == null) { line = ""; } string text = CommandLine.Unwrap(LastStatement(line)); CommandLine.TokenAtCaret(text, out var index, out var prefix); if (index != 0) { return ForArgument(text, index, prefix); } return ForCommandWord(prefix, line); } private SuggestionSet ForCommandWord(string prefix, string wholeLine) { List<Suggestion> list = new List<Suggestion>(); foreach (CommandInfo command in _catalogue.Commands) { MatchResult matchResult = FuzzyMatcher.Match(command.Word, prefix); if (matchResult.IsMatch) { list.Add(new Suggestion(SuggestionKind.Command, command.Word, command.Source, command.IsVanilla, matchResult.Score)); } } foreach (CommandInfo item in Builtins.Catalogue) { if (InCatalogue(item.Word) == null) { MatchResult matchResult2 = FuzzyMatcher.Match(item.Word, prefix); if (matchResult2.IsMatch) { list.Add(new Suggestion(SuggestionKind.Command, item.Word, item.Source, isVanilla: false, matchResult2.Score)); } } } foreach (KeyValuePair<string, string> item2 in _aliases.All) { MatchResult matchResult3 = FuzzyMatcher.Match(item2.Key, prefix); if (matchResult3.IsMatch) { list.Add(new Suggestion(SuggestionKind.Command, item2.Key, "Alias", isVanilla: false, matchResult3.Score)); } } Usage.Order(list, (Suggestion s) => _usage.CommandCount(s.Value)); Trim(list, 300); List<Suggestion> list2 = HistoryRows(wholeLine, (prefix.Length == 0) ? 8 : 3); list2.Reverse(); list.AddRange(list2); return new SuggestionSet(list, null, -1, prefix); } private SuggestionSet ForArgument(string statement, int tokenIndex, string prefix) { List<string> list = CommandLine.Tokenise(statement); string word = ((list.Count > 0) ? list[0] : ""); int argIndex = tokenIndex - 1; Resolve(ref word, ref argIndex); CommandInfo commandInfo = Find(word); List<Suggestion> list2 = new List<Suggestion>(); foreach (ArgValue item in Values(word, argIndex)) { MatchResult matchResult = FuzzyMatcher.Match(item.Value, prefix); if (matchResult.IsMatch) { list2.Add(new Suggestion(SuggestionKind.Argument, item.Value, item.Source, item.IsVanilla, matchResult.Score)); } } if (list2.Count == 0 && !_catalogue.Owns(word, argIndex) && commandInfo != null) { foreach (string item2 in UsageExample.Literals(commandInfo.Usage, commandInfo.Signature, argIndex)) { MatchResult matchResult2 = FuzzyMatcher.Match(item2, prefix); if (matchResult2.IsMatch) { list2.Add(new Suggestion(SuggestionKind.Argument, item2, commandInfo.Source, commandInfo.IsVanilla, matchResult2.Score)); } } } Usage.Order(list2, (Suggestion s) => _usage.ArgCount(word, s.Value)); list2 = Usage.GroupBySupplier(list2); Trim(list2, 300); list2.InsertRange(0, ContextRows(word, argIndex, prefix)); return new SuggestionSet(list2, commandInfo, argIndex, prefix); } private IEnumerable<ArgValue> Values(string word, int argIndex) { IReadOnlyList<ArgValue> readOnlyList = _catalogue.ValuesFor(word, argIndex); if (readOnlyList.Count > 0) { return readOnlyList; } if (argIndex != 0) { return readOnlyList; } return word.ToLowerInvariant() switch { "unalias" => Aliases(), "help" => Topics(), "logs" => Literals("off", "warn", "error", "log"), _ => readOnlyList, }; } private IEnumerable<ArgValue> Aliases() { foreach (KeyValuePair<string, string> item in _aliases.All) { yield return new ArgValue(item.Key, "Alias", isVanilla: false); } } private static IEnumerable<ArgValue> Topics() { yield return new ArgValue("all", "hash", isVanilla: false); (string Topic, string[] Words)[] groups = HelpTopics.Groups; for (int i = 0; i < groups.Length; i++) { string item = groups[i].Topic; yield return new ArgValue(item, "hash", isVanilla: false); } yield return new ArgValue("terminal", "hash", isVanilla: false); } private static IEnumerable<ArgValue> Literals(params string[] values) { foreach (string value in values) { yield return new ArgValue(value, "hash", isVanilla: false); } } private void Resolve(ref string word, ref int argIndex) { string text = _aliases.Expand(word); if (!string.IsNullOrEmpty(text)) { List<string> list = CommandLine.Tokenise(text); if (list.Count != 0) { word = list[0]; argIndex += list.Count - 1; } } } private List<Suggestion> ContextRows(string command, int argIndex, string prefix) { List<Suggestion> list = new List<Suggestion>(); if (_marks == null) { return list; } MarkKind markKind = _catalogue.KindOf(command, argIndex); if (markKind == MarkKind.None) { return list; } string[] words = Marks.Words; foreach (string text in words) { MatchResult matchResult = FuzzyMatcher.Match(text, prefix); if (matchResult.IsMatch) { Mark mark = _marks.Resolve(text); if (mark.Exists && (mark.Kind == MarkKind.Any || markKind == MarkKind.Any || mark.Kind == markKind)) { list.Add(new Suggestion(SuggestionKind.Argument, text, mark.Id, isVanilla: false, matchResult.Score)); } } } return list; } private List<Suggestion> HistoryRows(string line, int budget) { List<Suggestion> list = new List<Suggestion>(); if (budget <= 0) { return list; } foreach (string item in _history.Search(line.Trim())) { if (!string.Equals(item, line.Trim(), StringComparison.Ordinal)) { list.Add(new Suggestion(SuggestionKind.History, item, "History", isVanilla: false, 0)); if (list.Count >= budget) { break; } } } return list; } public CommandInfo Find(string word) { return InCatalogue(word) ?? InBuiltins(word); } public bool IsCommand(string word) { return InCatalogue(word) != null; } public bool IsKnown(string word) { return Find(word) != null; } private CommandInfo InCatalogue(string word) { if (string.IsNullOrEmpty(word)) { return null; } foreach (CommandInfo command in _catalogue.Commands) { if (string.Equals(command.Word, word, StringComparison.OrdinalIgnoreCase)) { return command; } } return null; } private static CommandInfo InBuiltins(string word) { if (string.IsNullOrEmpty(word)) { return null; } foreach (CommandInfo item in Builtins.Catalogue) { if (string.Equals(item.Word, word, StringComparison.OrdinalIgnoreCase)) { return item; } } return null; } private static void Trim(List<Suggestion> rows, int max) { if (max < 0) { max = 0; } if (rows.Count > max) { rows.RemoveRange(max, rows.Count - max); } } private static string LastStatement(string line) { int num = -1; bool flag = false; for (int i = 0; i < line.Length; i++) { if (line[i] == '"') { flag = !flag; } else if (line[i] == ';' && !flag) { num = i; } } if (num >= 0) { return line.Substring(num + 1).TrimStart(); } return line; } } public sealed class Transcript { public const int Kept = 2000; public const int Shown = 40; private readonly List<OutputLine> _lines = new List<OutputLine>(); public IReadOnlyList<OutputLine> Lines => _lines; public int Count => _lines.Count; public void Add(OutputLine line) { _lines.Add(line); if (_lines.Count > 2256) { _lines.RemoveRange(0, _lines.Count - 2000); } } public void Add(IEnumerable<OutputLine> lines) { if (lines == null) { return; } foreach (OutputLine line in lines) { Add(line); } } public void Clear() { _lines.Clear(); } public IEnumerable<OutputLine> Recent(int count) { int i = _lines.Count - 1; while (i >= 0 && count > 0) { yield return _lines[i]; i--; count--; } } public List<OutputLine> Window() { int num = Math.Max(0, _lines.Count - 40); return _lines.GetRange(num, _lines.Count - num); } } public sealed class Usage { private const string FileName = "usage.txt"; private const int CountedArgIndex = 0; private readonly Dictionary<string, int> _commands = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, int> _args = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); private bool _dirty; public void Record(IReadOnlyList<string> tokens) { if (tokens == null || tokens.Count == 0) { return; } string text = tokens[0]; if (!string.IsNullOrWhiteSpace(text)) { Bump(_commands, text); if (tokens.Count > 1) { Bump(_args, Key(text, tokens[1])); } _dirty = true; } } public int CommandCount(string word) { if (word == null || !_commands.TryGetValue(word, out var value)) { return 0; } return value; } public int ArgCount(string command, string value) { if (command == null || value == null || !_args.TryGetValue(Key(command, value), out var value2)) { return 0; } return value2; } public static void Order(List<Suggestion> candidates, Func<Suggestion, int> countOf) { if (candidates == null || candidates.Count < 2) { return; } candidates.Sort(delegate(Suggestion a, Suggestion b) { if (a.Score != b.Score) { return b.Score.CompareTo(a.Score); } int num = countOf(b).CompareTo(countOf(a)); return (num != 0) ? num : string.Compare(a.Value, b.Value, StringComparison.OrdinalIgnoreCase); }); } public static List<Suggestion> GroupBySupplier(List<Suggestion> ordered) { if (ordered == null || ordered.Count < 2) { return ordered ?? new List<Suggestion>(); } List<Suggestion> list = new List<Suggestion>(); List<Suggestion> list2 = new List<Suggestion>(); SortedDictionary<string, List<Suggestion>> sortedDictionary = new SortedDictionary<string, List<Suggestion>>(StringComparer.OrdinalIgnoreCase); foreach (Suggestion item in ordered) { if (item.IsVanilla) { list.Add(item); continue; } if (string.IsNullOrEmpty(item.Source)) { list2.Add(item); continue; } if (!sortedDictionary.TryGetValue(item.Source, out var value)) { value = (sortedDictionary[item.Source] = new List<Suggestion>()); } value.Add(item); } List<Suggestion> list4 = new List<Suggestion>(ordered.Count); list4.AddRange(list); foreach (KeyValuePair<string, List<Suggestion>> item2 in sortedDictionary) { list4.AddRange(item2.Value); } list4.AddRange(list2); return list4; } public void Load(IStore store) { _commands.Clear(); _args.Clear(); _dirty = false; string text = store?.Read(StoreScope.Save, "usage.txt"); if (string.IsNullOrEmpty(text)) { return; } string[] array = text.Split('\n'); for (int i = 0; i < array.Length; i++) { string text2 = array[i].Trim('\r', ' '); if (text2.Length == 0) { continue; } int num = text2.LastIndexOf('\t'); if (num > 0 && int.TryParse(text2.Substring(num + 1), out var result) && result > 0) { string text3 = text2.Substring(0, num); if (text3.IndexOf(' ') > 0) { _args[text3] = result; } else { _commands[text3] = result; } } } } public void Save(IStore store) { if (!_dirty || store == null) { return; } StringBuilder stringBuilder = new StringBuilder(); foreach (KeyValuePair<string, int> command in _commands) { stringBuilder.Append(command.Key).Append('\t').Append(command.Value) .Append('\n'); } foreach (KeyValuePair<string, int> arg in _args) { stringBuilder.Append(arg.Key).Append('\t').Append(arg.Value) .Append('\n'); } store.Write(StoreScope.Save, "usage.txt", stringBuilder.ToString()); _dirty = false; } private static string Key(string command, string value) { return command + " " + value; } private static void Bump(Dictionary<string, int> into, string key) { into.TryGetValue(key, out var value); into[key] = value + 1; } } public static class UsageExample { private static readonly Dictionary<string, string> Known = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { ["give"] = "give <item> [quantity]", ["packageproduct"] = "packageproduct <packaging>", ["setdiscovered"] = "setdiscovered <product>", ["teleport"] = "teleport <location|property|npc>", ["spawnvehicle"] = "spawnvehicle <vehicle>", ["setowned"] = "setowned <property|business>", ["setunlocked"] = "setunlocked <npc>", ["setrelationship"] = "setrelationship <npc> <value>", ["addemployee"] = "addemployee <type> <property>", ["setquality"] = "setquality <quality>", ["setregionunlocked"] = "setregionunlocked <region>", ["setqueststate"] = "setqueststate <quest> <state>", ["setquestentrystate"] = "setquestentrystate <quest> <entry> <state>", ["setvar"] = "setvar <variable> <value>", ["bind"] = "bind <key> <command>", ["unbind"] = "unbind <key>", ["setpoliceignoreplayers"] = "setpoliceignoreplayers <true|false>", ["setweather"] = "setweather <weather>", ["triggerlightning"] = "triggerlightning [npc|player]", ["settime"] = "settime <hhmm>", ["changecash"] = "changecash <amount>", ["changebalance"] = "changebalance <amount>", ["addxp"] = "addxp <amount>", ["setstaminareserve"] = "setstaminareserve <amount>", ["setmovespeed"] = "setmovespeed <multiplier>", ["setjumpforce"] = "setjumpforce <multiplier>", ["setemotion"] = "setemotion <emotion>", ["disable"] = "disable <label>", ["enable"] = "enable <label>", ["disablenpcasset"] = "disablenpcasset <asset|all>", ["playcutscene"] = "playcutscene <cutscene>", ["sethealth"] = "sethealth <amount>", ["setquantity"] = "setquantity <amount>", ["settimescale"] = "settimescale <scale>", ["setgravitymultiplier"] = "setgravitymultiplier <multiplier>", ["setdayduration"] = "setdayduration <minutes>", ["setlawintensity"] = "setlawintensity <intensity>" }; private static readonly HashSet<string> OptionalLast = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "give", "triggerlightning" }; public static string Signature(string word, string example) { if (string.IsNullOrEmpty(word)) { return ""; } if (Known.TryGetValue(word, out var value)) { return value; } if (!string.IsNullOrEmpty(example) && example.IndexOf('<') >= 0) { return Tighten(Collapse(FirstExample(example))); } return FromExample(word, example); } public static IEnumerable<string> Literals(string example, string signature, int argIndex) { string text = ((!string.IsNullOrEmpty(signature)) ? signature : example); if (string.IsNullOrEmpty(text) || argIndex < 0) { yield break; } List<string> list = Split(FirstExample(text)); int num = argIndex + 1; if (num >= list.Count) { yield break; } string text2 = list[num].Trim('<', '>', '[', ']'); if (text2.IndexOf('|') < 0) { yield break; } string[] array = text2.Split('|'); for (int i = 0; i < array.Length; i++) { string text3 = array[i].Trim(); if (text3.Length > 0) { yield return text3; } } } public static int ArgumentCount(string signature) { if (string.IsNullOrEmpty(signature)) { return 0; } return Math.Max(0, Split(signature).Count - 1); } private static string FromExample(string word, string example) { List<string> list = Split(FirstExample(example)); if (list.Count <= 1) { return word.ToLowerInvariant(); } StringBuilder stringBuilder = new StringBuilder(word.ToLowerInvariant()); for (int i = 1; i < list.Count; i++) { bool flag = i == list.Count - 1; bool flag2 = (flag && list.Count >= 3 && (LooksNumeric(list[i]) || LooksBoolean(list[i]))) || (flag && OptionalLast.Contains(word) && list.Count >= 3); st