/// <summary> /// Initializes a new instance of the <see cref="SynServerTool" /> main class. /// </summary> /// <param name="isRestart">if set to <c>true</c> then the application was /// launched for the process of restarting previous monitoring.</param> /// <remarks> /// Some notes/thoughts: All of the core functions of the bot including console text /// processing, player/server event processing, modules, command processing, vote /// management, parsing, etc. are initialized in this constructor and set as properties in /// this main class. The bot only allows one instance of itself for the explicit reason that /// Quake Live can only have one running copy open at a time. For this reason, this /// initilizated <see cref="SynServerTool" /> object is frequently passed around the rest of /// the code almost entirely through constructor injection and the properties are directly /// accessed rather than constantly instantiating new classes. In this application, access /// to state among most parts is crucial, and unfortunately that leads to some unavoidable /// tight coupling. Once intilizated, the bot will then call the /// <see cref="CheckForAutoMonitoring" /> method which reads the configuration to see if the /// user has specified whether server monitoring should begin on application start. If Quake /// Live is running, we will check to see if the client is connected to a server. If /// connected, we will retrieve the server information and players using built in QL /// commands. After that, we will start a timer that waits for ~6.5s to perform any final /// initlization tasks to make sure all necessary information is present. This project /// initially started as a VERY simple proof of concept and expanded dramatically from /// there, so refactoring in various places is almost certainly in order. For example, a /// user interface was not initially planned (the tool was going to only be command-driven /// in-game), but was later added during development for ease of use. /// </remarks> public SynServerTool(bool isRestart) { // Core ServerInfo = new ServerInfo(); QlCommands = new QlCommands(this); Parser = new Parser(); QlWindowUtils = new QlWindowUtils(); ConsoleTextProcessor = new ConsoleTextProcessor(this); ServerEventProcessor = new ServerEventProcessor(this); VoteManager = new VoteManager(); //Set the name of the bot AccountName = GetAccountNameFromConfig(); // Hook up modules Mod = new ModuleManager(this); // Hook up command listener CommandProcessor = new CommandProcessor(this); // If being launched as restart then automatically try to start monitoring and skip the check. if (isRestart) { // ReSharper disable once UnusedVariable (synchronous) var a = AttemptAutoMonitorStart(); } else { // Otherwise, check if we should begin monitoring a server immediately per user's settings. CheckForAutoMonitoring(); } }
/// <summary> /// Attempts to automatically start server monitoring on application launch, if the user has /// this option specified in the SST configuration file. /// </summary> public async Task AttemptAutoMonitorStart() { if (!QlWindowUtils.QuakeLiveConsoleWindowExists()) { Log.Write( "Attempt to auto-start server monitoring on program start failed: QL instance not found.", _logClassType, _logPrefix); MessageBox.Show( @"Could not auto-start server monitoring because a running instance of Quake Live was not found!", @"Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } await BeginMonitoring(); }
/// <summary> /// Reads the QL console window. /// </summary> private void ReadQlConsole() { var consoleWindow = QlWindowUtils.GetQuakeLiveConsoleWindow(); var cText = QlWindowUtils.GetQuakeLiveConsoleTextArea(consoleWindow, QlWindowUtils.GetQuakeLiveConsoleInputArea(consoleWindow)); if (cText != IntPtr.Zero) { while (IsReadingConsole) { var textLength = Win32Api.SendMessage(cText, Win32Api.WM_GETTEXTLENGTH, IntPtr.Zero, IntPtr.Zero); if ((textLength == 0) || (ConsoleTextProcessor.OldWholeConsoleLineLength == textLength)) { continue; } // Entire console window text var entireBuffer = new StringBuilder(textLength + 1); Win32Api.SendMessage(cText, Win32Api.WM_GETTEXT, new IntPtr(textLength + 1), entireBuffer); var received = entireBuffer.ToString(); ConsoleTextProcessor.ProcessEntireConsoleText(received, textLength); var lengthDifference = Math.Abs(textLength - _oldLength); if (received.Length > lengthDifference) { // Bounds checking int start; int length; if (_oldLength > received.Length) { start = 0; length = received.Length; } else { start = _oldLength; length = lengthDifference; } // Standardize QL's annoying string formatting var diffBuilder = new StringBuilder(received.Substring(start, length)); diffBuilder.Replace("\"\r\n\r\n", "\"\r\n"); diffBuilder.Replace("\r\n\"\r\n", "\r\n"); diffBuilder.Replace("\r\n\r\n", "\r\n"); ConsoleTextProcessor.ProcessShortConsoleLines(diffBuilder.ToString()); } // Detect when buffer is about to be full, in order to auto-clear. Win Edit // controls can have a max of 30,000 characters, see: "Limits of Edit Controls" // - http://msdn.microsoft.com/en-us/library/ms997530.aspx More info: Q3 source // (win_syscon.c), Conbuf_AppendText int begin, end; Win32Api.SendMessage(cText, Win32Api.EM_GETSEL, out begin, out end); if ((begin >= 29300) && (end >= 29300)) { Log.Write("Clearing nearly full conbuf.", _logClassType, _logPrefix); // Auto-clear QlCommands.ClearQlWinConsole(); } _oldLength = textLength; } } else { Log.WriteCritical("Unable to get necessary console handle.", _logClassType, _logPrefix); } }
/// <summary> /// Executes the specified command asynchronously. /// </summary> /// <param name="c">The cmd args.</param> public async Task <bool> ExecAsync(Cmd c) { if (!c.Args[1].Equals("start") && !c.Args[1].Equals("stop") && !c.Args[1].Equals("reset") && !c.Args[1].Equals("status")) { DisplayArgLengthError(c); return(false); } var qlw = new QlWindowUtils(); if (!qlw.QuakeLiveConsoleWindowExists()) { _irc.SendIrcNotice(c.FromUser, "[ERROR] A running instance of Quake Live could not be found."); Log.Write(string.Format( "{0} attempted to use {1} command but a running instance of Quake Live could not be found. Ignoring.", c.FromUser, c.CmdName), _logClassType, _logPrefix); return(false); } if (c.Args[1].Equals("start")) { if (_sst.IsMonitoringServer) { _irc.SendIrcNotice(c.FromUser, "[ERROR] Your QL server is already being monitored."); Log.Write(string.Format( "{0} attempted to start server monitoring but server is already being monitored. Ignoring.", c.FromUser), _logClassType, _logPrefix); return(false); } _irc.SendIrcMessage(_irc.IrcSettings.ircChannel, "\u0002[SUCCESS]\u0002 Attempting to start QL server monitoring."); await _sst.BeginMonitoring(); // Give it time to complete initilization, then show status. await Task.Delay(11000); ShowMonitorStatus(); } else if (c.Args[1].Equals("stop")) { if (!_sst.IsMonitoringServer) { _irc.SendIrcNotice(c.FromUser, "[ERROR] No QL server is currently being monitored."); Log.Write(string.Format( "{0} attempted to stop server monitoring but server is not currently being monitored. Ignoring.", c.FromUser), _logClassType, _logPrefix); return(false); } _sst.StopMonitoring(); _irc.SendIrcMessage(_irc.IrcSettings.ircChannel, "\u0002[SUCCESS]\u0002 Stopped monitoring your QL server."); } else if (c.Args[1].Equals("reset")) { if (_sst.IsMonitoringServer) { Log.Write(string.Format( "{0} reset server monitoring for actively monitored server; now stopping server monitoring.", c.FromUser), _logClassType, _logPrefix); _irc.SendIrcMessage(_irc.IrcSettings.ircChannel, "\u0002[SUCCESS]\u0002 Your QL server was being monitored; now stopping this monitoring."); _sst.StopMonitoring(); } else { Log.Write(string.Format( "{0} reset server monitoring for non-actively monitored server; now starting server monitoring.", c.FromUser), _logClassType, _logPrefix); _irc.SendIrcMessage(_irc.IrcSettings.ircChannel, "\u0002[SUCCESS]\u0002 Your QL server was not being monitored; now starting monitoring."); await _sst.BeginMonitoring(); } } else if (c.Args[1].Equals("status")) { ShowMonitorStatus(); } return(true); }