/// <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> /// Handles the player chat message. /// </summary> /// <param name="text">The text.</param> public void HandlePlayerChatMessage(string text) { // Typically we'd normalize using ToUpperInvariant() but QL doesn't allow accented // characters, so it doesn't matter var msgContent = ConsoleTextProcessor.Strip(text.Substring(text.IndexOf(": ", StringComparison.Ordinal) + 1)) .ToLowerInvariant(); var name = text.Substring(0, text.LastIndexOf('\u0019')); // teamchat is already ignored, so also ignore 'tell' messages which would crash bot if (name.StartsWith("\u0019[")) { return; } var msgFrom = Helpers.GetStrippedName(name); if (!_sst.AccountName.Equals(msgFrom, StringComparison.InvariantCultureIgnoreCase)) { Log.Write(string.Format("Detected chat message {0} from {1}", msgContent, msgFrom), _logClassType, _logPrefix); } // If IRC module is active, send the message to the IRC channel if (_sst.Mod.Irc.Active && _sst.Mod.Irc.IsConnectedToIrc) { // Don't show if (msgContent.StartsWith("[irc]", StringComparison.InvariantCultureIgnoreCase)) { return; } _sst.Mod.Irc.IrcManager.SendIrcMessage(_sst.Mod.Irc.IrcManager.IrcSettings.ircChannel, string.Format("[QL] {0}: {1}", msgFrom, msgContent)); } // Check to see if chat message is a valid command if (msgContent.StartsWith(CommandList.GameCommandPrefix)) { // ReSharper disable once UnusedVariable (synchronous) var s = _sst.CommandProcessor.HandleBotCommand(msgFrom, msgContent); } }
/// <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); } }