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 _pending = new List(); private static Action _registerApp; private static Action> _handle; private static Action _emit; private static Action _allowHost; private static Action _declareOrientations; private static Action _setBadge; private static Action _notify; private static Action _notifyFor; private static Func _isOnScreen; private static Action _setImage; private static Action _setIconHidden; private static Action _setAppOpen; private static Func _isAppOpen; private static Func _setPhoneRaised; private static Func _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 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>(type, "RegisterApp"); if (_registerApp == null) { return; } _handle = Get>>(type, "Handle"); _emit = Get>(type, "Emit"); _allowHost = Get>(type, "AllowHost"); _declareOrientations = Get>(type, "DeclareOrientations"); _setBadge = Get>(type, "SetBadge"); _notify = Get>(type, "Notify"); _notifyFor = Get>(type, "NotifyFor"); _isOnScreen = Get>(type, "IsAppOnScreen"); _setImage = Get>(type, "SetImage"); _setIconHidden = Get>(type, "SetIconHidden"); _setAppOpen = Get>(type, "SetAppOpen"); _isAppOpen = Get>(type, "IsAppOpen"); _setPhoneRaised = Get>(type, "SetPhoneRaised"); _isPhoneRaised = Get>(type, "IsPhoneRaised"); _bound = true; for (int i = 0; i < _pending.Count; i++) { try { _pending[i](); } catch { } } _pending.Clear(); } catch { } } private static T Get(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 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 _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("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 lines; if (!flag) { IReadOnlyList readOnlyList = Array.Empty(); lines = readOnlyList; } else { IReadOnlyList 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 WithLogs(IReadOnlyList ran) { if (!_session.Builtins.LogsOpen) { _logsDrawnUpTo = _log.Ring.Count; return ran; } List list = new List(ran); list.AddRange(Fresh()); return list; } private List Fresh() { List list = new List(); 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 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 _byName = new Dictionary(StringComparer.OrdinalIgnoreCase); private bool _dirty; public int Count => _byName.Count; public IEnumerable> All { get { List list = new List(_byName.Keys); list.Sort(StringComparer.OrdinalIgnoreCase); foreach (string item in list) { yield return new KeyValuePair(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 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 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 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 ", "remove an alias", "unalias gk"), Own("grep", "grep ", "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 ", "run the rest of the line as one command, semicolons and all", "raw bind t 'settime 1200'"), Own("repeat", "repeat ", "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 output) { output = null; List 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 list2 = new List(); 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 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 list = new List(); 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 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> dictionary = Sort(); List list = new List(); (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> 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 ' lists one, 'help ' explains one, " + "'help all' lists every one.")); } private void OneTopic(string topic, List 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 lines) { Dictionary> dictionary = Sort(); List> list = new List>(); (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> 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> list2 = new List> { new List() }; foreach (List item3 in list) { List list3 = list2[list2.Count - 1]; if (list3.Count > 0 && list3.Count + item3.Count > 16) { list3 = new List(); 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 TopicBlock(string topic, List words) { List list = new List { 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 words, List 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> Sort() { Dictionary> dictionary = new Dictionary>(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> byTopic, string topic, string word) { if (!byTopic.TryGetValue(topic, out var value)) { value = (byTopic[topic] = new List()); } 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 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 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 Grid(IReadOnlyList commands, IReadOnlyList builtins) { List words = new List(); foreach (CommandInfo command in commands) { words.Add(command.Word); } List list = new List(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 lines) { List 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 tokens, List lines) { if (rest.Length == 0) { if (_aliases.Count != 0) { foreach (KeyValuePair item in _aliases.All) { lines.Add(OutputLine.Out(Pad(item.Key, 16) + item.Value)); } return; } lines.Add(OutputLine.Dim("no aliases yet - alias 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 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 lines) { if (pattern.Length == 0) { lines.Add(OutputLine.Error("grep: needs something to look for")); UsageLine("grep", lines); return; } List list = new List(); 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 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 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 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 Commands { get; } public string Error { get; } public bool Failed => Error != null; internal Plan(IReadOnlyList commands, string error) { Commands = commands ?? Array.Empty(); 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 expandAlias = null) { if (string.IsNullOrWhiteSpace(line)) { return new Plan(Array.Empty(), 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 "); } 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 expandAlias, bool alreadyExpanded) { if (!TrySplitStatements(line, out var statements, out var error)) { return new Plan(null, error); } List list = new List(); 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 "); } 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 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 "; return false; } if (result < 1) { error = "repeat: the count has to be 1 or more\nusage: repeat "; 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 "; return false; } if (StartsWithWord(text2, "raw", out var rest2)) { if (rest2.Length == 0) { error = "raw: no command after it\nusage: raw "; return false; } text2 = rest2; } List list = new List(); 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 statements, out string error) { statements = new List(); 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 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 Tokenise(string command) { List list = new List(); 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 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 Index() { Dictionary dictionary = new Dictionary(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 _lines = new List(); private bool _dirty; public IReadOnlyList 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 Search(string query) { List list = new List(); 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 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 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 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("
"); } 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 list = new List(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 lines) { if (lines == null || lines.Count == 0) { return ""; } StringBuilder stringBuilder = new StringBuilder(); foreach (OutputLine line in lines) { if (stringBuilder.Length > 0) { stringBuilder.Append("
"); } 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("
"); } 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("") .Append(Escape(text)) .Append(""); } } } 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 Commands { get; } IReadOnlyList 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 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 Lines { get; internal set; } = Array.Empty(); 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 Banner(string identity) { List list = new List(); 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 list = new List(); 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 list2 = new List(); 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 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 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 into) { _transcript.Add(line); into.Add(line); if (line.Kind == LineKind.Out) { _marks.Printed(line.Text); } } private void Echo(string typed, List 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 Rows { get; } public CommandInfo Command { get; } public int ArgIndex { get; } public string Prefix { get; } public bool Any => Rows.Count > 0; internal SuggestionSet(IReadOnlyList rows, CommandInfo command, int argIndex, string prefix) { Rows = rows ?? Array.Empty(); 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 list = new List(); 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 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 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 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 list2 = new List(); 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 Values(string word, int argIndex) { IReadOnlyList 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 Aliases() { foreach (KeyValuePair item in _aliases.All) { yield return new ArgValue(item.Key, "Alias", isVanilla: false); } } private static IEnumerable 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 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 list = CommandLine.Tokenise(text); if (list.Count != 0) { word = list[0]; argIndex += list.Count - 1; } } } private List ContextRows(string command, int argIndex, string prefix) { List list = new List(); 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 HistoryRows(string line, int budget) { List list = new List(); 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 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 _lines = new List(); public IReadOnlyList 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 lines) { if (lines == null) { return; } foreach (OutputLine line in lines) { Add(line); } } public void Clear() { _lines.Clear(); } public IEnumerable Recent(int count) { int i = _lines.Count - 1; while (i >= 0 && count > 0) { yield return _lines[i]; i--; count--; } } public List 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 _commands = new Dictionary(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _args = new Dictionary(StringComparer.OrdinalIgnoreCase); private bool _dirty; public void Record(IReadOnlyList 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 candidates, Func 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 GroupBySupplier(List ordered) { if (ordered == null || ordered.Count < 2) { return ordered ?? new List(); } List list = new List(); List list2 = new List(); SortedDictionary> sortedDictionary = new SortedDictionary>(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()); } value.Add(item); } List list4 = new List(ordered.Count); list4.AddRange(list); foreach (KeyValuePair> 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 command in _commands) { stringBuilder.Append(command.Key).Append('\t').Append(command.Value) .Append('\n'); } foreach (KeyValuePair 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 into, string key) { into.TryGetValue(key, out var value); into[key] = value + 1; } } public static class UsageExample { private static readonly Dictionary Known = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["give"] = "give [quantity]", ["packageproduct"] = "packageproduct ", ["setdiscovered"] = "setdiscovered ", ["teleport"] = "teleport ", ["spawnvehicle"] = "spawnvehicle ", ["setowned"] = "setowned ", ["setunlocked"] = "setunlocked ", ["setrelationship"] = "setrelationship ", ["addemployee"] = "addemployee ", ["setquality"] = "setquality ", ["setregionunlocked"] = "setregionunlocked ", ["setqueststate"] = "setqueststate ", ["setquestentrystate"] = "setquestentrystate ", ["setvar"] = "setvar ", ["bind"] = "bind ", ["unbind"] = "unbind ", ["setpoliceignoreplayers"] = "setpoliceignoreplayers ", ["setweather"] = "setweather ", ["triggerlightning"] = "triggerlightning [npc|player]", ["settime"] = "settime ", ["changecash"] = "changecash ", ["changebalance"] = "changebalance ", ["addxp"] = "addxp ", ["setstaminareserve"] = "setstaminareserve ", ["setmovespeed"] = "setmovespeed ", ["setjumpforce"] = "setjumpforce ", ["setemotion"] = "setemotion ", ["disable"] = "disable