/// <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> /// Sends commands to Quake Live to verify that a server connection exists. /// </summary> public async Task CheckQlServerConnectionExists() { QlCommands.ClearQlWinConsole(); await QlCommands.CheckMainMenuStatus(); await QlCommands.CheckCmdStatus(); QlCommands.QlCmdClear(); }
/// <summary> /// Performs the delayed initialization steps. /// </summary> /// <param name="secondsToWait">The number of seconds the timer should wait before executing.</param> private void PerformDelayedInitTasks(double secondsToWait) { _delayedInitTaskTimer = new Timer(secondsToWait * 1000) { AutoReset = false, Enabled = true }; // Finalize the delayed initilization tasks _delayedInitTaskTimer.Elapsed += async(sender, args) => { Log.Write("Performing delayed initilization tasks.", _logClassType, _logPrefix); QlCommands.ClearQlWinConsole(); // Initiate modules such as MOTD and others that can't be started until after we're live Mod.Motd.Init(); // Get IP CheckServerAddress(); // Send configstrings request in order to get an accurate listing of the teams. // Strangely, this appears to not register w/ QL at various times, so send it a few // different times. for (var i = 1; i < 4; i++) { await QlCommands.SendToQlDelayedAsync("configstrings", true, (i * 3)); } // Update UI status bar with IP await Task.Delay(2000); UserInterface.UpdateMonitoringStatusUi(true, ServerInfo.CurrentServerAddress); QlCommands.QlCmdClear(); // Done QlCommands.ClearBothQlConsoles(); QlCommands.SendToQl("echo ^4***^5SST is now ^2LOADED^4***", false); QlCommands.SendToQl("print ^4***^5SST is now ^2LOADED^4***", false); Log.Write("SST is now loaded on the server.", _logClassType, _logPrefix); IsInitComplete = true; MonitoringStartedTime = DateTime.Now; _delayedInitTaskTimer.Enabled = false; _delayedInitTaskTimer = null; }; }
/// <summary> /// Gets the server information. /// </summary> private void GetServerInformation() { // First and foremost, clear the console and get the player listing. QlCommands.ClearQlWinConsole(); // Re-focus the window Win32Api.SwitchToThisWindow(QlWindowUtils.QlWindowHandle, true); // Disable developer mode if it's already set, so we can get accurate player listing. QlCommands.DisableDeveloperMode(); // Initially get the player listing when we start. Synchronous since initilization. // ReSharper disable once UnusedVariable var q = QlCommands.QlCmdPlayers(); // Get the server's id QlCommands.SendToQl("serverinfo", true); // Enable developer mode QlCommands.EnableDeveloperMode(); // Delay some initilization tasks and complete initilization PerformDelayedInitTasks(InitDelay); Log.Write("Requesting server information.", _logClassType, _logPrefix); }
/// <summary> /// Attempt to start monitoring the server, per the user's request. /// </summary> public async Task BeginMonitoring() { IsInitComplete = false; // We might've been previously monitoring without restarting the application, so also // reset any server information. ServerInfo.Reset(); // Start timer to continuously detect if QL process is running StartProcessDetectionTimer(); // Start reading the console StartConsoleReadThread(); // Hide console text if user has option enabled var cfgHandler = new ConfigHandler(); var cfg = cfgHandler.ReadConfiguration(); if (cfg.CoreOptions.hideAllQlConsoleText) { QlCommands.DisableConsolePrinting(); } // Are we connected? await CheckQlServerConnectionExists(); if (!ServerInfo.IsQlConnectedToServer) { MessageBox.Show( @"Could not detect connection to a Quake Live server, monitoring cannot begin!", @"Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } // Get player listing and perform initilization tasks GetServerInformation(); // We're live IsMonitoringServer = true; }
/// <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> /// Gets the server address. /// </summary> private void CheckServerAddress() { QlCommands.RequestServerAddress(); QlCommands.QlCmdClear(); }