/// <summary> /// Sends a command string to the mud and does not do typical line processing like splitting commands, identifying /// if an alias was run, identifying if a hash command was run or tracking the spam guard. /// </summary> /// <param name="cmd">The raw unprocessed command to send.</param> /// <param name="silent">Whether the commands should be outputted to the game window.</param> public async Task SendRaw(string cmd, bool silent) { if (Telnet == null) { Conveyor.EchoLog("You are not connected to the game.", LogType.Error); return; } // Show the command in the window that was sent. if (!silent) { EchoText(cmd, AnsiColors.Yellow); } try { await Telnet.SendAsync(cmd); } catch (Exception ex) { App.Conveyor.EchoError(ex.Message); if (this.Telnet == null || !this.Telnet.IsConnected()) { App.Conveyor.SetText("Disconnected from server."); } } }
/// <summary> /// Connects to the mud server. Requires that the event handlers for required events be passed in here where they will /// be wired up. /// </summary> public async void Connect(EventHandler <string> lineReceived, EventHandler <string> dataReceived, EventHandler connectionClosed) { try { if (Telnet != null) { Telnet.Dispose(); Telnet = null; } Conveyor.EchoLog($"Connecting: {App.Settings.ProfileSettings.IpAddress}:{App.Settings.ProfileSettings.Port}", LogType.Information); var ctc = new CancellationTokenSource(); Telnet = new TelnetClient(App.Settings.ProfileSettings.IpAddress, App.Settings.ProfileSettings.Port, TimeSpan.FromSeconds(0), ctc.Token); Telnet.ConnectionClosed += connectionClosed; Telnet.LineReceived += lineReceived; Telnet.DataReceived += dataReceived; await Telnet.Connect(); } catch (Exception ex) { Telnet.Dispose(); Conveyor.EchoLog($"Connection Failed: {ex.Message}", LogType.Error); } }
/// <summary> /// Connects to the mud server. Requires that the event handlers for required events be passed in here where they will /// be wired up. /// </summary> public async Task Connect(EventHandler <string> lineReceived, EventHandler <string> dataReceived, EventHandler connectionClosed) { try { if (Telnet != null) { Telnet.Dispose(); Telnet = null; } // If there was a last host and it was not the current IP to connect to it likely means // a new profile was loaded, in that case we're going to reset the ScriptingHost to it's default // so things aren't hanging around. if (!string.IsNullOrWhiteSpace(_lastHost) && !string.Equals(_lastHost, App.Settings.ProfileSettings.IpAddress)) { this.ScriptHost?.Reset(); Conveyor.EchoLog("Host change detected: Scripting environment reset.", LogType.Information); // Refresh the scripts so they will load when needed. this.ScriptHost?.RefreshScripts(); } // We can set this now, when we come back in if the IP changes they we'll reset above. _lastHost = App.Settings.ProfileSettings.IpAddress; Conveyor.EchoLog($"Connecting: {App.Settings.ProfileSettings.IpAddress}:{App.Settings.ProfileSettings.Port}", LogType.Information); var ctc = new CancellationTokenSource(); this.Telnet = new TelnetClient(App.Settings.ProfileSettings.IpAddress, App.Settings.ProfileSettings.Port, TimeSpan.FromSeconds(0), ctc.Token); this.Telnet.ConnectionClosed += connectionClosed; this.Telnet.LineReceived += lineReceived; this.Telnet.DataReceived += dataReceived; await this.Telnet.ConnectAsync(); } catch (Exception ex) { Telnet?.Dispose(); Conveyor.EchoLog($"Connection Failed: {ex.Message}", LogType.Error); } }
/// <summary> /// Sends a command string to the mud. /// </summary> /// <param name="cmd"></param> /// <param name="silent">Whether the commands should be outputted to the game window.</param> public async Task Send(string cmd, bool silent, bool addToInputHistory) { // Add the whole thing to the command history if (addToInputHistory) { AddInputHistory(cmd); } _aliasRecursionDepth = 0; var cmdList = ParseCommand(cmd); foreach (string item in cmdList) { if (Telnet == null) { Conveyor.EchoLog("You are not connected to the game.", LogType.Error); return; } try { if (item.SafeLeft(1) != "#") { // Show the command in the window that was sent. if (!silent) { EchoText(item, AnsiColors.Yellow); } // Spam guard if (App.Settings.ProfileSettings.SpamGuard) { // TODO, make this check carriage return and > 1 instead of > 2 if (_spamGuardLastCommand == item && item.Length > 2) { // Increment, we don't need to change the last command, it was the same. _spamGuardCounter++; } else { _spamGuardLastCommand = item; _spamGuardCounter = 0; } if (_spamGuardCounter == 15) { EchoText("where", AnsiColors.Yellow); await Telnet.Send("where"); _spamGuardCounter = 0; } } await Telnet.Send(item); } else { var hashCmd = HashCommands.FirstOrDefault(x => x.Name == item.FirstWord()); if (hashCmd == null) { Conveyor.EchoLog($"Hash command '{item.FirstWord()}' was not found.\r\n", LogType.Error); } else { string parameters = item.RemoveWord(1); hashCmd.Parameters = parameters; if (hashCmd.IsAsync) { await hashCmd.ExecuteAsync(); } else { hashCmd.Execute(); } } } } catch (Exception ex) { EchoText(ex.Message, AnsiColors.Red); } } }
/// <summary> /// Parses a command, also adds it to the history list. /// </summary> /// <param name="cmd"></param> public List <string> ParseCommand(string cmd) { _aliasRecursionDepth += 1; if (_aliasRecursionDepth >= 10) { Conveyor.EchoLog($"Alias error: Reached max recursion depth of {_aliasRecursionDepth}.\r\n", LogType.Error); return(new List <string>()); } // Swap known variables out, both system and user defined. Strings are immutable but if no variable // is found it won't create a new string, it will return the reference to the current one. cmd = this.Conveyor.ReplaceVariablesWithValue(cmd); // Get the current character string characterName = Conveyor.GetVariable("Character"); // Split the list var initialList = cmd.Split(';').ToList(); var list = new List <string>(); foreach (var item in initialList) { var first = item.FirstArgument(); var alias = App.Settings.ProfileSettings.AliasList.FirstOrDefault(x => x.AliasExpression == first.Item1 && x.Enabled == true && (string.IsNullOrEmpty(x.Character) || x.Character == characterName)); if (alias == null) { // No alias was found, just add the item. list.Add(item); } else { // See if the aliases are globally disabled. We're putting this here so we can let the user know they // tried to use an alias but they're disabled. if (!App.Settings.ProfileSettings.AliasesEnabled) { EchoText($"--> Alias found but aliases are globally disabled.", AnsiColors.Red); list.Add(item); continue; } // Increment that we used it alias.Count++; // If the alias is defined as Lua it will be processed verbatim. A lua alias anywhere in the command short circuits // any other input and only that Lua command gets run (and only the first one). // TODO: Make this work better. Is an alias the right way to do this? if (alias.IsLua) { list.Clear(); // TODO: Put this into it's own function. // Alias where the arguments are specified, we will support up to 9 arguments at this time. string lua = alias.Command; // %0 will represent the entire matched string. lua = lua.Replace("%0", first.Item2.Replace("\"", "\\\"")); // %1-%9 for (int i = 1; i <= 9; i++) { lua = lua.Replace($"%{i}", first.Item2.ParseWord(i, " ").Replace("\"", "\\\"")); } // This is all that's going to execute as it clears the list.. we can "fire and forget". this.LuaCaller.ExecuteAsync(lua); return(list); } // We have an alias, see if it's a simple alias where we put all of the text at the end or if // it's one where we'll let the user place the words where they need to be. if (!alias.Command.Contains("%")) { list.AddRange(ParseCommand($"{alias.Command} {first.Item2}".Trim())); _aliasRecursionDepth--; } else { // Alias where the arguments are specified, we will support up to 9 arguments at this time. string aliasStr = alias.Command; // %0 will represent the entire matched string. aliasStr = aliasStr.Replace("%0", first.Item2); // %1-%9 for (int i = 1; i <= 9; i++) { aliasStr = aliasStr.Replace($"%{i}", first.Item2.ParseWord(i, " ")); } list.AddRange(ParseCommand(aliasStr)); _aliasRecursionDepth--; } } } // Return the final list. return(list); }
/// <summary> /// Parses a command, also adds it to the history list. /// </summary> /// <param name="cmd"></param> public List <string> ParseCommand(string cmd) { _aliasRecursionDepth += 1; if (_aliasRecursionDepth >= 10) { Conveyor.EchoLog($"Alias error: Reached max recursion depth of {_aliasRecursionDepth.ToString()}.\r\n", LogType.Error); return(new List <string>()); } // Swap known variables out, both system and user defined. Strings are immutable but if no variable // is found it won't create a new string, it will return the reference to the current one. cmd = this.Conveyor.ReplaceVariablesWithValue(cmd); // Get the current character string characterName = Conveyor.GetVariable("Character"); // Split the list var list = new List <string>(); foreach (var item in cmd.Split(App.Settings.AvalonSettings.CommandSplitCharacter)) { var first = item.FirstArgument(); Alias?alias = null; for (int index = 0; index < App.Settings.ProfileSettings.AliasList.Count; index++) { var x = App.Settings.ProfileSettings.AliasList[index]; if (x.AliasExpression == first.Item1 && x.Enabled && (string.IsNullOrEmpty(x.Character) || x.Character == characterName)) { alias = x; break; } } if (alias == null) { // No alias was found, just add the item. list.Add(item); } else { // See if the aliases are globally disabled. We're putting this here so we can let the user know they // tried to use an alias but they're disabled. if (!App.Settings.ProfileSettings.AliasesEnabled) { this.Conveyor.EchoError($"Alias found for '{alias.AliasExpression ?? "null"}' but aliases are globally disabled."); list.Add(item); continue; } // Increment that we used it alias.Count++; // If the alias is Lua then variables will be swapped in if necessary and then executed. if (alias.IsLua || alias.ExecuteAs == ExecuteType.LuaMoonsharp) { list.Clear(); _ = this.ScriptHost.MoonSharp.ExecuteFunctionAsync <object>(alias.FunctionName, item.Split(' ', StringSplitOptions.RemoveEmptyEntries)); return(list); } // We have an alias, see if it's a simple alias where we put all of the text at the end or if // it's one where we'll let the user place the words where they need to be. if (!alias.Command.Contains('%')) { list.AddRange(ParseCommand($"{alias.Command} {first.Item2}".Trim())); _aliasRecursionDepth--; } else { // Alias where the arguments are specified, we will support up to 9 arguments at this time. if (alias.Command.Contains("%", StringComparison.Ordinal)) { var sb = ZString.CreateStringBuilder(); sb.Append(alias.Command); // %0 will represent the entire matched string. sb.Replace("%0", first.Item2); // %1-%9 for (int i = 1; i <= 9; i++) { sb.Replace($"%{i.ToString()}", first.Item2.ParseWord(i, " ")); } list.AddRange(ParseCommand(sb.ToString())); sb.Dispose(); _aliasRecursionDepth--; } else { list.AddRange(ParseCommand(alias.Command)); _aliasRecursionDepth--; } } } } // Return the final list. return(list); }
/// <summary> /// Sends a command string to the mud. /// </summary> /// <param name="cmd"></param> /// <param name="silent">Whether the commands should be outputted to the game window.</param> /// <param name="addToInputHistory">Whether the command should be added to the input history the user can scroll back through.</param> public async Task Send(string cmd, bool silent, bool addToInputHistory) { // Add the whole thing to the command history if (addToInputHistory) { AddInputHistory(cmd); } _aliasRecursionDepth = 0; foreach (string item in ParseCommand(cmd)) { if (Telnet == null) { Conveyor.EchoLog("You are not connected to the game.", LogType.Error); return; } try { if (!item.StartsWith('#')) { // Show the command in the window that was sent. if (!silent) { EchoText(item, AnsiColors.Yellow); } // Spam guard if (App.Settings.ProfileSettings.SpamGuard) { if (_spamGuardLastCommand == item && item.Length > 2) { // Increment, we don't need to change the last command, it was the same. _spamGuardCounter++; } else { _spamGuardLastCommand = item; _spamGuardCounter = 0; } if (_spamGuardCounter == 15) { EchoText(App.Settings.ProfileSettings.SpamGuardCommand, AnsiColors.Yellow); await Telnet.SendAsync(App.Settings.ProfileSettings.SpamGuardCommand); _spamGuardCounter = 0; } } await Telnet.SendAsync(item); } else { string firstWord = item.FirstWord(); // Avoided a closure allocation by loop instead of using Linq. IHashCommand?hashCmd = null; for (int index = 0; index < this.HashCommands.Count; index++) { var x = this.HashCommands[index]; if (x.Name.Equals(firstWord, StringComparison.Ordinal)) { hashCmd = x; break; } } if (hashCmd == null) { Conveyor.EchoLog($"Hash command '{firstWord}' was not found.\r\n", LogType.Error); } else { hashCmd.Parameters = item.RemoveWord(1); if (hashCmd.IsAsync) { await hashCmd.ExecuteAsync(); } else { // ReSharper disable once MethodHasAsyncOverload hashCmd.Execute(); } } } } catch (Exception ex) { App.Conveyor.EchoError(ex.Message); if (this.Telnet == null || !this.Telnet.IsConnected()) { App.Conveyor.SetText("Disconnected from server.", TextTarget.StatusBarText); } } } // Add words to our unique HashSet if the settings allow for it (and after the // commands for the game have been sent. if (App.Settings.AvalonSettings.AutoCompleteWord && addToInputHistory) { this.InputAutoCompleteKeywords.AddWords(cmd); } }
/// <summary> /// The Loaded event for the Window where we will execute code that should run when the Window /// is first put into place. /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void MainWindow_Loaded(object sender, RoutedEventArgs e) { // The Conveyor will be passed around to other objects so that they can interact with the UI. This Conveyor may have // state so it's important to re-use this object unless sandboxing is needed. Conveyor = new Conveyor(); // The settings for the app load in the app startup, they will then try to load the last profile // that was used. Conveyor.EchoLog($"Settings Folder: {App.Settings.AppDataDirectory}", LogType.Information); Conveyor.EchoLog($"Settings File: {App.Settings.AvalonSettingsFile}", LogType.Information); Conveyor.EchoLog($"Profiles Folder: {App.Settings.AvalonSettings.SaveDirectory}", LogType.Information); // Parse the command line arguments to see if a profile was specified. var args = Environment.GetCommandLineArgs(); // Try to load the last profile loaded, if not found create a new profile. if (File.Exists(App.Settings.AvalonSettings.LastLoadedProfilePath)) { App.Settings.LoadSettings(App.Settings.AvalonSettings.LastLoadedProfilePath); Conveyor.EchoLog($"Settings Loaded: {App.Settings.AvalonSettings.LastLoadedProfilePath}", LogType.Information); } else { if (string.IsNullOrWhiteSpace(App.Settings.AvalonSettings.LastLoadedProfilePath)) { Conveyor.EchoLog($"New Profile being created.", LogType.Information); } else { Conveyor.EchoLog($"Last Profile Loaded Not Found: {App.Settings.AvalonSettings.LastLoadedProfilePath}", LogType.Warning); } } // TODO - Figure out a better way to inject a single Conveyor, maybe static in App? // Inject the Conveyor into the Triggers. foreach (var trigger in App.Settings.ProfileSettings.TriggerList) { trigger.Conveyor = Conveyor; } // Wire up any events that have to be wired up through code. TextInput.Editor.PreviewKeyDown += this.Editor_PreviewKeyDown; AddHandler(TabControlEx.NetworkButtonClickEvent, new RoutedEventHandler(NetworkButton_Click)); AddHandler(TabControlEx.SettingsButtonClickEvent, new RoutedEventHandler(SettingsButton_Click)); // Pass the necessary reference from this page to the Interpreter. Interp = new Interpreter(this.Conveyor); // Setup the handler so when it wants to write to the main window it can by raising the echo event. Interp.Echo += this.InterpreterEcho; // Setup Lua Lua = new Script(); Lua.Options.CheckThreadAccess = false; UserData.RegisterType <LuaCommands>(); // create a userdata, again, explicitly. var luaCmd = UserData.Create(new LuaCommands(Interp)); Lua.Globals.Set("Cmd", luaCmd); LuaControl = new ExecutionControlToken(); // Setup the tick timer. TickTimer = new TickTimer(Conveyor); // Setup the auto complete commands. If they're found refresh them, if they're not // report it to the terminal window. It should -always be found-. RefreshAutoCompleteEntries(); // Auto connect to the game if the setting is set. if (App.Settings.ProfileSettings.AutoConnect) { NetworkButton_Click(null, null); } // Is there an auto execute command or set of commands to run? if (!string.IsNullOrWhiteSpace(App.Settings.ProfileSettings.AutoExecuteCommand)) { Interp.Send(App.Settings.ProfileSettings.AutoExecuteCommand, true, false); } // Finally, all is done, set the focus to the command box. TextInput.Focus(); }
/// <summary> /// Handles when a connection is closed. /// </summary> /// <param name="sender"></param> /// <param name="e"></param> public void HandleConnectionClosed(object sender, EventArgs e) { TabMain.IsConnected = false; Conveyor.EchoLog($"Disconnected: {DateTime.Now}", LogType.Warning); Interp.Telnet = null; }