Ejemplo n.º 1
0
        public void TestTotalDuration(string input, int expectedDur, int defaultDur, string[] validInputs)
        {
            Parser parser     = new Parser();
            string inputRegex = parser.BuildInputRegex(validInputs);

            ParsedInputSequence inputSequence = parser.ParseInputs(input, inputRegex, new ParserOptions(0, defaultDur, false));

            Assert.AreEqual(inputSequence.TotalDuration, expectedDur);
        }
Ejemplo n.º 2
0
        public void TestOneInput(string input, bool checkDur, int maxDur)
        {
            Parser parser     = new Parser();
            string inputRegex = parser.BuildInputRegex(new string[] { input });

            ParsedInputSequence inputSequence = parser.ParseInputs(input, inputRegex, new ParserOptions(0, 200, checkDur, maxDur));

            Assert.AreEqual(inputSequence.ParsedInputResult, inputSequence.TotalDuration <= maxDur ? ParsedInputResults.Valid : ParsedInputResults.Invalid);
            Assert.AreEqual(inputSequence.Inputs.Count, 1);
        }
Ejemplo n.º 3
0
        public void TestInputCount(string input, int expectedInputCount, string[] validInputs)
        {
            Parser parser     = new Parser();
            string inputRegex = parser.BuildInputRegex(validInputs);

            ParsedInputSequence inputSequence = parser.ParseInputs(input, inputRegex, new ParserOptions(0, 200, false));

            int inputCount = 0;

            for (int i = 0; i < inputSequence.Inputs.Count; i++)
            {
                inputCount += inputSequence.Inputs[i].Count;
            }

            Assert.AreEqual(inputCount, expectedInputCount);
        }
Ejemplo n.º 4
0
        public void TestSubInputDuration(string input, int[] expectedDuration, string[] validInputs)
        {
            Parser parser     = new Parser();
            string inputRegex = parser.BuildInputRegex(validInputs);

            ParsedInputSequence inputSequence = parser.ParseInputs(input, inputRegex, new ParserOptions(0, 200, false, 0));

            int expectedDurIndex = 0;

            for (int i = 0; i < inputSequence.Inputs.Count; i++)
            {
                for (int j = 0; j < inputSequence.Inputs[i].Count; j++)
                {
                    ParsedInput inp = inputSequence.Inputs[i][j];

                    Assert.AreEqual(inp.duration, expectedDuration[expectedDurIndex]);
                    expectedDurIndex++;
                }
            }
        }
Ejemplo n.º 5
0
        public override void ExecuteCommand(EvtChatCommandArgs args)
        {
            string input = args.Command.ArgumentsAsString;

            if (string.IsNullOrEmpty(input) == true)
            {
                QueueMessage(UsageMessage);
                return;
            }

            GameConsole usedConsole = null;

            int lastConsoleID = 1;

            lastConsoleID = (int)DataHelper.GetSettingInt(SettingsConstants.LAST_CONSOLE, 1L);

            using (BotDBContext context = DatabaseManager.OpenContext())
            {
                GameConsole lastConsole = context.Consoles.FirstOrDefault(c => c.ID == lastConsoleID);

                if (lastConsole != null)
                {
                    //Create a new console using data from the database
                    usedConsole = new GameConsole(lastConsole.Name, lastConsole.InputList, lastConsole.InvalidCombos);
                }
            }

            //If there are no valid inputs, don't attempt to parse
            if (usedConsole == null)
            {
                QueueMessage($"The current console does not point to valid data. Please set a different console to use, or if none are available, add one.");
                return;
            }

            if (usedConsole.ConsoleInputs.Count == 0)
            {
                QueueMessage($"The current console, \"{usedConsole.Name}\", does not have any available inputs. Cannot determine length.");
                return;
            }

            ParsedInputSequence inputSequence = default;

            try
            {
                string userName = args.Command.ChatMessage.Username;

                //Get default and max input durations
                //Use user overrides if they exist, otherwise use the global values
                int defaultDur = (int)DataHelper.GetUserOrGlobalDefaultInputDur(userName);
                int maxDur     = (int)DataHelper.GetUserOrGlobalMaxInputDur(userName);

                string regexStr = usedConsole.InputRegex;

                string readyMessage = string.Empty;

                Parser parser = new Parser();

                using (BotDBContext context = DatabaseManager.OpenContext())
                {
                    //Get input synonyms for this console
                    IQueryable <InputSynonym> synonyms = context.InputSynonyms.Where(syn => syn.ConsoleID == lastConsoleID);

                    //Prepare the message for parsing
                    readyMessage = parser.PrepParse(input, context.Macros, synonyms);
                }

                //Parse inputs to get our parsed input sequence
                inputSequence = parser.ParseInputs(readyMessage, regexStr, new ParserOptions(0, defaultDur, true, maxDur));
            }
            catch (Exception exception)
            {
                string excMsg = exception.Message;

                //Handle parsing exceptions
                inputSequence.ParsedInputResult = ParsedInputResults.Invalid;

                QueueMessage($"Invalid input: {excMsg}", Serilog.Events.LogEventLevel.Warning);
                return;
            }

            //Check for non-valid messages
            if (inputSequence.ParsedInputResult != ParsedInputResults.Valid)
            {
                const string dyMacroLenErrorMsg = "Note that length cannot be determined for dynamic macros without inputs filled in.";

                if (inputSequence.ParsedInputResult == ParsedInputResults.NormalMsg ||
                    string.IsNullOrEmpty(inputSequence.Error) == true)
                {
                    QueueMessage($"Invalid input. {dyMacroLenErrorMsg}");
                }
                else
                {
                    QueueMessage($"Invalid input: {inputSequence.Error}. {dyMacroLenErrorMsg}");
                }

                return;
            }

            QueueMessage($"Total length: {inputSequence.TotalDuration}ms");
        }
        public override void ExecuteCommand(EvtChatCommandArgs args)
        {
            string input = args.Command.ArgumentsAsString;

            if (string.IsNullOrEmpty(input) == true)
            {
                QueueMessage(UsageMessage);
                return;
            }

            GameConsole usedConsole = null;

            int lastConsoleID = (int)DataHelper.GetSettingInt(SettingsConstants.LAST_CONSOLE, 1L);

            using (BotDBContext context = DatabaseManager.OpenContext())
            {
                GameConsole lastConsole = context.Consoles.FirstOrDefault(c => c.ID == lastConsoleID);

                if (lastConsole != null)
                {
                    //Create a new console using data from the database
                    usedConsole = new GameConsole(lastConsole.Name, lastConsole.InputList, lastConsole.InvalidCombos);
                }
            }

            //If there are no valid inputs, don't attempt to parse
            if (usedConsole == null)
            {
                QueueMessage($"The current console does not point to valid data. Please set a different console to use, or if none are available, add one.");
                return;
            }

            if (usedConsole.ConsoleInputs.Count == 0)
            {
                QueueMessage($"The current console, \"{usedConsole.Name}\", does not have any available inputs.");
                return;
            }

            ParsedInputSequence inputSequence = default;
            int defaultPort = 0;

            try
            {
                string userName = args.Command.ChatMessage.Username;

                //Get default and max input durations
                //Use user overrides if they exist, otherwise use the global values
                User user = DataHelper.GetUser(userName);

                defaultPort = (int)user.ControllerPort;

                int defaultDur = (int)DataHelper.GetUserOrGlobalDefaultInputDur(userName);
                int maxDur     = (int)DataHelper.GetUserOrGlobalMaxInputDur(userName);

                string regexStr = usedConsole.InputRegex;

                string readyMessage = string.Empty;

                Parser parser = new Parser();

                using (BotDBContext context = DatabaseManager.OpenContext())
                {
                    //Get input synonyms for this console
                    IQueryable <InputSynonym> synonyms = context.InputSynonyms.Where(syn => syn.ConsoleID == lastConsoleID);

                    //Prepare the message for parsing
                    readyMessage = parser.PrepParse(input, context.Macros, synonyms);
                }

                //Parse inputs to get our parsed input sequence
                inputSequence = parser.ParseInputs(readyMessage, regexStr, new ParserOptions(defaultPort, defaultDur, true, maxDur));
            }
            catch (Exception exception)
            {
                string excMsg = exception.Message;

                //Handle parsing exceptions
                inputSequence.ParsedInputResult = ParsedInputResults.Invalid;

                QueueMessage($"Invalid input: {excMsg}");
                return;
            }

            //Check for invalid inputs
            if (inputSequence.ParsedInputResult != ParsedInputResults.Valid)
            {
                if (inputSequence.ParsedInputResult == ParsedInputResults.NormalMsg ||
                    string.IsNullOrEmpty(inputSequence.Error) == true)
                {
                    QueueMessage($"Invalid input.");
                }
                else
                {
                    QueueMessage($"Invalid input: {inputSequence.Error}.");
                }

                return;
            }

            //Reverse parse the string
            string reverseParsed = ReverseParser.ReverseParseNatural(inputSequence, usedConsole,
                                                                     new ReverseParser.ReverseParserOptions(ReverseParser.ShowPortTypes.ShowNonDefaultPorts, defaultPort));

            int botCharLimit = (int)DataHelper.GetSettingInt(SettingsConstants.BOT_MSG_CHAR_LIMIT, 500L);

            QueueMessageSplit(reverseParsed, botCharLimit, ", ");
        }
Ejemplo n.º 7
0
        public override void ExecuteCommand(EvtChatCommandArgs args)
        {
            List <string> arguments = args.Command.ArgumentsAsList;

            if (arguments.Count < 2)
            {
                QueueMessage(UsageMessage);
                return;
            }

            string macroName = arguments[0].ToLowerInvariant();

            //Make sure the first argument has at least a minimum number of characters
            if (macroName.Length < MIN_MACRO_NAME_LENGTH)
            {
                QueueMessage($"Input macros need to be at least {MIN_MACRO_NAME_LENGTH} characters long.");
                return;
            }

            if (macroName.StartsWith(Parser.DEFAULT_PARSER_REGEX_MACRO_INPUT) == false)
            {
                QueueMessage($"Input macros must start with \"{Parser.DEFAULT_PARSER_REGEX_MACRO_INPUT}\".");
                return;
            }

            //For simplicity with wait inputs, force the first character in the macro name to be alphanumeric
            if (char.IsLetterOrDigit(arguments[0][1]) == false)
            {
                QueueMessage("The first character in input macro names must be alphanumeric.");
                return;
            }

            //Check for max macro name
            if (macroName.Length > MAX_MACRO_NAME_LENGTH)
            {
                QueueMessage($"Input macros may have up to a max of {MAX_MACRO_NAME_LENGTH} characters in their name.");
                return;
            }

            int curConsoleID = (int)DataHelper.GetSettingInt(SettingsConstants.LAST_CONSOLE, 1L);

            GameConsole consoleInstance = null;

            using (BotDBContext context = DatabaseManager.OpenContext())
            {
                GameConsole curConsole = context.Consoles.FirstOrDefault(c => c.ID == curConsoleID);
                if (curConsole == null)
                {
                    QueueMessage("Cannot validate input macro, as the current console is invalid. Fix this by setting another console.");
                    return;
                }

                consoleInstance = new GameConsole(curConsole.Name, curConsole.InputList);
            }

            Parser parser = new Parser();

            //Trim the macro name from the input sequence
            string macroVal = args.Command.ArgumentsAsString.Remove(0, macroName.Length + 1).ToLowerInvariant();
            //Console.WriteLine(macroVal);

            bool isDynamic = false;

            //Check for a dynamic macro
            int openParenIndex = macroName.IndexOf('(', 0);

            if (openParenIndex >= 0)
            {
                //If we found the open parenthesis, check for the asterisk
                //This is not comprehensive, but it should smooth out a few issues
                if (openParenIndex == (macroName.Length - 1) || macroName[openParenIndex + 1] != '*')
                {
                    QueueMessage("Invalid input macro. Dynamic macro arguments must be specified with \"*\".");
                    return;
                }

                if (macroName[macroName.Length - 1] != ')')
                {
                    QueueMessage("Invalid input macro. Dynamic macros must end with \")\".");
                    return;
                }

                isDynamic = true;
            }

            //Validate input if not dynamic
            if (isDynamic == false)
            {
                ParsedInputSequence inputSequence = default;

                try
                {
                    string userName = args.Command.ChatMessage.Username;

                    //Get default and max input durations
                    //Use user overrides if they exist, otherwise use the global values
                    int defaultDur = (int)DataHelper.GetUserOrGlobalDefaultInputDur(userName);
                    int maxDur     = (int)DataHelper.GetUserOrGlobalMaxInputDur(userName);

                    string regexStr = consoleInstance.InputRegex;

                    string readyMessage = string.Empty;

                    using (BotDBContext context = DatabaseManager.OpenContext())
                    {
                        IQueryable <InputSynonym> synonyms = context.InputSynonyms.Where(syn => syn.ConsoleID == curConsoleID);

                        readyMessage = parser.PrepParse(macroVal, context.Macros, synonyms);
                    }

                    inputSequence = parser.ParseInputs(readyMessage, regexStr, new ParserOptions(0, defaultDur, true, maxDur));
                    //Console.WriteLine(inputSequence.ToString());

                    if (inputSequence.ParsedInputResult != ParsedInputResults.Valid)
                    {
                        if (string.IsNullOrEmpty(inputSequence.Error) == true)
                        {
                            QueueMessage("Invalid input macro.");
                        }
                        else
                        {
                            QueueMessage($"Invalid input macro: {inputSequence.Error}");
                        }

                        return;
                    }
                }
                catch (Exception e)
                {
                    QueueMessage($"Invalid input macro: {e.Message}");
                    return;
                }
            }

            string message = string.Empty;

            using (BotDBContext context = DatabaseManager.OpenContext())
            {
                InputMacro inputMacro = context.Macros.FirstOrDefault(m => m.MacroName == macroName);

                //Not an existing macro, so add it
                if (inputMacro == null)
                {
                    InputMacro newMacro = new InputMacro(macroName, macroVal);

                    context.Macros.Add(newMacro);

                    if (isDynamic == false)
                    {
                        message = $"Added input macro \"{macroName}\"!";
                    }
                    else
                    {
                        message = $"Added dynamic input macro \"{macroName}\"! Dynamic input macros can't be validated beforehand, so verify it works manually.";
                    }
                }
                //Update the macro value
                else
                {
                    inputMacro.MacroValue = macroVal;

                    if (isDynamic == false)
                    {
                        message = $"Updated input macro \"{macroName}\"!";
                    }
                    else
                    {
                        message = $"Updated dynamic input macro \"{macroName}\"! Dynamic input macros can't be validated beforehand, so verify it works manually.";
                    }
                }

                context.SaveChanges();
            }

            QueueMessage(message);
        }
Ejemplo n.º 8
0
        private void ProcessMsgAsInput(EvtUserMessageArgs e)
        {
            //Ignore commands as inputs
            if (e.UsrMessage.Message.StartsWith(DataConstants.COMMAND_IDENTIFIER) == true)
            {
                return;
            }

            GameConsole usedConsole = null;

            int lastConsoleID = 1;

            using (BotDBContext context = DatabaseManager.OpenContext())
            {
                lastConsoleID = (int)DataHelper.GetSettingIntNoOpen(SettingsConstants.LAST_CONSOLE, context, 1L);
                GameConsole lastConsole = context.Consoles.FirstOrDefault(c => c.ID == lastConsoleID);

                if (lastConsole != null)
                {
                    //Create a new console using data from the database
                    usedConsole = new GameConsole(lastConsole.Name, lastConsole.InputList, lastConsole.InvalidCombos);
                }
            }

            //If there are no valid inputs, don't attempt to parse
            if (usedConsole == null)
            {
                MsgHandler.QueueMessage($"The current console does not point to valid data. Please set a different console to use, or if none are available, add one.");
                return;
            }

            if (usedConsole.ConsoleInputs.Count == 0)
            {
                MsgHandler.QueueMessage($"The current console, \"{usedConsole.Name}\", does not have any available inputs.");
            }

            ParsedInputSequence inputSequence = default;
            string userName    = e.UsrMessage.Username;
            int    defaultDur  = 200;
            int    defaultPort = 0;

            try
            {
                int maxDur = 60000;

                string regexStr = usedConsole.InputRegex;

                string readyMessage = string.Empty;

                //Get default and max input durations
                //Use user overrides if they exist, otherwise use the global values
                User user = DataHelper.GetUser(userName);

                //Get default controller port
                defaultPort = (int)user.ControllerPort;

                defaultDur = (int)DataHelper.GetUserOrGlobalDefaultInputDur(userName);
                maxDur     = (int)DataHelper.GetUserOrGlobalMaxInputDur(userName);

                //TRBotLogger.Logger.Information($"Default dur: {defaultDur} | Max dur: {maxDur}");

                using (BotDBContext context = DatabaseManager.OpenContext())
                {
                    //Get input synonyms for this console
                    IQueryable <InputSynonym> synonyms = context.InputSynonyms.Where(syn => syn.ConsoleID == lastConsoleID);

                    //Prepare the message for parsing
                    readyMessage = InputParser.PrepParse(e.UsrMessage.Message, context.Macros, synonyms);
                }

                //Parse inputs to get our parsed input sequence
                inputSequence = InputParser.ParseInputs(readyMessage, regexStr, new ParserOptions(defaultPort, defaultDur, true, maxDur));
                TRBotLogger.Logger.Debug(inputSequence.ToString());
                TRBotLogger.Logger.Debug("Reverse Parsed (on parse): " + ReverseParser.ReverseParse(inputSequence, usedConsole,
                                                                                                    new ReverseParser.ReverseParserOptions(ReverseParser.ShowPortTypes.ShowNonDefaultPorts, defaultPort,
                                                                                                                                           ReverseParser.ShowDurationTypes.ShowNonDefaultDurations, defaultDur)));
            }
            catch (Exception exception)
            {
                string excMsg = exception.Message;

                //Handle parsing exceptions
                MsgHandler.QueueMessage($"ERROR PARSING: {excMsg} | {exception.StackTrace}", Serilog.Events.LogEventLevel.Warning);
                inputSequence.ParsedInputResult = ParsedInputResults.Invalid;
            }

            //Check for non-valid messages
            if (inputSequence.ParsedInputResult != ParsedInputResults.Valid)
            {
                //Display error message for invalid inputs
                if (inputSequence.ParsedInputResult == ParsedInputResults.Invalid)
                {
                    MsgHandler.QueueMessage(inputSequence.Error);
                }

                return;
            }

            #region Parser Post-Process Validation

            /* All this validation may be able to be performed faster.
             * Find a way to speed it up.
             */

            long globalInputPermLevel = DataHelper.GetSettingInt(SettingsConstants.GLOBAL_INPUT_LEVEL, 0L);
            int  userControllerPort   = 0;
            long userLevel            = 0;

            using (BotDBContext context = DatabaseManager.OpenContext())
            {
                User user = DataHelper.GetUserNoOpen(e.UsrMessage.Username, context);

                //Check if the user is silenced and ignore the message if so
                if (user.HasEnabledAbility(PermissionConstants.SILENCED_ABILITY) == true)
                {
                    return;
                }

                //Ignore based on user level and permissions
                if (user.Level < globalInputPermLevel)
                {
                    MsgHandler.QueueMessage($"Inputs are restricted to levels {(PermissionLevels)globalInputPermLevel} and above.");
                    return;
                }

                userControllerPort = (int)user.ControllerPort;
                userLevel          = user.Level;
            }

            //First, add delays between inputs if we should
            //We do this first so we can validate the inserted inputs later
            //The blank inputs can have a different permission level
            if (DataHelper.GetUserOrGlobalMidInputDelay(e.UsrMessage.Username, out long midInputDelay) == true)
            {
                MidInputDelayData midInputDelayData = ParserPostProcess.InsertMidInputDelays(inputSequence, userControllerPort, (int)midInputDelay, usedConsole);

                //If it's successful, replace the input list and duration
                if (midInputDelayData.Success == true)
                {
                    int oldDur = inputSequence.TotalDuration;
                    inputSequence.Inputs        = midInputDelayData.NewInputs;
                    inputSequence.TotalDuration = midInputDelayData.NewTotalDuration;

                    //TRBotLogger.Logger.Debug($"Mid input delay success. Message: {midInputDelayData.Message} | OldDur: {oldDur} | NewDur: {inputSequence.TotalDuration}\n{ReverseParser.ReverseParse(inputSequence, usedConsole, new ReverseParser.ReverseParserOptions(ReverseParser.ShowPortTypes.ShowAllPorts, 0))}");
                }
            }

            InputValidation validation = default;

            using (BotDBContext context = DatabaseManager.OpenContext())
            {
                User user = DataHelper.GetUserNoOpen(userName, context);

                //Check for restricted inputs on this user
                validation = ParserPostProcess.InputSequenceContainsRestrictedInputs(inputSequence, user.GetRestrictedInputs());

                if (validation.InputValidationType != InputValidationTypes.Valid)
                {
                    if (string.IsNullOrEmpty(validation.Message) == false)
                    {
                        MsgHandler.QueueMessage(validation.Message);
                    }
                    return;
                }

                //Check for invalid input combinations
                validation = ParserPostProcess.ValidateInputCombos(inputSequence, usedConsole.InvalidCombos,
                                                                   DataContainer.ControllerMngr, usedConsole);

                if (validation.InputValidationType != InputValidationTypes.Valid)
                {
                    if (string.IsNullOrEmpty(validation.Message) == false)
                    {
                        MsgHandler.QueueMessage(validation.Message);
                    }
                    return;
                }
            }

            //Check for level permissions and ports
            validation = ParserPostProcess.ValidateInputLvlPermsAndPorts(userLevel, inputSequence,
                                                                         DataContainer.ControllerMngr, usedConsole.ConsoleInputs);
            if (validation.InputValidationType != InputValidationTypes.Valid)
            {
                if (string.IsNullOrEmpty(validation.Message) == false)
                {
                    MsgHandler.QueueMessage(validation.Message, Serilog.Events.LogEventLevel.Warning);
                }
                return;
            }

            #endregion

            //Make sure inputs aren't stopped
            if (InputHandler.InputsHalted == true)
            {
                //We can't process inputs because they're currently stopped
                MsgHandler.QueueMessage("New inputs cannot be processed until all other inputs have stopped.", Serilog.Events.LogEventLevel.Warning);
                return;
            }

            //Fetch these values ahead of time to avoid passing the database context through so many methods
            long   autoPromoteEnabled  = DataHelper.GetSettingInt(SettingsConstants.AUTO_PROMOTE_ENABLED, 0L);
            long   autoPromoteInputReq = DataHelper.GetSettingInt(SettingsConstants.AUTO_PROMOTE_INPUT_REQ, long.MaxValue);
            long   autoPromoteLevel    = DataHelper.GetSettingInt(SettingsConstants.AUTO_PROMOTE_LEVEL, -1L);
            string autoPromoteMsg      = DataHelper.GetSettingString(SettingsConstants.AUTOPROMOTE_MESSAGE, string.Empty);

            bool addedInputCount = false;

            TRBotLogger.Logger.Debug($"Reverse Parsed (post-process): " + ReverseParser.ReverseParse(inputSequence, usedConsole,
                                                                                                     new ReverseParser.ReverseParserOptions(ReverseParser.ShowPortTypes.ShowNonDefaultPorts, defaultPort,
                                                                                                                                            ReverseParser.ShowDurationTypes.ShowNonDefaultDurations, defaultDur)));

            //Get the max recorded inputs per-user
            long maxUserRecInps = DataHelper.GetSettingInt(SettingsConstants.MAX_USER_RECENT_INPUTS, 0L);

            //It's a valid input - save it in the user's stats
            //Also record the input if we should
            using (BotDBContext context = DatabaseManager.OpenContext())
            {
                User user = DataHelper.GetUserNoOpen(e.UsrMessage.Username, context);

                //Ignore if the user is opted out
                if (user.IsOptedOut == false)
                {
                    user.Stats.ValidInputCount++;
                    addedInputCount = true;

                    context.SaveChanges();

                    //If we should store recent user inputs, do so
                    if (maxUserRecInps > 0)
                    {
                        //Get the input sequence - we may have added mid input delays between
                        //As a result, we'll need to reverse parse it
                        string message = ReverseParser.ReverseParse(inputSequence, usedConsole,
                                                                    new ReverseParser.ReverseParserOptions(ReverseParser.ShowPortTypes.ShowNonDefaultPorts, (int)user.ControllerPort,
                                                                                                           ReverseParser.ShowDurationTypes.ShowNonDefaultDurations, defaultDur));

                        //Add the recorded input
                        user.RecentInputs.Add(new RecentInput(message));
                        context.SaveChanges();

                        int diff = user.RecentInputs.Count - (int)maxUserRecInps;

                        //If we're over the max after adding, remove
                        if (diff > 0)
                        {
                            //Order by ascending ID and take the difference
                            //Lower IDs = older entries
                            IEnumerable <RecentInput> shouldRemove = user.RecentInputs.OrderBy(r => r.UserID).Take(diff);

                            foreach (RecentInput rec in shouldRemove)
                            {
                                user.RecentInputs.Remove(rec);
                                context.SaveChanges();
                            }
                        }
                    }
                }
            }

            bool autoPromoted = false;

            //Check if auto promote is enabled and auto promote the user if applicable
            if (addedInputCount == true)
            {
                using (BotDBContext context = DatabaseManager.OpenContext())
                {
                    User user = DataHelper.GetUserNoOpen(e.UsrMessage.Username, context);

                    //Check if the user was already autopromoted, autopromote is enabled,
                    //and if the user reached the autopromote input count requirement
                    if (user.Stats.AutoPromoted == 0 && autoPromoteEnabled > 0 &&
                        user.Stats.ValidInputCount >= autoPromoteInputReq)
                    {
                        //Only autopromote if this is a valid permission level
                        //We may not want to log or send a message for this, as it has potential to be very spammy,
                        //and it's not something the users can control
                        if (PermissionHelpers.IsValidPermissionValue(autoPromoteLevel) == true)
                        {
                            //Mark the user as autopromoted and save
                            user.Stats.AutoPromoted = 1;
                            autoPromoted            = true;

                            context.SaveChanges();
                        }
                    }
                }
            }

            if (autoPromoted == true)
            {
                //If the user is already at or above this level, don't set them to it
                //Only set if the user is below
                if (userLevel < autoPromoteLevel)
                {
                    //Adjust abilities and promote to the new level
                    DataHelper.AdjustUserLvlAndAbilitiesOnLevel(userName, autoPromoteLevel);

                    if (string.IsNullOrEmpty(autoPromoteMsg) == false)
                    {
                        PermissionLevels permLvl  = (PermissionLevels)autoPromoteLevel;
                        string           finalMsg = autoPromoteMsg.Replace("{0}", userName).Replace("{1}", permLvl.ToString());
                        MsgHandler.QueueMessage(finalMsg);
                    }
                }
            }

            InputModes inputMode = (InputModes)DataHelper.GetSettingInt(SettingsConstants.INPUT_MODE, 0L);

            //If the mode is Democracy, add it as a vote for this input
            if (inputMode == InputModes.Democracy)
            {
                //Set up the routine if it doesn't exist
                BaseRoutine      foundRoutine     = RoutineHandler.FindRoutine(RoutineConstants.DEMOCRACY_ROUTINE_ID, out int indexFound);
                DemocracyRoutine democracyRoutine = null;

                if (foundRoutine == null)
                {
                    long voteTime = DataHelper.GetSettingInt(SettingsConstants.DEMOCRACY_VOTE_TIME, 10000L);

                    democracyRoutine = new DemocracyRoutine(voteTime);
                    RoutineHandler.AddRoutine(democracyRoutine);
                }
                else
                {
                    democracyRoutine = (DemocracyRoutine)foundRoutine;
                }

                democracyRoutine.AddInputSequence(userName, inputSequence.Inputs);
            }
            //If it's Anarchy, carry out the input
            else
            {
                /************************************
                * Finally carry out the inputs now! *
                ************************************/

                InputHandler.CarryOutInput(inputSequence.Inputs, usedConsole, DataContainer.ControllerMngr);
            }
        }
        public override void ExecuteCommand(EvtChatCommandArgs args)
        {
            List <string> arguments          = args.Command.ArgumentsAsList;
            string        userName           = args.Command.ChatMessage.Username.ToLowerInvariant();
            long          userControllerPort = 0;
            long          userLevel          = 0;

            using (BotDBContext context = DatabaseManager.OpenContext())
            {
                User user = DataHelper.GetUserNoOpen(userName, context);
                if (user == null)
                {
                    QueueMessage("Huh, looks like you're not in the database!");
                    return;
                }

                if (user.HasEnabledAbility(PermissionConstants.INPUT_EXERCISE_ABILITY) == false)
                {
                    QueueMessage("You do not have the ability to use input exercises!");
                    return;
                }

                userControllerPort = user.ControllerPort;
                userLevel          = user.Level;
            }

            //Get the last console used
            int lastConsoleID = (int)DataHelper.GetSettingInt(SettingsConstants.LAST_CONSOLE, 1L);

            GameConsole usedConsole = null;

            using (BotDBContext context = DatabaseManager.OpenContext())
            {
                GameConsole lastConsole = context.Consoles.FirstOrDefault(c => c.ID == lastConsoleID);

                if (lastConsole != null)
                {
                    //Create a new console using data from the database
                    usedConsole = new GameConsole(lastConsole.Name, lastConsole.InputList, lastConsole.InvalidCombos);
                }
            }

            //If there are no valid inputs, don't attempt to generate or solve
            if (usedConsole == null)
            {
                QueueMessage($"The current console does not point to valid data, making it impossible to solve or generate a new exercise. Please set a different console to use, or if none are available, add one.");
                return;
            }

            if (usedConsole.ConsoleInputs.Count == 0)
            {
                QueueMessage($"The current console, \"{usedConsole.Name}\", does not have any available inputs. Cannot determine solve or generate an exercise.");
                return;
            }

            string creditsName  = DataHelper.GetCreditsName();
            int    botCharLimit = (int)DataHelper.GetSettingInt(SettingsConstants.BOT_MSG_CHAR_LIMIT, 500L);

            ReverseParser.ReverseParserOptions parseOptions = new ReverseParser.ReverseParserOptions(ReverseParser.ShowPortTypes.None, (int)userControllerPort);

            //Handle no arguments
            if (arguments.Count == 0)
            {
                if (UserExercises.TryGetValue(userName, out InputExercise inputExercise) == true)
                {
                    OutputInputExercise(inputExercise, usedConsole, botCharLimit, creditsName, inputExercise.ParseOptions);
                }
                else
                {
                    QueueMessage(NO_EXERCISE_FOUND_MSG);
                }
                return;
            }

            //If "new" is specified, generate a new input sequence
            if ((arguments.Count == 1 || arguments.Count == 2) && arguments[0].ToLowerInvariant() == GENERATE_NEW_ARG)
            {
                //Check for a difficulty argument
                if (arguments.Count == 2)
                {
                    string difString = arguments[1].ToLowerInvariant();

                    //Check difficulty level
                    if (difString == EASY_DIFFICULTY_ARG)
                    {
                        parseOptions.ShowPortType = ReverseParser.ShowPortTypes.None;
                    }
                    else if (difString == HARD_DIFFICULTY_ARG)
                    {
                        parseOptions.ShowPortType = ReverseParser.ShowPortTypes.ShowNonDefaultPorts;
                    }
                    else
                    {
                        QueueMessage($"Invalid difficulty level specified. Please choose either \"{EASY_DIFFICULTY_ARG}\" or \"{HARD_DIFFICULTY_ARG}\".");
                        return;
                    }
                }

                //Use the global default input duration for consistency
                int defaultInputDur = (int)DataHelper.GetSettingInt(SettingsConstants.DEFAULT_INPUT_DURATION, 200L);

                //Generate the exercise
                ParsedInputSequence newSequence = GenerateExercise(userLevel, defaultInputDur, usedConsole, parseOptions);

                //Give greater credit rewards for longer input sequences
                long creditReward = (newSequence.Inputs.Count * BASE_CREDIT_REWARD) * CREDIT_REWARD_MULTIPLIER;

                //This parser option is set when performing a hard exercise
                if (parseOptions.ShowPortType == ReverseParser.ShowPortTypes.ShowNonDefaultPorts)
                {
                    creditReward = (long)Math.Ceiling(creditReward * HARD_EXERCISE_MULTIPLIER);
                }

                InputExercise inputExercise = new InputExercise(newSequence, parseOptions, creditReward);
                UserExercises[userName] = inputExercise;

                OutputInputExercise(inputExercise, usedConsole, botCharLimit, creditsName, parseOptions);
                return;
            }

            //Make sure the user has an exercise - if not, output the same message
            if (UserExercises.TryGetValue(userName, out InputExercise exercise) == false)
            {
                QueueMessage(NO_EXERCISE_FOUND_MSG);
                return;
            }

            //There's more than one argument and the user has an exercise; so it has to be the input
            //Let's validate it!
            if (ExecuteValidateInput(userName, args.Command.ArgumentsAsString, usedConsole, parseOptions) == true)
            {
                using (BotDBContext context = DatabaseManager.OpenContext())
                {
                    User user = DataHelper.GetUserNoOpen(userName, context);

                    //Grant credits if the user isn't opted out
                    if (user.IsOptedOut == false)
                    {
                        long creditReward = exercise.CreditReward;
                        user.Stats.Credits += creditReward;
                        context.SaveChanges();

                        QueueMessage($"Correct input! Awesome job! You've earned your {creditReward} {creditsName.Pluralize(false, creditReward)}!");
                    }
                    else
                    {
                        QueueMessage("Correct input! Awesome job!");
                    }
                }

                //Remove the entry
                UserExercises.TryRemove(userName, out InputExercise value);
            }
        }
        public override void ExecuteCommand(EvtChatCommandArgs args)
        {
            string userName         = args.Command.ChatMessage.Username;
            string argInputSequence = args.Command.ArgumentsAsString;

            if (string.IsNullOrEmpty(argInputSequence) == true)
            {
                string periodicInputSequence = DataHelper.GetSettingString(SettingsConstants.PERIODIC_INPUT_VALUE, string.Empty);
                if (string.IsNullOrEmpty(periodicInputSequence) == true)
                {
                    QueueMessage("The current periodic input sequence is not defined! You can define one by providing an input sequence as an argument.");
                }

                int botCharLimit = (int)DataHelper.GetSettingInt(SettingsConstants.BOT_MSG_CHAR_LIMIT, 500L);

                QueueMessageSplit($"The current periodic input sequence is: {periodicInputSequence}", botCharLimit, string.Empty);
                return;
            }

            //Replace all non-space whitespace in the argument with space for readability
            //Some platforms will do this by default (Ex. Twitch), but we can't rely on them all being consistent here
            argInputSequence = Helpers.ReplaceAllWhitespaceWithSpace(argInputSequence);

            long globalInputPermLevel = DataHelper.GetSettingInt(SettingsConstants.GLOBAL_INPUT_LEVEL, 0L);

            //Consider the user's level so they cannot use this to perform inputs normally unavailable to them
            long userLevel = 0L;

            using (BotDBContext context = DatabaseManager.OpenContext())
            {
                User user = DataHelper.GetUserNoOpen(userName, context);

                //Check for permissions
                if (user == null || user.HasEnabledAbility(PermissionConstants.SET_PERIODIC_INPUT_SEQUENCE_ABILITY) == false)
                {
                    QueueMessage("You do not have the ability to set the periodic input sequence!");
                    return;
                }

                //Check if the user is silenced
                if (user.HasEnabledAbility(PermissionConstants.SILENCED_ABILITY) == true)
                {
                    QueueMessage("Nice try, but you can't set the periodic input sequence while silenced!");
                    return;
                }

                //Ignore based on user level and permissions
                if (user.Level < globalInputPermLevel)
                {
                    QueueMessage($"Inputs are restricted to levels {(PermissionLevels)globalInputPermLevel} and above, so you can't set the periodic input sequence.");
                    return;
                }

                userLevel = user.Level;
            }

            //Parse the input sequence
            //Set up the console
            GameConsole usedConsole = null;

            int lastConsoleID = (int)DataHelper.GetSettingInt(SettingsConstants.LAST_CONSOLE, 1L);

            using (BotDBContext context = DatabaseManager.OpenContext())
            {
                GameConsole lastConsole = context.Consoles.FirstOrDefault(c => c.ID == lastConsoleID);

                if (lastConsole != null)
                {
                    //Create a new console using data from the database
                    usedConsole = new GameConsole(lastConsole.Name, lastConsole.InputList, lastConsole.InvalidCombos);
                }
            }

            //If there are no valid inputs, don't attempt to parse
            if (usedConsole == null)
            {
                DataContainer.MessageHandler.QueueMessage("The current console does not point to valid data. Please set a different console to use, or if none are available, add one.");
                return;
            }

            if (usedConsole.ConsoleInputs.Count == 0)
            {
                DataContainer.MessageHandler.QueueMessage($"The current console, \"{usedConsole.Name}\", does not have any available inputs.");
                return;
            }

            int defaultDur             = (int)DataHelper.GetUserOrGlobalDefaultInputDur(string.Empty);
            int maxDur                 = (int)DataHelper.GetUserOrGlobalMaxInputDur(string.Empty);
            int defaultPeriodicInpPort = (int)DataHelper.GetSettingInt(SettingsConstants.PERIODIC_INPUT_PORT, 0L);

            ParsedInputSequence inputSequence = default;

            try
            {
                string regexStr = usedConsole.InputRegex;

                string readyMessage = string.Empty;

                Parser parser = new Parser();

                using (BotDBContext context = DatabaseManager.OpenContext())
                {
                    //Get input synonyms for this console
                    IQueryable <InputSynonym> synonyms = context.InputSynonyms.Where(syn => syn.ConsoleID == lastConsoleID);

                    //Prepare the message for parsing
                    readyMessage = parser.PrepParse(argInputSequence, context.Macros, synonyms);
                }

                //Parse inputs to get our parsed input sequence
                inputSequence = parser.ParseInputs(readyMessage, regexStr, new ParserOptions(defaultPeriodicInpPort, defaultDur, true, maxDur));
            }
            catch (Exception exception)
            {
                string excMsg = exception.Message;

                //Handle parsing exceptions
                inputSequence.ParsedInputResult = ParsedInputResults.Invalid;

                DataContainer.MessageHandler.QueueMessage($"Couldn't parse periodic input sequence: {excMsg}");
                return;
            }

            //Check for non-valid messages
            if (inputSequence.ParsedInputResult != ParsedInputResults.Valid)
            {
                string message = inputSequence.Error;
                if (string.IsNullOrEmpty(message) == true)
                {
                    message = "Input is invalid";
                }

                DataContainer.MessageHandler.QueueMessage($"Cannot set periodic input sequence: {message}");
                return;
            }

            /* Perform some post-processing ahead of time since this input will be carried out */

            using (BotDBContext context = DatabaseManager.OpenContext())
            {
                User user = DataHelper.GetUserNoOpen(userName, context);

                //Check for restricted inputs on this user
                InputValidation inpValidation = ParserPostProcess.InputSequenceContainsRestrictedInputs(inputSequence,
                                                                                                        user.GetRestrictedInputs());

                if (inpValidation.InputValidationType != InputValidationTypes.Valid)
                {
                    if (string.IsNullOrEmpty(inpValidation.Message) == false)
                    {
                        QueueMessage(inpValidation.Message);
                    }
                    return;
                }
            }

            //Check for level permissions and ports
            InputValidation validation = ParserPostProcess.ValidateInputLvlPermsAndPorts(userLevel, inputSequence,
                                                                                         DataContainer.ControllerMngr, usedConsole.ConsoleInputs);

            if (validation.InputValidationType != InputValidationTypes.Valid)
            {
                if (string.IsNullOrEmpty(validation.Message) == false)
                {
                    QueueMessage(validation.Message);
                }

                return;
            }

            //Defer input combo validation to when it's about to be be executed
            //This way, the state of the virtual controllers now shouldn't
            //affect an otherwise valid combo from being accepted

            string newPeriodicInpSequence = ReverseParser.ReverseParse(inputSequence, usedConsole,
                                                                       new ReverseParser.ReverseParserOptions(ReverseParser.ShowPortTypes.ShowNonDefaultPorts,
                                                                                                              defaultPeriodicInpPort, ReverseParser.ShowDurationTypes.ShowNonDefaultDurations, defaultDur));

            /*
             * Finally, after everything is good, set the input sequence!
             */
            using (BotDBContext context = DatabaseManager.OpenContext())
            {
                Settings periodicInputSeqSetting = DataHelper.GetSettingNoOpen(SettingsConstants.PERIODIC_INPUT_VALUE, context);
                periodicInputSeqSetting.ValueStr = newPeriodicInpSequence;

                context.SaveChanges();
            }

            QueueMessage("Successfully set the periodic input sequence!");
        }