diff --git a/.editorconfig b/.editorconfig index d21af99f..84a6e742 100644 --- a/.editorconfig +++ b/.editorconfig @@ -42,3 +42,6 @@ dotnet_diagnostic.SA1507.severity = error # SE1516: Using directives should be separated by blank line. dotnet_diagnostic.SA1516.severity = error +# CS8618: Non nullable field _name is not initialized. Consider declare the field as nullable type +dotnet_diagnostic.CS8618.severity = none + diff --git a/.idea/.idea.ScriptedEvents/.idea/.gitignore b/.idea/.idea.ScriptedEvents/.idea/.gitignore new file mode 100644 index 00000000..6b74f4d4 --- /dev/null +++ b/.idea/.idea.ScriptedEvents/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/modules.xml +/.idea.ScriptedEvents.iml +/contentModel.xml +/projectSettingsUpdater.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.idea.ScriptedEvents/.idea/dictionaries/Andrzej.xml b/.idea/.idea.ScriptedEvents/.idea/dictionaries/Andrzej.xml new file mode 100644 index 00000000..e91e052e --- /dev/null +++ b/.idea/.idea.ScriptedEvents/.idea/dictionaries/Andrzej.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/.idea.ScriptedEvents/.idea/encodings.xml b/.idea/.idea.ScriptedEvents/.idea/encodings.xml new file mode 100644 index 00000000..df87cf95 --- /dev/null +++ b/.idea/.idea.ScriptedEvents/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.ScriptedEvents/.idea/indexLayout.xml b/.idea/.idea.ScriptedEvents/.idea/indexLayout.xml new file mode 100644 index 00000000..7b08163c --- /dev/null +++ b/.idea/.idea.ScriptedEvents/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.ScriptedEvents/.idea/vcs.xml b/.idea/.idea.ScriptedEvents/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/.idea.ScriptedEvents/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 81778423..19b25538 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ # ScriptedEvents SCP:SL Exiled plugin to create event "scripts". These scripts can be set up to run once per round, multiple times per round, or by command only. +![Repository analytics](https://repobeats.axiom.co/api/embed/1959377c83f76a4c53448880a81a55c09e6abe15.svg "Repobeats analytics image") + ## Getting Started Fair warning: This plugin is very complex and has a lot of features. However, once you understand it, the capabilities are close to endless. My best suggestion is to play around with the plugin, as that's the easiest way to learn it. Tips to get started include: * Read the documents that are generated when you first install the plugin and restart the server. Your console will tell you where they are located the first time (typically directly inside the Configs folder). diff --git a/ScriptedEvents/API/APITestLab/ScriptedEventsIntegration.cs b/ScriptedEvents/API/APITestLab/ScriptedEventsIntegration.cs index e23f6b90..bd8c4963 100644 --- a/ScriptedEvents/API/APITestLab/ScriptedEventsIntegration.cs +++ b/ScriptedEvents/API/APITestLab/ScriptedEventsIntegration.cs @@ -8,7 +8,6 @@ using Exiled.API.Features; using Exiled.Loader; - using UnityEngine; /// /// The class for Scripted Events custom action integration. @@ -46,7 +45,7 @@ internal static class ScriptedEventsIntegration internal static MethodInfo RemoveAction => API?.GetMethod("UnregisterCustomAction"); /// - /// Gets the MethodInfo for removing a custom action. + /// Gets the MethodInfo for getting the players from a variable. /// internal static MethodInfo APIGetPlayersMethod => API?.GetMethod("GetPlayers"); @@ -55,26 +54,23 @@ internal static class ScriptedEventsIntegration /// internal static List CustomActions { get; } = new(); -#pragma warning disable SA1629 // Documentation text should end with a period /// /// Registers a custom action. /// /// The name of the action. /// The action implementation. /// - /// Action implementation is Func>, where: + /// Action implementation is Func.>, where: /// /// Tuple - the action input, where: - /// string[] - The input to the action. Usually represented by single word strings, BUT can also include multiple words in one string. - /// object - The script in which the action was ran. + /// string[] - the input to the action. Usually represented by single word strings, BUT can also include multiple words in one string. + /// object - the script in which the action was ran. /// /// Tuple - the action result, where: - /// bool - Did action execute without any errors. - /// string - The action response to the console when there was an error.. - /// object[] - optional values to return from an action, either strings or Player[]s, anything different will result in an error. + /// bool - did action execute without any errors. + /// string - action response to the console when there was an error. + /// object[] - optional values to return from an action, only STRINGS or PLAYER ARRAYS, anything different will result in an ERROR!!!!!!!!!!!! /// -#pragma warning restore SA1629 // Documentation text should end with a period - public static void RegisterCustomAction(string name, Func, Tuple> action) { try @@ -187,12 +183,12 @@ public static void UnregisterCustomActions() } /// - /// Gets the MethodInfo for getting the players from a variable. + /// Gets the player objects from a SE variable. /// /// The input to process. /// The script as object. /// The number of players to return (-1 for unlimited). - /// The list of players. + /// Player objects inhabiting the variable. internal static Player[] GetPlayers(string input, object script, int max = -1) { return (Player[])APIGetPlayersMethod.Invoke(null, new[] { input, script, max }); diff --git a/ScriptedEvents/API/Constants/ConstMessages.cs b/ScriptedEvents/API/Constants/ConstMessages.cs index db76f72c..c534fa89 100644 --- a/ScriptedEvents/API/Constants/ConstMessages.cs +++ b/ScriptedEvents/API/Constants/ConstMessages.cs @@ -1,4 +1,6 @@ -namespace ScriptedEvents.API.Constants +using PlayerRoles; + +namespace ScriptedEvents.API.Constants { using System; using System.Linq; @@ -50,12 +52,15 @@ The following keys can ONLY be used in DISABLE and ENABLE. They cannot be tied t - 'HeavyContainment' - Targets heavy containment rooms - 'Entrance' - Targets entrance rooms -Alternatively, a Room ID can be used. A full list of valid Room IDs (as of {DateTime.Now:g}) follows: +Alternatively, a Room ID can be used. A full list of valid RoomType inputs (as of {DateTime.Now:g}) follows: {string.Join("\n", ((RoomType[])Enum.GetValues(typeof(RoomType))).Where(r => r is not RoomType.Unknown).Select(r => $"- [{r:d}] {r}"))}"; - public static readonly string ItemInput = $@" A full list of valid Item IDs (as of {DateTime.Now:g}) follows: + public static readonly string ItemInput = $@" A full list of valid ItemType inputs (as of {DateTime.Now:g}) follows: {string.Join("\n", ((ItemType[])Enum.GetValues(typeof(ItemType))).Where(r => r is not ItemType.None).Select(r => $"- [{r:d}] {r}"))} Alternatively, the ID of a CustomItem can be used."; + + public static readonly string RoleTypeInput = $@" A full list of valid RoleTypeId inputs (as of {DateTime.Now:g}) follows: +{string.Join("\n", ((RoleTypeId[])Enum.GetValues(typeof(ItemType))).Where(r => r is not RoleTypeId.None).Select(r => $"- [{r:d}] {r}"))}"; } } diff --git a/ScriptedEvents/API/Enums/ActionSubgroup.cs b/ScriptedEvents/API/Enums/ActionSubgroup.cs deleted file mode 100644 index 4d467299..00000000 --- a/ScriptedEvents/API/Enums/ActionSubgroup.cs +++ /dev/null @@ -1,88 +0,0 @@ -namespace ScriptedEvents.API.Enums -{ - /// - /// Represents a group to give to each action. - /// - public enum ActionSubgroup - { - /// - /// Broadcast-related action. - /// - Broadcast, - - /// - /// Cassie-related action. - /// - Cassie, - - /// - /// Debugging-related action. - /// - Debug, - - /// - /// Health-related action. - /// - Health, - - /// - /// Item-related action. - /// - Item, - - /// - /// Facility lighting related action. - /// - Lights, - - /// - /// Logic action, such as IFs and STOPs. - /// - Logic, - - /// - /// Map-related action. - /// - Map, - - /// - /// Uncategorized action. - /// - Misc, - - /// - /// Player-related action. - /// - Player, - - /// - /// Round-related action. - /// - Round, - - /// - /// Round Rule related action. - /// - RoundRule, - - /// - /// Variable-related action. - /// - Variable, - - /// - /// Yielding action. - /// - Yielding, - - /// - /// Server action. - /// - Server, - - /// - /// Teleportation action. - /// - Teleportation, - } -} diff --git a/ScriptedEvents/API/Extensions/InterfaceExtensions.cs b/ScriptedEvents/API/Extensions/InterfaceExtensions.cs index f033a454..d88bd797 100644 --- a/ScriptedEvents/API/Extensions/InterfaceExtensions.cs +++ b/ScriptedEvents/API/Extensions/InterfaceExtensions.cs @@ -1,46 +1,15 @@ -namespace ScriptedEvents.API.Extensions +using ScriptedEvents.Interfaces; + +namespace ScriptedEvents.API.Extensions { using System; using System.Linq; using ScriptedEvents.API.Features; - using ScriptedEvents.API.Interfaces; using ScriptedEvents.Variables.Interfaces; public static class InterfaceExtensions { - public static string String(this IVariable variable, Script source = null, bool reversed = false) - { - try - { - switch (variable) - { - case IBoolVariable @bool: - bool result = reversed ? !@bool.Value : @bool.Value; - return result.ToUpper(); - case IFloatVariable @float: - return @float.Value.ToString(); - case ILongVariable @long: - return @long.Value.ToString(); - case IStringVariable @string: - return @string.Value; - default: // Shouldn't be possible - throw new InvalidCastException($"{variable.Name} tried to cast to string, which resulted in an error."); - } - } - catch (InvalidCastException e) - { - Logger.Warn(source?.Debug == true ? e.ToString() : e.Message, source); - } - catch (Exception e) - { - Logger.Warn(source?.Debug == true ? e.ToString() : e.Message, source); - return source?.Debug == true ? e.ToString() : e.Message; - } - - return "ERROR"; - } - /// /// Determines if an action is obsolete. /// diff --git a/ScriptedEvents/API/Extensions/PlayerExtensions.cs b/ScriptedEvents/API/Extensions/PlayerExtensions.cs new file mode 100644 index 00000000..cb5dd54b --- /dev/null +++ b/ScriptedEvents/API/Extensions/PlayerExtensions.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using Exiled.API.Features; +using ScriptedEvents.API.Modules; + +namespace ScriptedEvents.API.Extensions +{ + public static class PlayerExtensions + { + public static Dictionary PlayerDataVariables(this Player plr) + { + if (VariableSystem.PlayerDataVariables.TryGetValue(plr, out var value)) + { + return value; + } + + var dict = new Dictionary(); + VariableSystem.PlayerDataVariables.Add(plr, dict); + return dict; + } + } +} diff --git a/ScriptedEvents/API/Extensions/StringExtensions.cs b/ScriptedEvents/API/Extensions/StringExtensions.cs index 458e5c35..14fa64a6 100644 --- a/ScriptedEvents/API/Extensions/StringExtensions.cs +++ b/ScriptedEvents/API/Extensions/StringExtensions.cs @@ -2,9 +2,10 @@ { using System.Linq; using System.Text; + using System.Text.RegularExpressions; using Exiled.API.Features.Pools; - using ScriptedEvents.API.Modules; + using ScriptedEvents.API.Features; using ScriptedEvents.Structures; /// @@ -19,55 +20,48 @@ public static class StringExtensions /// The new string, without any whitespace characters. public static string RemoveWhitespace(this string input) { - // StackOverflow my beloved - string newString = string.Empty; - char[] chars = input.ToCharArray(); - bool isCurrentlyInVariable = false; - foreach (char c in chars) - { - if (c == '{') - isCurrentlyInVariable = true; - else if (c == '}') - isCurrentlyInVariable = false; - - if (isCurrentlyInVariable) - newString += c; - else if (!char.IsWhiteSpace(c)) - newString += c; - } - - return newString; + return Regex.Replace(input, @"\s+", string.Empty); } - public static bool IsBool(this string input, out bool value, Script source = null) + public static bool IsBool(this string input, out bool value, out ErrorInfo? errorInfo, Script? script = null) { - if (input is null) + if (script is not null) + input = Parser.ReplaceContaminatedValueSyntax(input, script); + + if (string.IsNullOrEmpty(input)) { - value = false; + value = default; + errorInfo = new( + "Empty value cannot be intrpreted as a true/false value", + "The provided value is null or empty, which does not qualify for a true/false value.", + "IsBool Extension"); return false; } - else if (bool.TryParse(input, out bool r)) + + if (bool.TryParse(input, out var r)) { value = r; - return true; - } - else if (input.ToUpper() is "YES" or "Y" or "T") - { - value = true; - return true; - } - else if (input.ToUpper() is "NO" or "N" or "F") - { - value = false; + errorInfo = null; return true; } - if (source is not null && VariableSystemV2.TryGetVariable(input, source, out VariableResult result) && result.ProcessorSuccess) + switch (input.ToUpper()) { - return IsBool(result.String(), out value, source); + case "YES" or "Y" or "T": + value = true; + errorInfo = null; + return true; + case "NO" or "N" or "F": + value = false; + errorInfo = null; + return true; } value = false; + errorInfo = new( + $"Value '{input}' cannot be interpreted as a true/false value", + "The provided value does not match any criteria by which it could be assigned a true/false value.", + "IsBool Extension"); return false; } @@ -77,9 +71,9 @@ public static bool IsBool(this string input, out bool value, Script source = nul /// The input string. /// The script source. /// The boolean. - public static bool AsBool(this string input, Script source = null) + public static bool AsBool(this string input, Script? source = null) { - IsBool(input, out bool v, source); + IsBool(input, out var v, out _, source); return v; } @@ -108,22 +102,63 @@ public static bool AsBool(this string input, Script source = null) /// Amount of parameters to skip. /// The separator string. /// The new string. - public static string JoinMessage(this object[] param, int skipCount = 0, string sep = " ") + public static string JoinMessage(this object?[] param, int skipCount = 0, string sep = " ") { StringBuilder sb = StringBuilderPool.Pool.Get(); var list = param.Skip(skipCount); - if (list.Count() == 0) return string.Empty; - foreach (object obj in list) + // ReSharper disable once CanReplaceCastWithVariableType + var enumerable = list.Where(x => x is not null).ToArray() as object[]; + if (!enumerable.Any()) return string.Empty; + + foreach (var obj in enumerable) { if (obj is string s) sb.Append(s + sep); else - sb.Append(obj.ToString() + sep); + sb.Append(obj + sep); } - string str = StringBuilderPool.Pool.ToStringReturn(sb); + var str = StringBuilderPool.Pool.ToStringReturn(sb); return str.Substring(0, str.Length - sep.Length); } + + /// + /// Joins object parameters into a string message. + /// + /// The parameters. + /// Amount of parameters to skip. + /// The separator string. + /// The new string. + public static string JoinMessage(this string[] param, int skipCount = 0, string sep = " ") + { + StringBuilder sb = StringBuilderPool.Pool.Get(); + var list = param.Skip(skipCount); + var enumerable = list as object[] ?? list.ToArray(); + if (!enumerable.Any()) return string.Empty; + + foreach (var obj in enumerable) + { + if (obj is string s) + sb.Append(s + sep); + else + sb.Append(obj + sep); + } + + var str = StringBuilderPool.Pool.ToStringReturn(sb); + return str.Substring(0, str.Length - sep.Length); + } + + public static int CountOccurrences(this string text, char character) + { + int count = 0; + + foreach (char c in text) + { + if (c == character) count++; + } + + return count; + } } } diff --git a/ScriptedEvents/API/Extensions/TimeSpanExtensions.cs b/ScriptedEvents/API/Extensions/TimeSpanExtensions.cs new file mode 100644 index 00000000..0bcb0d54 --- /dev/null +++ b/ScriptedEvents/API/Extensions/TimeSpanExtensions.cs @@ -0,0 +1,24 @@ +namespace ScriptedEvents.API.Extensions +{ + using System; + + /// + /// Contains useful extensions. + /// + public static class TimeSpanExtensions + { + public static T ToSeconds(this TimeSpan input) + where T : struct, IConvertible + { + try + { + return (T)Convert.ChangeType(input.TotalSeconds, typeof(T)); + } + catch + { + throw new InvalidCastException( + $"TimeSpan extension `ToSeconds` failed. Provided type {nameof(T)} is not valid."); + } + } + } +} diff --git a/ScriptedEvents/API/Features/ApiHelper.cs b/ScriptedEvents/API/Features/ApiHelper.cs index 64878265..b4d7ae17 100644 --- a/ScriptedEvents/API/Features/ApiHelper.cs +++ b/ScriptedEvents/API/Features/ApiHelper.cs @@ -1,12 +1,14 @@ -namespace ScriptedEvents.API.Features +using System.Linq; +using ScriptedEvents.Actions.DebugActions; +using ScriptedEvents.API.Modules; + +namespace ScriptedEvents.API.Features { using System; - using System.Collections.Generic; using System.Reflection; using Exiled.API.Features; using ScriptedEvents.Actions; - using ScriptedEvents.API.Modules; using ScriptedEvents.Structures; /// @@ -16,7 +18,7 @@ public static class ApiHelper { public static bool IsModuleLoaded() { - return MainPlugin.ScriptModule is not null; + return ScriptModule.Singleton is not null; } /// @@ -25,7 +27,7 @@ public static bool IsModuleLoaded() /// The assembly to search through. public static void RegisterActions(Assembly assembly) { - MainPlugin.ScriptModule.RegisterActions(assembly); + ScriptModule.Singleton!.RegisterActions(assembly); } /// @@ -48,7 +50,7 @@ public static void RegisterActions(this Type plugin) /// The name of the action. /// The function to execute when the action is used. An array string of parameters is provided, and the action must return a tuple with a bool (successful?) and a string message (optional, can be set to string.Empty). /// A string message representing whether or not the unregister process was successful. - public static string RegisterCustomAction(string name, Func, Tuple> action) + public static string RegisterCustomAction(string? name, Func, Tuple>? action) { if (!IsModuleLoaded()) { @@ -62,13 +64,13 @@ public static string RegisterCustomAction(string name, Func /// The name of the action. /// A string message representing whether or not the unregister process was successful. - public static string UnregisterCustomAction(string name) + public static string UnregisterCustomAction(string? name) { if (name is null) { @@ -87,12 +89,12 @@ public static string UnregisterCustomAction(string name) name = name.ToUpper(); - if (!MainPlugin.ScriptModule.CustomActions.ContainsKey(name)) + if (!ScriptModule.Singleton!.CustomActions.ContainsKey(name)) { return "The custom action with the provided name does not exist."; } - MainPlugin.ScriptModule.CustomActions.Remove(name); + ScriptModule.Singleton!.CustomActions.Remove(name); return "Success"; } @@ -101,7 +103,7 @@ public static string UnregisterCustomAction(string name) /// /// A string array of action names. /// A string message representing whether or not the unregister process was successful. - public static string UnregisterCustomActions(string[] actionNames) + public static string UnregisterCustomActions(string[]? actionNames) { if (actionNames is null) { @@ -126,27 +128,27 @@ public static string UnregisterCustomActions(string[] actionNames) /// Input string. /// Script object. /// Maximum amount of players to get. Leave below zero for unlimited. - /// A of players. - public static Player[] GetPlayers(string input, object script, int max = -1) + /// An array of players. + public static Player[]? GetPlayers(string input, object script, int max = -1) { if (script is Script actualScript == false) { return null; } - ScriptModule.TryGetPlayers(input, max, out PlayerCollection list, actualScript); - return list.GetInnerList().ToArray(); + Parser.TryGetPlayers(input, max, out var list, actualScript, out _); + return list.ToArray(); } /// - /// Evaluates a string math equation, replacing all variables in the string. + /// Replaces all variables and tries to perform math. /// /// The input string. /// Script object. /// A tuple indicating success and the value. public static Tuple Math(string input, Script script) { - bool success = ConditionHelperV2.TryMath(VariableSystemV2.ReplaceVariables(input, script), out MathResult result); + bool success = ConditionHelper.TryMath(Parser.ReplaceContaminatedValueSyntax(input, script), out MathResult result); return new(success, result.Result); } } diff --git a/ScriptedEvents/API/Features/ArgumentProcessor.cs b/ScriptedEvents/API/Features/ArgumentProcessor.cs index 127aa23a..2bdeb8dd 100644 --- a/ScriptedEvents/API/Features/ArgumentProcessor.cs +++ b/ScriptedEvents/API/Features/ArgumentProcessor.cs @@ -1,17 +1,16 @@ -namespace ScriptedEvents.API.Features +using ScriptedEvents.Interfaces; + +namespace ScriptedEvents.API.Features { using System; using System.Collections.Generic; using System.Linq; + using System.Text.RegularExpressions; using Exiled.API.Features; using Exiled.API.Features.Doors; - using Exiled.API.Features.Items; using PlayerRoles; - - using ScriptedEvents.API.Enums; using ScriptedEvents.API.Extensions; - using ScriptedEvents.API.Interfaces; using ScriptedEvents.API.Modules; using ScriptedEvents.Structures; using ScriptedEvents.Variables.Interfaces; @@ -27,139 +26,85 @@ public static class ArgumentProcessor /// The expected arguments. /// The provided arguments. /// The action or variable performing the process. - /// The script source. - /// If brackets are required to convert variables. + /// The script source. /// The result of the process. - public static ArgumentProcessResult Process(Argument[] expectedArguments, string[] args, IScriptComponent action, Script source, bool requireBrackets = true) + public static ArgumentProcessResult ProcessActionArguments(Argument[] expectedArguments, string[] args, IAction action, Script script) { - if (args is null) + if (expectedArguments.Length == 0) { - Logger.Debug("[ARGPROC] There are no raw arguments provided for this action. Ending processing.", source); + Log("This action doesnt use arguments. Ending processing."); return new(true); } - if (args.Length != 0) + int requiredArguments = expectedArguments.Count(arg => arg.Required); + if (args.Length < requiredArguments) { - Logger.Debug($"[ARGPROC] Arguments to process: {string.Join(", ", args)}", source); - - ArgumentProcessResult processedForLoop = HandlePlayerListComprehension(args, source, out string[] strippedArgs); - if (!processedForLoop.Success) - { - Logger.Debug("[$FOR @ ARGPROC] '$FOR' action decorator parsing failed. Ending processing.", source); - return processedForLoop; - } - else - { - Logger.Debug("[$FOR @ ARGPROC] '$FOR' action decorator parsing success. Continuing processing.", source); - } - - args = strippedArgs; - - int conditionSectionKeyword = args.IndexOf("$IF"); - if (conditionSectionKeyword != -1) - { - string[] conditionArgs = args.Skip(conditionSectionKeyword + 1).ToArray(); - args = args.Take(conditionSectionKeyword).ToArray(); - Logger.Debug($"[$IF @ ARGPROC] Evaluating condition: {string.Join(",", conditionArgs)}", source); - ConditionResponse resp = ConditionHelperV2.Evaluate(string.Join(" ", conditionArgs), source); - - if (!resp.Success) - { - Logger.Debug("[$IF @ ARGPROC] Evaluation resulted in an error. Ending processing.", source); - return new(false, true, string.Empty, resp.Message); - } - - if (!resp.Passed) - { - Logger.Debug("[$IF @ ARGPROC] Evaluation resulted in FALSE. Action shall not execute. Ending processing.", source); - return new(false); - } - else - { - Logger.Debug($"[$IF @ ARGPROC] Evaluation resulted in TRUE. Action shall execute like normal. Continuing parsing.", source); - } - } - else - { - Logger.Debug($"[$IF @ ARGPROC] No '$IF' syntax was found. Continuing parsing.", source); - } + IEnumerable labeledArgs = expectedArguments.Select(arg => $"({(arg.Required ? "Required" : "Optional")} '{arg.ArgumentName}')"); + return new( + false, + true, + Error( + $"Action '{action.Name}' is missing {requiredArguments - args.Length} arguments.", + $"Action defines these arguments: {string.Join(", ", labeledArgs)}, of which {requiredArguments - args.Length} required arguments were not provided.") + .ToTrace()); } - if (expectedArguments is null || expectedArguments.Length == 0) + ArgumentProcessResult success = new(true) { - Logger.Debug("[ARGPROC] There are no arguments for this action. Ending parsing.", source); - return new(true); - } - - int required = expectedArguments.Count(arg => arg.Required); + StrippedRawParameters = args.ToArray(), + }; - if (args.Length < required) + bool addNotExpectedArgsToParameters = true; + for (int i = 0; i < expectedArguments.Length; i++) { - IEnumerable args2 = expectedArguments.Select(arg => $"{(arg.Required ? "<" : "[")}{arg.ArgumentName}{(arg.Required ? ">" : "]")}"); - return new(false, true, string.Empty, ErrorGen.Get(ErrorCode.MissingArguments, action.Name, action is IAction ? "action" : "variable", required, string.Join(", ", args2))); - } + // assign null to the expected argument (thats not required!) if there are no more raw arguments + if (args.Length <= i) + { + success.NewParameters.Add(null); + continue; + } - ArgumentProcessResult success = new(true); + Argument argument = expectedArguments[i]; + string input = args[i]; - // raw args? aww hell nah :trollface: - List rawProcessedArgs = Exiled.API.Features.Pools.ListPool.Pool.Get(); - foreach (string arg in args) - { - if (TryProcessSmartArgument(arg, action, source, out string res, false)) + // if parsing MultiArgumentString, join all left arguments into one and end processing + if (argument.Type == typeof(MultiArgumentString)) { - rawProcessedArgs.Add(res); + success.NewParameters.Add(string.Join(" ", ParseForValueSyntax(args.Skip(i)))); + addNotExpectedArgsToParameters = false; + break; } - else + + ArgumentProcessResult res = ProcessIndividualParameter(argument, input, action, script); + if (!res.ShouldExecute) { - rawProcessedArgs.Add(arg); + return res; // Throw issue to end-user } + + success.NewParameters.Add(res.NewParameters.First()); } - success.StrippedRawParameters = rawProcessedArgs.ToArray(); + // arguments which are not defined as expected have their value syntax replaced before being added + // this should not happen if MultiArgumentString is used + if (addNotExpectedArgsToParameters) + success.NewParameters.AddRange(ParseForValueSyntax(args.Skip(expectedArguments.Length))); + + Log($"Processed action parameters: '{string.Join(", ", success.NewParameters.Select(x => x?.ToString()))}'"); + return success; - for (int i = 0; i < expectedArguments.Length; i++) + void Log(string message) { - Argument expect = expectedArguments[i]; - string input = string.Empty; - - if (args.Length > i) - input = args[i]; - else - continue; - - ArgumentProcessResult res = ProcessIndividualParameter(expect, input, action, source, requireBrackets); - if (!res.Success) return res; // Throw issue to end-user - - success.NewParameters.AddRange(res.NewParameters); + if (!script.IsDebug) return; + Logger.Debug($"[ArgumentProcessor] [ProcessActionArguments] [{action.Name}] {message}", script); } - // If the raw argument list is larger than the expected list, do not process any extra arguments - // Edge-cases with long strings being the last parameter - if (args.Length > expectedArguments.Length) + IEnumerable ParseForValueSyntax(IEnumerable value) { - // TODO: Figure out a method where ReplaceVariables isn't called for each extra argument. - // While also allowing variables + strings to be combined - // Eg. Using 'ReplaceVariable' instead won't turn '{PLAYERS}test' into '0test' like expected. - // This works for now, we need version 3 :| - IEnumerable extraArgs = args.Skip(expectedArguments.Length); - foreach (string arg in extraArgs) - { - if (TryProcessSmartArgument(arg, action, source, out string saResult, true)) - { - success.NewParameters.Add(saResult); - } - else - { - success.NewParameters.Add(VariableSystemV2.ReplaceVariables(arg, source)); - } - - Logger.Debug("New parameters: " + string.Join(", ", success.NewParameters, source)); - } + return value.Select(arg => + TryProcessAttachedArgumentsInContaminatedString(arg, action, script, out string saRes) + ? Parser.ReplaceContaminatedValueSyntax(saRes, script) + : Parser.ReplaceContaminatedValueSyntax(arg, script)); } - - success.NewParameters.RemoveAll(o => o is string st && string.IsNullOrWhiteSpace(st)); - - return success; } /// @@ -169,58 +114,50 @@ public static ArgumentProcessResult Process(Argument[] expectedArguments, string /// The action or variable performing the process. /// The script source. /// The resulting string. Empty if method returns false. - /// TShould fetched values from smart params be processed. /// The output of the process. - public static bool TryProcessSmartArgument(string input, IScriptComponent action, Script source, out string result, bool processForVariables) + public static bool TryProcessAttachedArgumentsInContaminatedString(string input, IAction action, Script source, out string result) { - result = string.Empty; bool didSomething = false; - if (action is not IAction actualAction) - { - return false; - } + // Regex pattern to match '#' followed by a digit + Regex regex = new(@"#(\d)"); + result = input; // Start with input as the base result - bool skipAddingTrailingNumber = false; - for (int i = 0; i < input.Length; i++) - { - char c = input[i]; - if (skipAddingTrailingNumber) - { - skipAddingTrailingNumber = false; - continue; - } + var matches = regex.Matches(input); - result += c; + foreach (Match match in matches) + { + int index = match.Index; - if (c != '#') + // Try to parse the number after '#' + if (!int.TryParse(match.Groups[1].Value, out int lastNum) || lastNum < 1) { continue; } - Logger.Debug($"[SMART ARG PROC] Found '#' syntax at index {i}", source); + Logger.Debug($"[ATTACHED ARG PROC] Found '#' syntax with index '{lastNum}' at position {index}", source); - int lastNum; + string argument; try { - lastNum = (int)char.GetNumericValue(input[i + 1]); - } - catch (IndexOutOfRangeException) - { - continue; - } - - if (lastNum == -1) - { - continue; - } + // Fetch smart argument based on index + var res = source.AttachedArguments[action][lastNum - 1](); + if (res.Item1 is not null) + { + Logger.ScriptError(res.Item1!, source); + continue; + } - Logger.Debug($"[SMART ARG PROC] Found a index '{lastNum}' behind the '#'", source); + if (res.Item3 != typeof(string)) + { + var trace = Error( + "Invalid type returned from a attached argument", + $"The value under the smart argument '{match.Value}' is not a literal value, but value of type '{res.Item3!.Name}'.") + .ToTrace(); + Logger.ScriptError(trace, source); + } - string argument; - try - { - argument = source.SmartArguments[actualAction][lastNum - 1]; + argument = (string)res.Item2!; } catch (IndexOutOfRangeException) { @@ -233,27 +170,78 @@ public static bool TryProcessSmartArgument(string input, IScriptComponent action Logger.Debug($"[SMART ARG PROC] Index '{lastNum}' is valid", source); - if (processForVariables) - { - argument = VariableSystemV2.ReplaceVariables(argument, source); - - if (ConditionHelperV2.TryMath(argument, out MathResult mathRes)) - { - argument = mathRes.Result.ToString(); - } - } - - result = result.Substring(0, result.Length - 1); - - result += argument; + // Replace the '#' with the processed argument + result = result.Replace(match.Value, argument); didSomething = true; - skipAddingTrailingNumber = true; + Logger.Debug($"[SMART ARG PROC] Success! Smart arg used correctly. Result: {result}", source); } return didSomething; } + /// + /// Tries to process the argument for a quick argument. + /// + /// The provided input. + /// The action or variable performing the process. + /// The script source. + /// The resulting string. Empty if method returns false. + /// The output of the process. + public static bool TryProcessAttachedArgument(string input, IAction action, Script source, out object? result, out Type? type) + { + result = null; + type = null; + + // Regex pattern to match '#' followed by a digit + Regex regex = new(@"#(\d)"); + result = input; // Start with input as the base result + + var matches = regex.Matches(input); + + if (matches.Count != 1) + { + return false; + } + + var match = matches[0]; + + if (match.Length != input.Length) + { + return false; + } + + // Try to parse the number after '#' + if (!int.TryParse(match.Groups[1].Value, out int lastNum) || lastNum < 1) + { + return false; + } + + try + { + // Fetch smart argument based on index + var res = source.AttachedArguments[action][lastNum - 1](); + if (res.Item1 is not null) + { + Logger.ScriptError(res.Item1, source); + return false; + } + + result = res.Item2!; + type = res.Item3!; + } + catch (IndexOutOfRangeException) + { + return false; + } + catch (KeyNotFoundException) + { + return false; + } + + return true; + } + /// /// Processes an individual argument. /// @@ -261,237 +249,260 @@ public static bool TryProcessSmartArgument(string input, IScriptComponent action /// The provided input. /// The action or variable performing the process. /// The script source. - /// If brackets are required to convert variables. /// The output of the process. - public static ArgumentProcessResult ProcessIndividualParameter(Argument expected, string input, IScriptComponent action, Script source, bool requireBrackets = true) + public static ArgumentProcessResult ProcessIndividualParameter(Argument expected, string input, IAction action, Script source) { ArgumentProcessResult success = new(true); - source.DebugLog($"[C: {action.Name}] Param {expected.ArgumentName} needs a {expected.Type.Name}"); + Log($"Parameter '{expected.ArgumentName}' needs a '{expected.Type}' type."); // Extra magic for options if (expected is OptionsArgument options) { - if (!options.Options.Any(o => o.Name.ToUpper() == input.ToUpper()) && options is not SuggestedOptionsArgument) - return new(false, true, expected.ArgumentName, ErrorGen.Get(ErrorCode.ParameterError_Option, input, expected.ArgumentName, action.Name, string.Join(", ", options.Options.Select(x => x.Name)))); + if (options.Options.All(o => !string.Equals(o.Name, input, StringComparison.CurrentCultureIgnoreCase)) + && options is not SuggestedOptionsArgument) + { + return new( + false, + true, + Error( + $"Input '{input}' is not recongnized by option argument '{options.ArgumentName}' of action '{action.Name}'", + $"This argument only supports one of the following: '{string.Join("', '", options.Options.Select(o => o.Name))}'.") + .ToTrace()); + } success.NewParameters.Add(input); - source?.DebugLog($"[OPTION ARG] [C: {action.Name}] Param {expected.ArgumentName} now has a processed value '{success.NewParameters.Last()}' and raw value '{input}'"); + Log($"[OPTION ARG] Parameter '{expected.ArgumentName}' now has a value '{input}'"); return success; } + if (TryProcessAttachedArgument(input, action, source, out var smartArgRes, out var type)) + { + if (expected.Type == type) + { + success.NewParameters.Add(smartArgRes!); + } + } + // smart action arguments - if (TryProcessSmartArgument(input, action, source, out string saResult, true)) + if (TryProcessAttachedArgumentsInContaminatedString(input, action, source, out var saResult)) { input = saResult; } switch (expected.Type.Name) { - // Number Types: case "Boolean": - if (!input.IsBool(out bool result, source)) - return new(false, true, expected.ArgumentName, ErrorGen.Get(ErrorCode.InvalidBoolean, input)); + if (!input.IsBool(out var result, out var boolErr, source)) + { + return ErrorByInfo(boolErr!); + } success.NewParameters.Add(result); break; + case "Int32": // int - if (!SEParser.TryParse(input, out int intRes, source, requireBrackets)) - return new(false, true, expected.ArgumentName, ErrorGen.Get(ErrorCode.InvalidInteger, input)); + if (!Parser.TryCast(int.TryParse, input, source, out var intRes, out var intErr)) + { + ErrorByInfo(intErr!); + } success.NewParameters.Add(intRes); break; + case "Int64": // long - if (!SEParser.TryParse(input, out long longRes, source, requireBrackets)) - return new(false, true, expected.ArgumentName, ErrorGen.Get(ErrorCode.InvalidInteger, input)); + if (!Parser.TryCast(long.TryParse, input, source, out var longRes, out var longErr)) + { + ErrorByInfo(longErr!); + } success.NewParameters.Add(longRes); break; + case "Single": // float - if (!SEParser.TryParse(input, out float floatRes, source, requireBrackets)) - return new(false, true, expected.ArgumentName, ErrorGen.Get(ErrorCode.InvalidNumber, input)); + if (!Parser.TryCast(float.TryParse, input, source, out var floatRes, out var floatErr)) + { + ErrorByInfo(floatErr!); + } success.NewParameters.Add(floatRes); break; + + case "UInt16": // ushort + if (!Parser.TryCast(ushort.TryParse, input, source, out var ushortRes, out var ushortErr)) + { + ErrorByInfo(ushortErr!); + } + + success.NewParameters.Add(ushortRes); + break; + case "Char": - if (!char.TryParse(input, out char charRes)) - return new(false, true, expected.ArgumentName, ErrorGen.Get(ErrorCode.InvalidCharacter, input)); + if (!Parser.TryCast(char.TryParse, input, source, out var charRes, out var charErr)) + { + ErrorByInfo(charErr!); + } success.NewParameters.Add(charRes); break; - // Variable Interfaces - case "IConditionVariable": - if (!VariableSystemV2.TryGetVariable(input, source, out VariableResult variable, requireBrackets)) - return new(false, true, expected.ArgumentName, ErrorGen.Get(ErrorCode.InvalidVariable, input)); + case "Item": throw new NotImplementedException(); - success.NewParameters.Add(variable.Variable); + case "TimeSpan": + if (!Parser.TryGetTimeSpan(input, out TimeSpan timeSpan, out ErrorInfo? timeSpanErr)) + { + return ErrorByInfo(timeSpanErr!); + } + + success.NewParameters.Add(timeSpan); break; - case "IStringVariable": - if (!VariableSystemV2.TryGetVariable(input, source, out VariableResult variable2, requireBrackets)) - return new(false, true, expected.ArgumentName, ErrorGen.Get(ErrorCode.InvalidVariable, input)); - if (variable2.Variable is not IStringVariable strVar) - return new(false, true, expected.ArgumentName, ErrorGen.Get(ErrorCode.InvalidStringVariable, input)); + + case "Script": throw new NotImplementedException(); + + case "ItemType[]": throw new NotImplementedException(); - success.NewParameters.Add(strVar); - break; - case "IPlayerVariable": - if (!VariableSystemV2.TryGetVariable(input, source, out VariableResult variable3, requireBrackets)) - return new(false, true, expected.ArgumentName, ErrorGen.Get(ErrorCode.InvalidVariable, input)); - if (variable3.Variable is not IPlayerVariable playerVar) - return new(false, true, expected.ArgumentName, ErrorGen.Get(ErrorCode.InvalidPlayerVariable, input)); + case "IVariable": + if (!VariableSystem.TryGetVariable(input, source, out var someVar, false, out var someVarTrace)) + { + return ErrorByTrace(someVarTrace!); + } - success.NewParameters.Add(playerVar); + success.NewParameters.Add(someVar!); break; - case "IItemVariable": - if (!VariableSystemV2.TryGetVariable(input, source, out VariableResult variable4, requireBrackets)) - return new(false, true, expected.ArgumentName, ErrorGen.Get(ErrorCode.InvalidVariable, input)); - if (variable4.Variable is not IItemVariable itemVar) + case "ILiteralVariable": + if (!VariableSystem.TryGetVariable(input, source, out var strVar, false, + out var strVarTrace)) + { + return ErrorByTrace(strVarTrace!); + } - // TODO: ??? - return new(false, true, expected.ArgumentName, ErrorGen.Get(ErrorCode.InvalidVariable, input)); - if (Item.Get(itemVar.Value) is null) - return new(false, true, expected.ArgumentName, "The provided item variable is not valid."); + success.NewParameters.Add(strVar!); + break; - success.NewParameters.Add(itemVar); + case "IPlayerVariable": + if (!VariableSystem.TryGetVariable(input, source, out var plrVar, false, + out var plrVarTrace)) + { + return ErrorByTrace(plrVarTrace!); + } + + success.NewParameters.Add(plrVar!); break; - // Array Types: case "Room[]": - if (!ScriptModule.TryGetRooms(input, out Room[] rooms, source)) - return new(false, true, expected.ArgumentName, ErrorGen.Get(ErrorCode.ParameterError_Rooms, input, expected.ArgumentName)); + if (!Parser.TryGetRooms(input, out var rooms, source, out var roomError)) + { + return ErrorByInfo(roomError!); + } success.NewParameters.Add(rooms); break; + case "Door[]": - if (!ScriptModule.TryGetDoors(input, out Door[] doors, source)) - return new(false, true, expected.ArgumentName, ErrorGen.Get(ErrorCode.InvalidDoor, input)); + if (!Parser.TryGetDoors(input, out var doors, source, out var doorError)) + { + return ErrorByInfo(doorError!); + } success.NewParameters.Add(doors); break; + case "Lift[]": - if (!ScriptModule.TryGetLifts(input, out Lift[] lifts, source)) - return new(false, true, expected.ArgumentName, ErrorGen.Get(ErrorCode.InvalidLift, input)); + if (!Parser.TryGetLifts(input, out var lifts, source, out var liftError)) + { + return ErrorByInfo(liftError!); + } success.NewParameters.Add(lifts); break; + + case "Player[]": + if (!Parser.TryGetPlayers(input, null, out var players, source, out var collectionError)) + { + return ErrorByTrace(collectionError!); + } - // Special - case "PlayerCollection": - if (!ScriptModule.TryGetPlayers(input, null, out PlayerCollection players, source, requireBrackets)) - return new(false, true, expected.ArgumentName, players.Message); - - success.NewParameters.Add(players); + success.NewParameters.Add(players as Player[] ?? players.ToArray()); break; case "Player": - if (!ScriptModule.TryGetPlayers(input, null, out PlayerCollection players1, source, requireBrackets)) - return new(false, true, expected.ArgumentName, players1.Message); - - if (players1.Length == 0) + if (!Parser.TryGetPlayers(input, null, out var players1, source, out var playerError)) { - return new(false, true, expected.ArgumentName, $"One player is required, but value '{input}' holds no players."); + return ErrorByTrace(playerError!); } - else if (players1.Length > 1) + + var enumerable = players1 as Player[] ?? players1.ToArray(); + switch (enumerable.Length) { - return new(false, true, expected.ArgumentName, $"One player is required, but value '{input}' holds more than one player ({players1.Length} players)."); + case 0: + return ErrorByInfo(Error( + $"Provided variable '{input}' has no players.", + $"There was one player expected, but no player is present.")); + case > 1: + return ErrorByInfo(Error( + $"Provided variable '{input}' has too many players.", + $"There was one player expected, but {enumerable.Length} players are present.")); } - success.NewParameters.Add(players1.FirstOrDefault()); - break; - - case "RoleTypeIdOrTeam": - if (SEParser.TryParse(input, out RoleTypeId rtResult, source, requireBrackets)) - success.NewParameters.Add(rtResult); - else if (SEParser.TryParse(input, out Team teamResult, source, requireBrackets)) - success.NewParameters.Add(teamResult); - else - return new(false, true, expected.ArgumentName, ErrorGen.Get(ErrorCode.InvalidRoleTypeOrTeam, input)); - + success.NewParameters.Add(enumerable.First()); break; default: // Handle all enum types if (expected.Type.BaseType == typeof(Enum)) { - object res = SEParser.Parse(input, expected.Type, source); - if (res is null) - return new(false, true, expected.ArgumentName, ErrorGen.Get(ErrorCode.InvalidEnumGeneric, input, expected.Type.Name)); + var genericMethod = typeof(ArgumentProcessor) + .GetMethod("TryGetEnum")! + .MakeGenericMethod(expected.Type); + + object?[] arguments = { input, null, source, null }; - success.NewParameters.Add(res); - break; + genericMethod.Invoke(null, arguments); + + if (arguments[3] is ErrorInfo errorInfo) + { + return ErrorByInfo(errorInfo); + } + + success.NewParameters.Add((arguments[1] as Enum)!); } - // Unsupported types: Parse variables in string and use that as a param (RawArguments are used for getting the raw string) - // TODO: ReplaceVariable works only when a "clean" variable is provided, meaning it doesnt work when provided things like ({PLAYERSALIVE}) - // so we need to fix that instead of calling ReplaceVariables all the time - success.NewParameters.Add(VariableSystemV2.ReplaceVariables(input, source, requireBrackets)); + success.NewParameters.Add(Parser.ReplaceContaminatedValueSyntax(input, source)); break; } - source?.DebugLog($"[C: {action.Name}] Param {expected.ArgumentName} has a processed value '{success.NewParameters.Last()}' and raw value '{input}'"); + Log($"Param '{expected.ArgumentName}' processed! STD value: '{success.NewParameters.Last()}' RAW value: '{input}'"); return success; - } - private static ArgumentProcessResult HandlePlayerListComprehension(string[] inArgs, Script source, out string[] args) - { - args = inArgs; - int loopSyntaxIndex = inArgs.IndexOf("$FOR"); - - if (loopSyntaxIndex == -1) + ArgumentProcessResult ErrorByTrace(ErrorTrace trace) { - Logger.Debug("$FOR: no syntax found.", source); - return new(true); + trace.Append(Error( + $"Failed to process argument '{expected.ArgumentName}' for action '{action.Name}'", + $"Provided input '{input}' is not possible to be interpreted as value of type '{expected.Type.MemberType}'.")); + return new(false, true, trace); } - string[] loopArgs = inArgs.Skip(loopSyntaxIndex + 1).ToArray(); - args = inArgs.Take(loopSyntaxIndex).ToArray(); - - string newPlayerVarName = loopArgs[0]; - string inKeyword = loopArgs[1]; - string playerVarNameLoopingThrough = loopArgs[2]; - - if (inKeyword != "IN") - Logger.Warn($"$FOR: statement requires 'IN' keyword, provided '{inKeyword}'.", source); - - List playersToLoop; - - if (source.PlayerLoopInfo is not null && source.PlayerLoopInfo.Line == source.CurrentLine) - { - playersToLoop = source.PlayerLoopInfo.PlayersToLoopThrough; - Logger.Debug("$FOR: first time init loop - copy player var", source); - } - else + ArgumentProcessResult ErrorByInfo(ErrorInfo error) { - if (!ScriptModule.TryGetPlayers(playerVarNameLoopingThrough, null, out PlayerCollection outPlayers, source)) - { - Logger.Debug("$FOR: provided player variable to loop through is invalid", source); - return new(false, true, playerVarNameLoopingThrough, ErrorGen.Get(ErrorCode.InvalidPlayerVariable, playerVarNameLoopingThrough)); - } - - playersToLoop = outPlayers.GetInnerList(); - Logger.Debug("$FOR: not first time init loop - use existing player var", source); + var trace = error.ToTrace(); + trace.Append(Error( + $"Failed to process argument '{expected.ArgumentName}' for action '{action.Name}'", + $"Provided input '{input}' is not possible to be interpreted as value of type '{expected.Type.Name}'.")); + return new(false, true, trace); } - if (playersToLoop.Count == 0) + void Log(string message) { - Logger.Debug("$FOR: players to loop through are 0, going to next action", source); - source.PlayerLoopInfo = null; - return new(false); + if (!source.IsDebug) return; + Logger.Debug($"[ArgumentProcessor] [PIP] [{action.Name}] " + message, source); } + } - Player player = playersToLoop.FirstOrDefault(); - playersToLoop.Remove(player); - - source.AddPlayerVariable(newPlayerVarName, string.Empty, new[] { player }); - - source.PlayerLoopInfo = new(source.CurrentLine, playersToLoop); - - source.Jump(source.CurrentLine); - - return new(true); + private static ErrorInfo Error(string name, string desc) + { + return new ErrorInfo(name, desc, "ArgumentProcessor"); } } } diff --git a/ScriptedEvents/API/Features/ConditionHelperV2.cs b/ScriptedEvents/API/Features/ConditionHelper.cs similarity index 95% rename from ScriptedEvents/API/Features/ConditionHelperV2.cs rename to ScriptedEvents/API/Features/ConditionHelper.cs index 0d45664e..7acd491d 100644 --- a/ScriptedEvents/API/Features/ConditionHelperV2.cs +++ b/ScriptedEvents/API/Features/ConditionHelper.cs @@ -1,287 +1,286 @@ -namespace ScriptedEvents.API.Features -{ - using System; - using System.Collections.Generic; - using System.Collections.ObjectModel; - using System.Data; - using System.Text.RegularExpressions; - - using ScriptedEvents.API.Modules; - using ScriptedEvents.Conditions.Floats; - using ScriptedEvents.Conditions.Interfaces; - using ScriptedEvents.Conditions.Strings; - using ScriptedEvents.Structures; - -#pragma warning disable SA1600 // Remove this later - public static class ConditionHelperV2 - { - // Constants - public const string AND = " AND "; - public const string OR = " OR "; - - public static readonly char[] IgnoreChars = new[] - { - '>', - '<', - '=', - '!', - }; - - // Conditions - - /// - /// Gets a of float operators. - /// - public static ReadOnlyCollection FloatConditions { get; } = new List() - { - new GreaterThan(), - new LessThan(), - new Equal(), - new NotEqual(), - - new LessThanOrEqualTo(), - new GreaterThanOrEqualTo(), - }.AsReadOnly(); - - /// - /// Gets a of string operators. - /// - public static ReadOnlyCollection StringConditions { get; } = new List() - { - new StringEqual(), - new StringNotEqual(), - new StringContains(), - new StringNotContains(), - }.AsReadOnly(); - - // Methods - - /// - /// Performs a math equation and returns the result. - /// - /// The string math equation. - /// The result of the math equation. - public static float Math(string expression) - { - // StackOverflow my beloved - DataTable loDataTable = new(); - DataColumn loDataColumn = new("Eval", typeof(double), expression); - loDataTable.Columns.Add(loDataColumn); - loDataTable.Rows.Add(0); - return (float)(double)loDataTable.Rows[0]["Eval"]; - } - - /// - /// Tries to perform a math equation. - /// - /// The string math equation. - /// A indicating the success, result, and the exception if any. - /// Whether or not the math was successful. - public static bool TryMath(string expression, out MathResult result) - { - try - { - float floatResult = Math(expression); - - result = new() { Success = true, Result = floatResult }; - } - catch (Exception ex) - { - result = new() { Success = false, Result = -1, Exception = ex }; - } - - return result.Success; - } - - public static List CaptureGroups(string input) - { - MatchCollection matches = Regex.Matches(input, @"\(([^)]*)\)"); - if (matches.Count == 0) - return new(1) { input }; - - List ret = new(); - - foreach (Match m in matches) - { - ret.Add(m.Groups[1].Value); - input = input.Replace($"({m.Groups[1].Value})", string.Empty); - } - - ret.Add(input); - ret.RemoveAll(r => string.IsNullOrWhiteSpace(r)); - return ret; - } - - public static ConditionResponse Evaluate(string input, Script source = null) - { - source?.DebugLog($"Evaluating condition: {input}"); - return EvaluateInternal(input, source); - } - - private static ConditionResponse EvaluateAndOr(string input) - { - string[] andSplit = input.Split(new[] { AND }, StringSplitOptions.RemoveEmptyEntries); - bool stillGo = true; - foreach (string fragAnd in andSplit) - { - string[] orSplit = fragAnd.Split(new[] { OR }, StringSplitOptions.RemoveEmptyEntries); - foreach (string fragOr in orSplit) - { - if (bool.TryParse(fragOr, out bool r)) - { - if (r is true) - { - stillGo = true; - break; - } - else - { - stillGo = false; - } - } - } - - if (!stillGo) - break; - } - - return new(true, stillGo, string.Empty); - } - - private static ConditionResponse EvaluateSingleCondition(string input, string raw) - { - // Goofball checks first - if (bool.TryParse(input, out bool r)) - return new(true, r, string.Empty); - - switch (input) - { - case "0": - return new(true, false, string.Empty); - case "1": - return new(true, true, string.Empty); - } - - // Attempt to run math first - IFloatCondition match = null; - foreach (var floatCondition in FloatConditions) - { - int index = input.IndexOf(floatCondition.Symbol); - - Logger.Debug($"CND: {floatCondition.GetType().FullName}"); - if (index != -1) - { - Logger.Debug($"INDEX: " + index); - Logger.Debug("SYM BEF: " + input[index - 1]); - Logger.Debug("SYM AFT: " + input[index + floatCondition.Symbol.Length]); - if (!IgnoreChars.Contains(input[index - 1]) && !IgnoreChars.Contains(input[index + floatCondition.Symbol.Length])) - { - Logger.Debug("MATCH: TRUE"); - match = floatCondition; - break; - } - } - - Logger.Debug("MATCH: FALSE"); - } - - if (match is not null) - { - string[] split = input.Split(new[] { match.Symbol }, StringSplitOptions.RemoveEmptyEntries); - if (split.Length < 2) - { - return new(false, false, $"[F] Malformed condition provided! Condition: {raw}"); - } - - if (TryMath(split[0], out MathResult res1) && TryMath(split[1], out MathResult res2)) - { - return new(true, match.Execute(res1.Result, res2.Result), string.Empty); - } - } - - // Math failed - compare strings directly - IStringCondition match2 = null; - foreach (var stringCondition in StringConditions) - { - int index = input.IndexOf(stringCondition.Symbol); - - Logger.Debug($"CND: {stringCondition.GetType().FullName}"); - if (index != -1) - { - Logger.Debug($"INDEX: " + index); - Logger.Debug("SYM BEF: " + input[index - 1]); - Logger.Debug("SYM AFT: " + input[index + stringCondition.Symbol.Length]); - if (!IgnoreChars.Contains(input[index - 1]) && !IgnoreChars.Contains(input[index + stringCondition.Symbol.Length])) - { - Logger.Debug("MATCH: TRUE"); - match2 = stringCondition; - break; - } - } - - Logger.Debug("MATCH: FALSE"); - } - - if (match2 is not null) - { - string[] split = input.Split(new[] { " " + match2.Symbol + " " }, StringSplitOptions.RemoveEmptyEntries); - if (split.Length < 2) - { - return new(false, false, $"[S] Malformed condition provided! Condition: {raw}"); - } - - return new(true, match2.Execute(split[0], split[1]), string.Empty); - } - - return new(false, false, $"Invalid condition operator provided! Condition: {raw}"); - } - - private static ConditionResponse EvaluateInternal(string input, Script source = null) - { - string convertedInput = input; - if (bool.TryParse(convertedInput, out bool boolResult)) - return new ConditionResponse(true, boolResult, string.Empty); - - List groups = CaptureGroups(input); - foreach (string group in groups) - { - source?.DebugLog($"GROUP: " + group); - string[] andSplit = group.Split(new[] { AND }, StringSplitOptions.RemoveEmptyEntries); - foreach (string fragAnd in andSplit) - { - source?.DebugLog($"FRAG [AND]: " + fragAnd); - string[] orSplit = fragAnd.Split(new[] { OR }, StringSplitOptions.RemoveEmptyEntries); - foreach (string fragOr in orSplit) - { - source?.DebugLog($"FRAG [OR]: " + fragOr); - string convertedFrag = VariableSystemV2.ReplaceVariables(fragOr, source); - ConditionResponse eval = EvaluateSingleCondition(convertedFrag, group); - if (!eval.Success) - { - return new(false, eval.Passed, eval.Message); - } - - input = input.Replace($"{fragOr}", eval.Passed.ToString().ToUpper()); - } - } - } - - source?.DebugLog($"CONVERTED INPUT: " + input); - - if (bool.TryParse(input, out bool r)) - return new(true, r, string.Empty); - - MatchCollection matches = Regex.Matches(input, @"\(([^)]*)\)"); - if (matches.Count > 0) - { - foreach (Match match in matches) - { - ConditionResponse conditionResult = EvaluateAndOr(match.Groups[1].Value); - input = input.Replace($"({match.Groups[1].Value})", conditionResult.Passed.ToString().ToUpper()); - } - } - - return EvaluateAndOr(input); - } - } -} +namespace ScriptedEvents.API.Features +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Data; + using System.Text.RegularExpressions; + + using ScriptedEvents.Conditions.Floats; + using ScriptedEvents.Conditions.Interfaces; + using ScriptedEvents.Conditions.Strings; + using ScriptedEvents.Structures; + +#pragma warning disable SA1600 // Remove this later + public static class ConditionHelper + { + // Constants + public const string AND = " AND "; + public const string OR = " OR "; + + public static readonly char[] IgnoreChars = new[] + { + '>', + '<', + '=', + '!', + }; + + // Conditions + + /// + /// Gets a of float operators. + /// + public static ReadOnlyCollection FloatConditions { get; } = new List() + { + new GreaterThan(), + new LessThan(), + new Equal(), + new NotEqual(), + + new LessThanOrEqualTo(), + new GreaterThanOrEqualTo(), + }.AsReadOnly(); + + /// + /// Gets a of string operators. + /// + public static ReadOnlyCollection StringConditions { get; } = new List() + { + new StringEqual(), + new StringNotEqual(), + new StringContains(), + new StringNotContains(), + }.AsReadOnly(); + + // Methods + + /// + /// Performs a math equation and returns the result. + /// + /// The string math equation. + /// The result of the math equation. + public static float Math(string expression) + { + // StackOverflow my beloved + DataTable loDataTable = new(); + DataColumn loDataColumn = new("Eval", typeof(double), expression); + loDataTable.Columns.Add(loDataColumn); + loDataTable.Rows.Add(0); + return (float)(double)loDataTable.Rows[0]["Eval"]; + } + + /// + /// Tries to perform a math equation. + /// + /// The string math equation. + /// A indicating the success, result, and the exception if any. + /// Whether or not the math was successful. + public static bool TryMath(string expression, out MathResult result) + { + try + { + float floatResult = Math(expression); + + result = new() { Success = true, Result = floatResult }; + } + catch (Exception ex) + { + result = new() { Success = false, Result = -1, Exception = ex }; + } + + return result.Success; + } + + public static List CaptureGroups(string input) + { + MatchCollection matches = Regex.Matches(input, @"\(([^)]*)\)"); + if (matches.Count == 0) + return new(1) { input }; + + List ret = new(); + + foreach (Match m in matches) + { + ret.Add(m.Groups[1].Value); + input = input.Replace($"({m.Groups[1].Value})", string.Empty); + } + + ret.Add(input); + ret.RemoveAll(r => string.IsNullOrWhiteSpace(r)); + return ret; + } + + public static ConditionResponse Evaluate(string input, Script source = null) + { + source?.DebugLog($"Evaluating condition: {input}"); + return EvaluateInternal(input, source); + } + + private static ConditionResponse EvaluateAndOr(string input) + { + string[] andSplit = input.Split(new[] { AND }, StringSplitOptions.RemoveEmptyEntries); + bool stillGo = true; + foreach (string fragAnd in andSplit) + { + string[] orSplit = fragAnd.Split(new[] { OR }, StringSplitOptions.RemoveEmptyEntries); + foreach (string fragOr in orSplit) + { + if (bool.TryParse(fragOr, out bool r)) + { + if (r is true) + { + stillGo = true; + break; + } + else + { + stillGo = false; + } + } + } + + if (!stillGo) + break; + } + + return new(true, stillGo, string.Empty); + } + + private static ConditionResponse EvaluateSingleCondition(string input, string raw) + { + // Goofball checks first + if (bool.TryParse(input, out bool r)) + return new(true, r, string.Empty); + + switch (input) + { + case "0": + return new(true, false, string.Empty); + case "1": + return new(true, true, string.Empty); + } + + // Attempt to run math first + IFloatCondition match = null; + foreach (var floatCondition in FloatConditions) + { + int index = input.IndexOf(floatCondition.Symbol); + + Logger.Debug($"CND: {floatCondition.GetType().FullName}"); + if (index != -1) + { + Logger.Debug($"INDEX: " + index); + Logger.Debug("SYM BEF: " + input[index - 1]); + Logger.Debug("SYM AFT: " + input[index + floatCondition.Symbol.Length]); + if (!IgnoreChars.Contains(input[index - 1]) && !IgnoreChars.Contains(input[index + floatCondition.Symbol.Length])) + { + Logger.Debug("MATCH: TRUE"); + match = floatCondition; + break; + } + } + + Logger.Debug("MATCH: FALSE"); + } + + if (match is not null) + { + string[] split = input.Split(new[] { match.Symbol }, StringSplitOptions.RemoveEmptyEntries); + if (split.Length < 2) + { + return new(false, false, $"[F] Malformed condition provided! Condition: {raw}"); + } + + if (TryMath(split[0], out MathResult res1) && TryMath(split[1], out MathResult res2)) + { + return new(true, match.Execute(res1.Result, res2.Result), string.Empty); + } + } + + // Math failed - compare strings directly + IStringCondition match2 = null; + foreach (var stringCondition in StringConditions) + { + int index = input.IndexOf(stringCondition.Symbol); + + Logger.Debug($"CND: {stringCondition.GetType().FullName}"); + if (index != -1) + { + Logger.Debug($"INDEX: " + index); + Logger.Debug("SYM BEF: " + input[index - 1]); + Logger.Debug("SYM AFT: " + input[index + stringCondition.Symbol.Length]); + if (!IgnoreChars.Contains(input[index - 1]) && !IgnoreChars.Contains(input[index + stringCondition.Symbol.Length])) + { + Logger.Debug("MATCH: TRUE"); + match2 = stringCondition; + break; + } + } + + Logger.Debug("MATCH: FALSE"); + } + + if (match2 is not null) + { + string[] split = input.Split(new[] { " " + match2.Symbol + " " }, StringSplitOptions.RemoveEmptyEntries); + if (split.Length < 2) + { + return new(false, false, $"[S] Malformed condition provided! Condition: {raw}"); + } + + return new(true, match2.Execute(split[0], split[1]), string.Empty); + } + + return new(false, false, $"Invalid condition operator provided! Condition: {raw}"); + } + + private static ConditionResponse EvaluateInternal(string input, Script source = null) + { + string convertedInput = input; + if (bool.TryParse(convertedInput, out bool boolResult)) + return new ConditionResponse(true, boolResult, string.Empty); + + List groups = CaptureGroups(input); + foreach (string group in groups) + { + source?.DebugLog($"GROUP: " + group); + string[] andSplit = group.Split(new[] { AND }, StringSplitOptions.RemoveEmptyEntries); + foreach (string fragAnd in andSplit) + { + source?.DebugLog($"FRAG [AND]: " + fragAnd); + string[] orSplit = fragAnd.Split(new[] { OR }, StringSplitOptions.RemoveEmptyEntries); + foreach (string fragOr in orSplit) + { + source?.DebugLog($"FRAG [OR]: " + fragOr); + string convertedFrag = Parser.ReplaceContaminatedValueSyntax(fragOr, source); + ConditionResponse eval = EvaluateSingleCondition(convertedFrag, group); + if (!eval.Success) + { + return new(false, eval.Passed, eval.Message); + } + + input = input.Replace($"{fragOr}", eval.Passed.ToString().ToUpper()); + } + } + } + + source?.DebugLog($"CONVERTED INPUT: " + input); + + if (bool.TryParse(input, out bool r)) + return new(true, r, string.Empty); + + MatchCollection matches = Regex.Matches(input, @"\(([^)]*)\)"); + if (matches.Count > 0) + { + foreach (Match match in matches) + { + ConditionResponse conditionResult = EvaluateAndOr(match.Groups[1].Value); + input = input.Replace($"({match.Groups[1].Value})", conditionResult.Passed.ToString().ToUpper()); + } + } + + return EvaluateAndOr(input); + } + } +} diff --git a/ScriptedEvents/API/Features/CoroutineHelper.cs b/ScriptedEvents/API/Features/CoroutineHelper.cs index b4f7ef58..53adb957 100644 --- a/ScriptedEvents/API/Features/CoroutineHelper.cs +++ b/ScriptedEvents/API/Features/CoroutineHelper.cs @@ -2,10 +2,7 @@ { using System.Collections.Generic; - using Exiled.API.Features; - using MEC; - using ScriptedEvents.Structures; public static class CoroutineHelper @@ -22,7 +19,7 @@ public static void AddCoroutine(string type, CoroutineHandle coroutine, Script s if (Coroutines.ContainsKey(type)) Coroutines[type].Add(data); else - Coroutines.Add(type, new List() { data }); + Coroutines.Add(type, new List { data }); if (coroutine.Tag is not null) data.Key = coroutine.Tag; diff --git a/ScriptedEvents/API/Features/ErrorGen.cs b/ScriptedEvents/API/Features/ErrorGen.cs deleted file mode 100644 index 0125ad68..00000000 --- a/ScriptedEvents/API/Features/ErrorGen.cs +++ /dev/null @@ -1,418 +0,0 @@ -namespace ScriptedEvents.API.Features -{ - using System; - using System.Collections.Generic; - using System.Collections.ObjectModel; - using System.Linq; - - using Exiled.API.Enums; - - using PlayerRoles; - using ScriptedEvents.API.Enums; - using ScriptedEvents.Structures; - - /// - /// Exposes API to generate consistent error messages throughout the entire plugin. - /// - public static class ErrorGen - { - /// - /// Get a from the given ID. - /// - /// Error ID. - /// An object. Its will be 0 if the operation was unsuccessful. - public static ErrorInfo GetError(int errorCode) => - ErrorList.Errors.FirstOrDefault(err => err.Id == errorCode); - - /// - /// Gets a from the given . - /// - /// Error code. - /// An object. Its will be 0 if the operation was unsuccessful. - public static ErrorInfo GetError(ErrorCode code) => - ErrorList.Errors.FirstOrDefault(err => err.Code == code); - - /// - /// Try-get a from the given error ID. - /// - /// Error ID. - /// The object. - /// Whether or not the process was successful. - public static bool TryGetError(int errorID, out ErrorInfo errorInfo) - { - errorInfo = GetError(errorID); - return errorInfo.Id != 0; - } - - /// - /// Try-get a from the given code. - /// - /// Error code. - /// The object. - /// Whether or not the process was successful. - public static bool TryGetError(ErrorCode errorCode, out ErrorInfo errorInfo) - { - errorInfo = GetError(errorCode); - return errorInfo.Id != 0; - } - - /// - /// Generates an error string given an error ID. - /// - /// Error ID. - /// Arguments for the error. - /// An error string. - public static string Generate(int errorID, params object[] arguments) - { - ErrorInfo err = GetError(errorID); - - if (err.Id == 0) - err = GetError(126); - - return string.Format(err.ToString(), arguments); - } - - /// - /// Generates an error string given an error code. - /// - /// Error code. - /// Arguments for the error. - /// An error string. - public static string Generate(ErrorCode errorCode, params object[] arguments) - { - ErrorInfo err = GetError(errorCode); - - if (err.Id == 0) - err = GetError(126); - - return string.Format(err.ToString(), arguments); - } - - /// - /// Generates an error string given an error ID. - /// - /// Error ID. - /// Arguments for the error. - /// An error string. - [Obsolete("Use overload with enum.")] - public static string Get(int errorID, params object[] arguments) => Generate(errorID, arguments); - - /// - /// Generates an error string given an error code. - /// - /// Error code. - /// Arguments for the error. - /// An error string. - public static string Get(ErrorCode errorCode, params object[] arguments) => Generate(errorCode, arguments); - } - - /// - /// Contains all error messages. - /// - internal static class ErrorList - { - /// - /// Gets a list of all possible SE errors. - /// - public static ReadOnlyCollection Errors { get; } = new List() - { - new ErrorInfo( - 100, - ErrorCode.AutoRun_Disabled, - "The '{0}' script is set to run each round, but the script is disabled!", - "This error occurs when a disabled script is set to run in the auto_run_scripts Exiled config. This error can be resolved by removing the script from the config option, or by enabling the script by removing its !-- DISABLED flag."), - - new ErrorInfo( - 101, - ErrorCode.AutoRun_NotFound, - "The '{0}' script is set to run each round, but the script is not found!", - "This error occurs when a script is specified to run automatically in the auto_run_scripts Exiled config, but the script cannot be found in the Scripted Events directory. This may either be due to a typo in the config, in the script name, or if the script doesn't exist. This error can be resolved by resolving any typos in the script name, by creating a script that doesn't exist, or by deleting a non-existent script from the config option."), - - new ErrorInfo( - 102, - ErrorCode.InvalidAction, - "Invalid action '{0}' detected in script '{1}'.", - "This error occurs when a script is read, and the script reader finds one or more invalid actions in the script. This error can be resolved by checking for typos in the name of the action."), - - new ErrorInfo( - 103, - ErrorCode.MultipleFlagDefs, - "Multiple definitions for the '{0}' flag detected in script {1}.", - "This error occurs when a script has multiple flag declarations (!-- [FLAG]) for the same flag. This error can be resolved by simply removing multiple declarations of the same flag -- these declarations are only necessary once per script."), - - new ErrorInfo( - 104, - ErrorCode.MultipleLabelDefs, - "Multiple definitions for the '{0}' label detected in script {1}.", - "This error occurs when a script has multiple labels (LABELNAME:) with the same name. This error can be resolved by renaming labels so that each label has a unique name."), - - new ErrorInfo( - 105, - ErrorCode.AutoRun_AdminEvent, - "The '{0}' script is set to run each round, but the script is marked as an admin event!", - "This error occurs when a script is specified to run automatically in the auto_run_scripts Exiled config, but the script is marked as an admin event via the !-- ADMINEVENT flag. Admin event scripts are not meant to be run automatically. This error can be resolved by removing the script from the auto_run_scripts config, or removing the !-- ADMINEVENT flag from the script."), - - new ErrorInfo( - 106, - ErrorCode.IOPermissionError, - "Unable to create the required ScriptedEvents directories due to a permission error. Please ensure that ScriptedEvents has proper system permissions to Exiled's Config folder.", - "This error occurs when Scripted Events is unable to initialize the required directory due to an unauthorized permission error (likely due to the PC or server machine's own antivirus software). This error can be resolved by adjusting the system's settings to allow Scripted Events to make directory changes, or creating the directories manually."), - - new ErrorInfo( - 107, - ErrorCode.IOError, - "Unable to load ScriptedEvents due to a directory error.", - "This error occurs when Scripted Events is unable to initialize the required directory due to any non-permission error. Please report this error in the Scripted Events Discord server."), - - new ErrorInfo( - 108, - ErrorCode.On_UnknownEvent, - "The specified event '{0}' in the 'On' config was not found!", - "This error occurs when an invalid event name is provided in the on Exiled config. This error can be resolved by checking for typos in the name of events and referencing Exiled's list of provided events."), - - new ErrorInfo( - 109, - ErrorCode.On_IncompatibleEvent, - "The '{0}' event is not currently compatible with the On config.", - "This error occurs when an unsupported event is present in the on Exiled config. Unfortunately, there isn't much of a solution here at the moment. Due to how the system works, events must have an event argument in order to be usable with the on config. 99% of Exiled's events DO have an event argument, but very few of them do not. These are the unsupported events.\r\n\r\nMaybe this error will be forever gone in the future!"), - - new ErrorInfo( - 110, - ErrorCode.On_DisabledScript, - "Error in 'On' handler (event: {0}): Script '{1}' is disabled!", - "This error occurs when a disabled script is present in the on Exiled config. This error can be resolved by removing the script from the config option, or by enabling the script by removing its !-- DISABLED flag."), - - new ErrorInfo( - 111, - ErrorCode.On_NotFoundScript, - "Error in 'On' handler (event: {0}): Script '{1}' cannot be found!", - "This error occurs when a script is present in the on Exiled config that is not present in the Scripted Events directory. This may either be due to a typo in the config, in the script name, or if the script doesn't exist. This error can be resolved by resolving any typos in the script name, by creating a script that doesn't exist, or by deleting a non-existent script from the config option."), - - new ErrorInfo( - 112, - ErrorCode.On_UnknownError, - "Error in 'On' handler (event: {0})", - "This error occurs when Scripted Events is unable to handle the 'on' config correctly due to an error. Please report this error in the Scripted Events Discord server."), - - new ErrorInfo( - 113, - ErrorCode.LEGACY_SafetyError, - "Script '{0}' exceeded safety limit of {1} actions per 1 second and has been force-stopped, saving from a potential crash. If this is intentional, add '!-- NOSAFETY' to the top of the script. All script loops should have a delay in them.", - "This error occurs when a script executes more actions in 1 second than what is allowed in the Exiled configs, per the max_actions_per_second configuration. This is likely an indicator that the script is trying to do too much at one time, and could be due to a loop without a yield. This error can be resolved by performing less actions per second, and adding delays (such as WAITSEC) where they wont cause issues. Additionally, the max_actions_per_second config can be raised, allowing more actions to occur per-second. Lastly, as a last-case resort, adding the !-- NOSAFETY flag to the top of a script disables this warning for that individual script."), - - new ErrorInfo( - 114, - ErrorCode.IOHelpPermissionError, - "Unable to create the help file, the plugin does not have permission to access the ScriptedEvents directory!", - "This error occurs when the HELP action is executed, however, ScriptedEvents is unable to create the documentation file due to an unauthorized permission error (likely due to the PC or server machine's own antivirus software). This error can be resolved by adjusting the system's settings to allow Scripted Events to write files, or by using the NOFILE argument in the HELP action."), - new ErrorInfo( - 115, - ErrorCode.IOHelpError, - "Error when writing to file.", - "This error occurs when the HELP action is executed, however, ScriptedEvents is unable to create the documentation file due to any non-permission error. Please report this error in the Scripted Events Discord server."), - new ErrorInfo( - 116, - ErrorCode.InvalidActionUsage, - "Invalid '{0}' action usage. Usage: {1}", - "This error occurs when an action is executed without the proper arguments. This error can be resolved by executing the action with the proper arguments, as specified in the error message.\r\n\r\n"), - - new ErrorInfo( - 117, - ErrorCode.LEGACY_InvalidActionUsage, - "Invalid '{0}' action usage.", - "This error occurs when an action is executed without the proper arguments. This error can be resolved by executing the action with the proper arguments. This error is obsolete and has been replaced with SE-116."), - - new ErrorInfo( - 118, - ErrorCode.ParameterError_Option, - "Invalid option {0} provided for the '{1}' parameter of the {2} action. This parameter expects one of the following options: {3}.", - "This error occurs when an action is executed successfully, however one of its parameters received a string value that it was not expecting. This error can be resolved by replacing the specified parameter with one of the specified options."), - - new ErrorInfo( - 119, - ErrorCode.ParameterError_Number, - "Invalid number '{0}' provided for the '{1}' parameter of the {2} action.", - "This error occurs when an action is executed successfully. However, a numerical parameter received a non-numerical value. This error can be resolved by replacing the value of the specified parameter with a numerical value."), - - new ErrorInfo( - 120, - ErrorCode.ParameterError_Condition, - "Invalid {0} condition provided in the {1} action! Condition: {2} Error type: '{3}' Message: '{4}'.", - "This error occurs when an action is executed successfully. However, a numerical parameter received a value that cannot be evaluated as numerical. This error can be resolved by replacing the specified parameter with a valid numerical value, or a valid mathematical formula."), - - new ErrorInfo( - 121, - ErrorCode.ParameterError_LessThanZeroNumber, - "Negative number '{0}' cannot be used in the '{1}' parameter of the {2} action.", - "This error occurs when an action is executed successfully. However, a numerical parameter received a less-than-zero value, which is invalid for the specified action. This error can be resolved by ensuring that the result of the numerical expression does not equal a less-than-zero value.\r\n\r\n"), - - new ErrorInfo( - 122, - ErrorCode.ParameterError_RoleType, - "Invalid {0} provided in the {1} action. '{2}' is not a valid RoleType.", - $"This error occurs when an action is executed successfully. However, a role parameter received an invalid role type. This error can be resolved by ensuring that the value of the parameter matches an internal RoleType value. A full list of valid RoleTypes (as of {DateTime.Now:g}) follows:\n{string.Join("\n", ((RoleTypeId[])Enum.GetValues(typeof(RoleTypeId))).Where(r => r is not RoleTypeId.None).Select(r => $"- [{r:d}] {r}"))}"), - - new ErrorInfo( - 123, - ErrorCode.ParameterError_Players, - "No players were found matching the given criteria ('{0}' parameter).", - "This error occurs when an action is executed successfully. However, no players were found matching the given variables. This error can be resolved by ensuring that there is at least one player match when running a script."), - - new ErrorInfo( - 124, - ErrorCode.ParameterError_Rooms, - "No rooms were found matching the given criteria '{0}' ('{1}' parameter).", - $"This error occurs when an action is executed successfully. However, no rooms were found matching the given names or IDs. This error can be resolved by ensuring that there are no typos in the name or ID of rooms. A full list of valid Room IDs (as of {DateTime.Now:g}) follows:\n{string.Join("\n", ((RoomType[])Enum.GetValues(typeof(RoomType))).Where(r => r is not RoomType.Unknown).Select(r => $"- [{r:d}] {r}"))}"), - - new ErrorInfo( - 125, - ErrorCode.ParameterError_CassieNoAnnc, - "Cannot show captions without a corresponding CASSIE announcement.", - "This error occurs when using the CASSIE and SILENTCASSIE actions, if a CASSIE caption is provided but no message is provided. This error can be resolved by ensuring that the 'message' portion of the text parameter is always provided."), - - new ErrorInfo( - 126, - ErrorCode.UnknownError, - "Unknown error", - "This error can occur if there is an action failure without a valid message. Please report this error in the Scripted Events Discord server."), - - new ErrorInfo( - 127, - ErrorCode.IOMissing, - "Critical error: Missing script path. Please reload plugin.", - "This error occurs when the Scripted Events directory is deleted while the plugin is running. Reloading the plugin usually fixes this issue."), - - new ErrorInfo( - 128, - ErrorCode.CustomCommand_NoName, - "Custom command is defined without a name.", - "This error occurs when there is a command specified in the commands Exiled config, but its name parameter is blank. This error can be resolved by simply providing a name to the command, or by removing the unfinished command structure from the config."), - - new ErrorInfo( - 129, - ErrorCode.CustomCommand_NoScripts, - "Custom command '{0}' ({1}) will not be created because it is set to run zero scripts.", - "This error occurs when there is a command specified in the commands Exiled config, but its run parameter is non-existent or empty. Custom commands must have at least one script set to run in order for the command to be created. As such, this error can be resolved by adding a script to run when the command is executed, or by removing the unfinished command structure from the config."), - - new ErrorInfo( - 130, - ErrorCode.MissingArguments, - "The '{0}' {1} requires {2} argument(s) ({3})", - "This error occurs when an action or variable is used with an insufficient amount of arguments. Most actions require arguments, separated by spaces, such as 'LOG TEST'. Likewise, some variables require arguments, separated by :, such as '{FILTER:PLAYERS:ROLE:ClassD}'. This error can be resolved by supplying the proper amount of arguments."), - - new ErrorInfo( - 131, - ErrorCode.LEGACY_InvalidPlayerVariable, - "The provided value '{0}' is not a valid variable or has no associated players.", - "This error occurs when a variable requires another player variable as an argument, but that other variable is not a valid variable. This error will also occur if the variable is valid, but is not a variable that contains players. This error can be resolved by providing a valid variable that contains players."), - - new ErrorInfo( - 132, - ErrorCode.InvalidVariable, - "The provided value '{0}' is not a valid variable.", - "This error occurs when a variable requires another variable as an argument, but that other variable is not a valid variable. This error can be resolved by providing a valid variable."), - - new ErrorInfo( - 133, - ErrorCode.InvalidPlayerVariable, - "The provided variable '{0}' has no associated players.", - "This error occurs when a variable requires another player variable as an argument, but the other variable is not a variable that contains players. This error can be resolved by providing a valid variable that contains players."), - - new ErrorInfo( - 134, - ErrorCode.InvalidInteger, - "The provided value '{0}' is not a valid integer or variable containing an integer.", - "This error occurs when a variable requires a variable that must be an integer, or a variable containing an integer. However, the provided variable is not an integer or a valid integer variable."), - - new ErrorInfo( - 135, - ErrorCode.IndexTooLarge, - "The provided index '{index}' is greater than the size of the player collection.", - "This error only occurs in the {INDEXVAR} variable. It occurs when the index provided is larger than the list it is trying to index. As an example, this error will occur if you try to get the 5th player from a variable containing only two players."), - - new ErrorInfo( - 136, - ErrorCode.CustomCommand_MultCooldowns, - "Custom command '{0}' ({1}) will not be created because it has multiple cooldowns set to a value other than -1. Only one cooldown can have a value other than -1.", - "This error only occurs if both the cooldown and player_cooldown settings are set to a value other than -1 in a custom command. Only one cooldown type may have a value of -1."), - - new ErrorInfo( - 137, - ErrorCode.InvalidNumber, - "The provided value '{0}' is not a valid number or variable containing a number.", - "This error occurs when a variable requires a variable that must be a number, or a variable containing a number. However, the provided variable is not a number or a valid number variable."), - - new ErrorInfo( - 138, - ErrorCode.UnsupportedArgumentVariables, - "Argument variables are not supported in the '{0}' variable. Please use a custom variable instead.", - $"This error occurs when a variable expects a variable as one of its arguments. However, the provided variable is a variable with arguments, which is not supported. This error can be resolved by using a custom variable in its place."), - - new ErrorInfo( - 139, - ErrorCode.ScriptJumpFailed, - "Failed to jump to {0}, '{1}' value is not a valid label.", - $"This error occurs when an invalid label or keyword is provided for an action that jumps to labels."), - - new ErrorInfo( - 140, - ErrorCode.VariableReplaceError, - "Error replacing the {0} variable: {1}", - "This error occurs when an error occurred while replacing a variable. Please report this error in the Scripted Events Discord server."), - - new ErrorInfo( - 141, - ErrorCode.UnknownActionError, - "Ran into an error while running '{0}' action (please report to developer)", - "This error occurs when there was an unexpected error in an action. Please report this error in the Scripted Events Discord server."), - - new ErrorInfo( - 142, - ErrorCode.InvalidDoor, - "'{0}' is not a valid door input.", - "This error occurs when an input to target in-game doors was expected, but the input was invalid. This error can be resolved by using a proper door input, such as 'ALL', a ZoneType/RoomType, or the name/ID of a door."), - - new ErrorInfo( - 143, - ErrorCode.InvalidLift, - "'{0} is not a valid lift input.", - "This error occurs when an input to target in-game lift was expected, but the input was invalid. This error can be resolved by using a proper lift input, such as 'ALL' or the name/ID of a door."), - - new ErrorInfo( - 144, - ErrorCode.InvalidEnumGeneric, - "Provided value '{0}' is not a valid {1} input. See all options by running 'shelp {1}' in the server console.", - "This error occurs when an input to a specific enum was expected, but the input was invalid. This error can be resolved by using a valid enum input. All valid enum inputs can be seen by running 'shelp ' in the server console."), - - new ErrorInfo( - 145, - ErrorCode.InvalidStringVariable, - "Provided variable '{0}' is not a valid string variable.", - "This error occurs when a string variable was expected, but is not provided. This error can be resolved by providing a valid string variable."), - - new ErrorInfo( - 146, - ErrorCode.InvalidCharacter, - "Provided value '{0}' is not a valid character.", - "This error occurs when a single character input was expected, but the input is not a valid character. This error can be resolved by providing a valid character."), - - new ErrorInfo( - 147, - ErrorCode.InvalidRoleTypeOrTeam, - "Provided value '{0}' is not a valid RoleTypeId or Team.", - $"This error occurs when a RoleTypeId or Team input was expected, but the input is not either. This error can be resolved by providing a valid RoleTypeId or Team. A full list of valid RoleTypeId IDs (as of {DateTime.Now:g}) follows:\n{string.Join("\n", ((RoleTypeId[])Enum.GetValues(typeof(RoleTypeId))).Where(r => r is not RoleTypeId.None).Select(r => $"- [{r:d}] {r}"))}\n A full list of valid Team IDs (as of {DateTime.Now:g}) follows:\n{string.Join("\n", ((Team[])Enum.GetValues(typeof(Team))).Select(r => $"- [{r:d}] {r}"))}"), - - new ErrorInfo( - 148, - ErrorCode.InvalidBoolean, - "Provided value '{0}' is not a valid boolean input.", - "This error occurs when a single boolean input was expected, but the input is not a valid boolean. This error can be resolved by providing a valid boolean input. Valid inputs are as follows: 'TRUE', 'T', 'FALSE', 'F', 'YES', 'Y', 'NO', 'N'."), - - new ErrorInfo( - 149, - ErrorCode.ParameterError_TooManyPlayers, - "Provided player variable '{0}' contains too many players for propper execution.", - "This error occurs when an action or a variable has a strict limit on how many players can be provided in order for propper execution. Read the action or variable documentation to learn more about the specifications."), - }.AsReadOnly(); - } -} diff --git a/ScriptedEvents/API/Features/Exceptions/DisabledScriptException.cs b/ScriptedEvents/API/Features/Exceptions/DisabledScriptException.cs deleted file mode 100644 index d4c0b01d..00000000 --- a/ScriptedEvents/API/Features/Exceptions/DisabledScriptException.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace ScriptedEvents.API.Features.Exceptions -{ - /// - /// Exception thrown when a disabled script is executed. - /// - public class DisabledScriptException : ScriptedEventsException - { - /// - /// Initializes a new instance of the class. - /// - /// The name of the script that is attempting to execute. - public DisabledScriptException(string scriptName) - : base($"The given script '{scriptName}' is disabled.") - { - ScriptName = scriptName; - } - - /// - /// Gets the name of the script that threw the exception. - /// - public string ScriptName { get; } - } -} diff --git a/ScriptedEvents/API/Features/Exceptions/ImpossibleException.cs b/ScriptedEvents/API/Features/Exceptions/ImpossibleException.cs new file mode 100644 index 00000000..14ce3f34 --- /dev/null +++ b/ScriptedEvents/API/Features/Exceptions/ImpossibleException.cs @@ -0,0 +1,22 @@ +namespace ScriptedEvents.API.Features.Exceptions +{ + using System; + + /// + /// An expection that should be impossible to trigger. + /// + public class ImpossibleException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public ImpossibleException() + : base( + "This exception triggers only when internal handling systems fail, " + + "and should not be possible under normal conditions. Please report this " + + "to the dev team." + ) + { + } + } +} diff --git a/ScriptedEvents/API/Features/Exceptions/ScriptedEventsException.cs b/ScriptedEvents/API/Features/Exceptions/ScriptedEventsException.cs deleted file mode 100644 index 02f836b1..00000000 --- a/ScriptedEvents/API/Features/Exceptions/ScriptedEventsException.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace ScriptedEvents.API.Features.Exceptions -{ - using System; - - /// - /// Exception thrown by Scripted Events. - /// - public class ScriptedEventsException : Exception - { - /// - /// Initializes a new instance of the class. - /// - /// The message in the exception. - public ScriptedEventsException(string message) - : base(message) - { - } - } -} diff --git a/ScriptedEvents/API/Features/Exceptions/VariableException.cs b/ScriptedEvents/API/Features/Exceptions/VariableException.cs deleted file mode 100644 index c4382cb7..00000000 --- a/ScriptedEvents/API/Features/Exceptions/VariableException.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace ScriptedEvents.API.Features.Exceptions -{ - /// - /// Exception thrown when a variable errors. - /// - public class VariableException : ScriptedEventsException - { - /// - /// Initializes a new instance of the class. - /// - /// The message in the exception. - public VariableException(string message) - : base(message) - { - } - } -} diff --git a/ScriptedEvents/API/Features/Logger.cs b/ScriptedEvents/API/Features/Logger.cs index 17a5d2f2..c0326651 100644 --- a/ScriptedEvents/API/Features/Logger.cs +++ b/ScriptedEvents/API/Features/Logger.cs @@ -1,22 +1,21 @@ -namespace ScriptedEvents.API.Features -{ - using CommandSystem; +using ScriptedEvents.Enums; +namespace ScriptedEvents.API.Features +{ using Exiled.API.Features; - - using ScriptedEvents.API.Enums; + using ScriptedEvents.Structures; using LogInternal = Exiled.API.Features.Log; public static class Logger { - public static void Log(string message, LogType logType, Script script = null, int? printableLine = null) + public static void Log(string message, LogType logType, Script? script = null, int? printableLine = null, bool addErrorContext = true) { if (MainPlugin.Configs.DisableAllLogs) return; - if (script is not null) - message = $"[Script: {script.ScriptName}] [L: {(printableLine == null ? script.CurrentLine + 1 : printableLine)}] {message}"; + if (script is not null && addErrorContext) + message = $"[Script: {script.ScriptName}] [Line: {printableLine ?? script.CurrentLine + 1}] {message}"; switch (logType) { @@ -30,48 +29,93 @@ public static void Log(string message, LogType logType, Script script = null, in LogInternal.Info(message); break; case LogType.Debug: - if (script is not null && script.Debug) + if (script is not null && script.IsDebug) script.DebugLog(message); else LogInternal.Debug(message); break; + default: + break; } } - public static void Info(string message, Script source = null) => Log(message, LogType.Info, source); + public static void Info(string message, Script? source = null) => Log(message, LogType.Info, source); + + public static void Warn(string message, Script? source = null) => Log(message, LogType.Warning, source); - public static void Warn(string message, Script source = null) => Log(message, LogType.Warning, source); + public static void Warn(ErrorTrace trace, Script? source = null) => Log($"\n{trace.Format()}", LogType.Warning, source); - public static void Error(string message, Script source = null) => Log(message, LogType.Error, source); + public static void Error(string message, Script? source = null) => Log(message, LogType.Error, source); - public static void Debug(string message, Script source = null) => Log(message, LogType.Debug, source); + public static void Error(ErrorTrace trace, Script? source = null, int? printableLine = null) => Log($"\n{trace.Format()}", LogType.Error, source, printableLine); + + public static void Debug(string message, Script? source = null) => Log(message, LogType.Debug, source); public static void ScriptError(string message, Script source, bool fatal = false, int? printableLine = null) { - string formattedMessage = $"[Script: {source.ScriptName}] [L: {(printableLine == null ? source.CurrentLine + 1 : printableLine)}]\n" + message; - string broadcastMessage = $"{(fatal ? "Fatal e" : "E")}rror when running the '{source.ScriptName}' script.\nSee the console for more details."; - ICommandSender sender = source.Sender; + var formattedMessage = $"[Script: {source.ScriptName}] [Line: {printableLine ?? source.CurrentLine + 1}] " + message; + var broadcastMessage = $"{(fatal ? "Fatal e" : "E")}rror when running the '{source.ScriptName}' script.\nSee the console for more details."; + var sender = source.Sender; switch (source.Context) { case ExecuteContext.RemoteAdmin: - Player ply = Player.Get(sender); + var ply = Player.Get(sender); ply?.RemoteAdminMessage(formattedMessage, false, MainPlugin.Singleton.Name); if (MainPlugin.Configs.BroadcastIssues) ply?.Broadcast(5, broadcastMessage); break; + case ExecuteContext.PlayerConsole: - Player ply2 = Player.Get(sender); + var ply2 = Player.Get(sender); ply2?.SendConsoleMessage(formattedMessage, "red"); if (MainPlugin.Configs.BroadcastIssues) ply2?.Broadcast(5, broadcastMessage); break; + + case ExecuteContext.Automatic: + case ExecuteContext.ServerConsole: + case ExecuteContext.None: + default: + Log(formattedMessage, LogType.Error, source, null, false); + break; + } + } + + public static void ScriptError(ErrorTrace trace, Script source, bool fatal = false, int? printableLine = null) + { + var error = $"[Script: {source.ScriptName}] [Line: {printableLine ?? source.CurrentLine + 1}]\n{trace.Format()}"; + var broadcastMessage = $"{(fatal ? "Fatal e" : "E")}rror when running the '{source.ScriptName}' script.\nSee the console for more details."; + + switch (source.Context) + { + case ExecuteContext.RemoteAdmin: + var ply = Player.Get(source.Sender); + ply?.RemoteAdminMessage(error, false, MainPlugin.Singleton.Name); + + if (MainPlugin.Configs.BroadcastIssues) + ply?.Broadcast(5, broadcastMessage); + + break; + + case ExecuteContext.PlayerConsole: + var ply2 = Player.Get(source.Sender); + ply2?.SendConsoleMessage(error, "red"); + + if (MainPlugin.Configs.BroadcastIssues) + ply2?.Broadcast(5, broadcastMessage); + + break; + + case ExecuteContext.Automatic: + case ExecuteContext.ServerConsole: + case ExecuteContext.None: default: - Warn(message); + Log(error, LogType.Error, source, null, false); break; } } diff --git a/ScriptedEvents/API/Features/MsgGen.cs b/ScriptedEvents/API/Features/MsgGen.cs index 84c58b4f..6875583f 100644 --- a/ScriptedEvents/API/Features/MsgGen.cs +++ b/ScriptedEvents/API/Features/MsgGen.cs @@ -1,4 +1,6 @@ -namespace ScriptedEvents.API.Features +using ScriptedEvents.Enums; + +namespace ScriptedEvents.API.Features { using System; using System.Collections.Generic; @@ -11,9 +13,6 @@ using PlayerRoles; using Respawning; - - using ScriptedEvents.API.Enums; - using ScriptedEvents.API.Interfaces; using ScriptedEvents.Structures; using ScriptedEvents.Variables.Interfaces; @@ -33,7 +32,7 @@ public static class MsgGen { typeof(byte), "Byte (Whole Number, 0-255)" }, { typeof(float), "Float (Number)" }, { typeof(bool), "Boolean (TRUE/FALSE)" }, - { typeof(PlayerCollection), "Player List" }, + { typeof(Player[]), "Player List" }, { typeof(Door[]), "Door List" }, { typeof(Room[]), "Room List" }, { typeof(RoleTypeId), "RoleTypeId (ID / Number)" }, @@ -41,67 +40,10 @@ public static class MsgGen { typeof(RoomType), "RoomType (ID / Number)" }, { typeof(IVariable), "Variable" }, { typeof(IPlayerVariable), "Player Variable" }, - { typeof(IConditionVariable), "Condition Variable" }, - { typeof(IStringVariable), "String (Message/Text) Variable" }, - { typeof(IFloatVariable), "Numerical Variable" }, - { typeof(ILongVariable), "Numerical Variable" }, - { typeof(RoleTypeIdOrTeam), "RoleTypeId (ID / Number) OR Team (ID / Number)" }, + { typeof(ILiteralVariable), "Literal (Raw Text) Variable" }, { typeof(object), "Any Type" }, }; - /// - /// Generates an error message, based on provided input. - /// - /// The type of message to show. - /// The action currently executing. - /// The name of the parameter that is causing a skill issue. - /// The arguments of the MessageType. See for documentation on what MessageTypes require what arguments. - /// The string to display to the user. - public static string Generate(MessageType type, IScriptComponent action, string paramName, params object[] arguments) - { - switch (type) - { - case MessageType.OK: - return "OK"; - - case MessageType.InvalidUsage when arguments[0] is Argument[] argList: - StringBuilder sb = StringBuilderPool.Pool.Get(); - foreach (Argument arg in argList) - { - string[] chars = arg.Required ? new[] { "<", ">" } : new[] { "[", "]" }; - sb.Append($" {chars[0]}{arg.ArgumentName}{chars[1]}"); - } - - return ErrorGen.Get(ErrorCode.InvalidActionUsage, action.Name, action.Name + StringBuilderPool.Pool.ToStringReturn(sb)); - - case MessageType.InvalidUsage: - return ErrorGen.Get(ErrorCode.LEGACY_InvalidActionUsage, action.Name); - - case MessageType.NotANumber when arguments[0] is not null: - return ErrorGen.Get(ErrorCode.ParameterError_Number, arguments[0], paramName, action.Name); - - case MessageType.NotANumberOrCondition when arguments[0] is not null && arguments[1] is MathResult result: - return ErrorGen.Get(ErrorCode.ParameterError_Condition, paramName, action.Name, arguments[0], result.Exception.GetType().Name, result.Message); - - case MessageType.LessThanZeroNumber when arguments[0] is not null: - return ErrorGen.Get(ErrorCode.ParameterError_LessThanZeroNumber, arguments[0], paramName, action.Name); - - case MessageType.InvalidRole when arguments[0] is not null: - return ErrorGen.Get(ErrorCode.ParameterError_RoleType, paramName, action.Name, arguments[0]); - - case MessageType.NoPlayersFound: - return ErrorGen.Get(ErrorCode.ParameterError_Players, paramName); - - case MessageType.NoRoomsFound: - return ErrorGen.Get(ErrorCode.ParameterError_Rooms, arguments[0], paramName); - - case MessageType.CassieCaptionNoAnnouncement: - return ErrorGen.Get(ErrorCode.ParameterError_CassieNoAnnc); - } - - return ErrorGen.Get(ErrorCode.UnknownError); - } - /// /// Gets a pretty display for a type. /// diff --git a/ScriptedEvents/API/Features/Parser.cs b/ScriptedEvents/API/Features/Parser.cs new file mode 100644 index 00000000..77ce265b --- /dev/null +++ b/ScriptedEvents/API/Features/Parser.cs @@ -0,0 +1,831 @@ +using ScriptedEvents.Interfaces; + +namespace ScriptedEvents.API.Features +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Text.RegularExpressions; + + using Exiled.API.Enums; + using Exiled.API.Features; + using Exiled.API.Features.Doors; + using Exiled.API.Features.Roles; + using Exiled.CustomItems.API.Features; + using PlayerRoles; + using ScriptedEvents.API.Extensions; + using ScriptedEvents.API.Modules; + using ScriptedEvents.Structures; + using ScriptedEvents.Variables.Interfaces; + + /// + /// A class used to store and retrieve all variables. + /// + public static class Parser + { + public delegate bool TryParseDelegate(string input, out T result); + + public static bool TryCast(TryParseDelegate tryParseFunc, string input, Script source, out T result, out ErrorInfo? errorInfo) + { + var success = tryParseFunc(ReplaceContaminatedValueSyntax(input, source), out result); + + errorInfo = success + ? null + : Error( + $"Invalid cast attempt for type {typeof(T).Name}'", + $"Value '{input}' is not convertable to a value of type '{typeof(T).Name}'"); + + return success; + } + + /// + /// Attempts to parse a string input into , where T is an . Functionally similar to , but also supports SE variables. + /// + /// The input string. + /// The result of the parse. + /// The source script. + /// The Enum type to cast to. + /// Whether or not the parse was successful. + public static bool TryGetEnum(string input, out T result, Script source, out ErrorInfo? errorInfo) + where T : struct, Enum + { + input = input.Trim(); + input = ReplaceContaminatedValueSyntax(input, source); + var success = Enum.TryParse(input, true, out result); + + errorInfo = success + ? null + : Error( + $"Invalid input for enum '{typeof(T).Name}'", + $"Provided input '{input}' could not be converted to the enum '{typeof(T).Name}'"); + + return success; + } + + public static bool TryGetEnumArray(string input, out T[] result, Script source) + where T : struct, Enum + { + input = input.Trim(); + input = ReplaceContaminatedValueSyntax(input, source); + IEnumerable resInIEnumerable = Array.Empty(); + + foreach (string item in input.Split('|')) + { + if (!Enum.TryParse(item, true, out T value)) + { + result = Array.Empty(); + return false; + } + + resInIEnumerable = resInIEnumerable.Append(value); + } + + result = resInIEnumerable.ToArray(); + return true; + } + + internal static bool TryGetTimeSpan(string input, out TimeSpan result, out ErrorInfo? errorInfo) + { + input = input.ToLower(); + result = default; + errorInfo = default; + + if (input.Contains(" ")) + { + errorInfo = Error( + "A time span cannot have white spaces", + $"Provided input '{input}' cannot be converted to a time span, because time spans cannot contain spaces"); + return false; + } + + string numberPart = string.Empty; + foreach (var character in input) + { + switch (character) + { + case '-': + errorInfo = Error( + "A time span cannot be negative", + $"Provided input '{input}' uses '-', which is not allowed."); + return false; + case '.': + errorInfo = Error( + "A time span is not a float value", + $"Provided input '{input}' uses '.', which is not allowed, because time spans dont support floating point numbers. If you want to do something like '0.5s', consider using '500ms'."); + return false; + } + + if (!int.TryParse(character.ToString(), out _)) + { + break; + } + + numberPart += character; + } + + if (numberPart.Length == 0) + { + errorInfo = Error( + "A time span must have a valid number", + $"Provided input '{input}' must have at least 1 digit at the beginning in order to specify a time span (e.g. '1s' instead of 'test')"); + return false; + } + + int length = int.Parse(numberPart); + string unitPart = input.Substring(numberPart.Length); + + if (unitPart.Length == 0) + { + errorInfo = Error( + "A time span must have a valid unit", + $"Provided input '{input}' must have a time unit at the end in order to specify a time span (e.g. '1s' instead of '123')"); + return false; + } + + TimeSpan timeSpan = unitPart switch + { + "s" => TimeSpan.FromSeconds(length), + "ms" => TimeSpan.FromMilliseconds(length), + "m" => TimeSpan.FromMinutes(length), + "h" => TimeSpan.FromHours(length), + "d" => TimeSpan.FromDays(length), + _ => TimeSpan.MinValue + }; + + if (timeSpan == TimeSpan.MinValue) + { + errorInfo = Error( + "A time span must have a valid unit", + $"Provided time unit '{unitPart}' in the input '{input}' is not valid. Available time units: 's' (seconds), 'ms' (milliseconds), 'm' (minutes), 'h' (hours), 'd' (days)"); + return false; + } + + result = timeSpan; + return true; + } + + /// + /// Try-get a array given an input. + /// + /// The input. + /// The doors. + /// The script source. + /// Whether or not the try-get was successful. + public static bool TryGetDoors(string input, out Door[] doors, Script source, out ErrorInfo? errorInfo) + { + IEnumerable doorList; + if (input == "*") + { + doorList = Door.List; + } + else if (TryGetEnum(input, out ZoneType zt, source, out _)) + { + doorList = Door.List.Where(d => d.Zone.HasFlag(zt)); + } + else if (TryGetEnum(input, out DoorType dt, source, out _)) + { + doorList = Door.List.Where(d => d.Type == dt); + } + else if (TryGetEnum(input, out RoomType rt, source, out _)) + { + doorList = Door.List.Where(d => d.Room?.Type == rt); + } + else + { + doorList = Door.List.Where(d => string.Equals(d.Name, input, StringComparison.CurrentCultureIgnoreCase)); + } + + doors = doorList.ToArray().Where(d => d.IsElevator is false && AirlockController.Get(d) is null).ToArray(); + + var found = doors.Length > 0; + errorInfo = found + ? null + : Error("Invalid door list input", $"Specified input '{input}' is not a valid representation of a list of doors."); + + return found; + } + + /// + /// Try-get a array given an input. + /// + /// The input. + /// The lift objects. + /// The script source. + /// Whether or not the try-get was successful. + public static bool TryGetLifts(string input, out Lift[] lifts, Script source, out ErrorInfo? errorInfo) + { + IEnumerable liftList; + if (input == "*") + { + liftList = Lift.List; + } + else if (TryGetEnum(input, out ElevatorType et, source, out _)) + { + liftList = Lift.List.Where(l => l.Type == et); + } + else + { + liftList = Lift.List.Where(l => string.Equals(l.Name, input, StringComparison.CurrentCultureIgnoreCase)); + } + + lifts = liftList.ToArray(); + + var found = lifts.Length > 0; + errorInfo = found + ? null + : Error("Invalid lift list input", $"Specified input '{input}' is not a valid representation of a list of lifts."); + + return found; + } + + /// + /// Try-get a array given an input. + /// + /// The input. + /// The rooms. + /// The script source. + /// Whether or not the try-get was successful. + public static bool TryGetRooms(string input, out Room[] rooms, Script source, out ErrorInfo? errorInfo) + { + IEnumerable roomList; + if (input == "*") + { + roomList = Room.List; + } + else if (TryGetEnum(input, out ZoneType zt, source, out _)) + { + roomList = Room.List.Where(room => room.Zone.HasFlag(zt)); + } + else if (TryGetEnum(input, out RoomType rt, source, out _)) + { + roomList = Room.List.Where(d => d.Type == rt); + } + else + { + roomList = Room.List.Where(d => string.Equals(d.Name, input, StringComparison.CurrentCultureIgnoreCase)); + } + + rooms = roomList.ToArray(); + + var found = rooms.Length > 0; + errorInfo = found + ? null + : Error("Invalid room list input", $"Specified input '{input}' is not a valid representation of a list of rooms."); + + return found; + } + + /// + /// Converts a string input into a player collection. + /// + public static bool TryGetPlayers(string input, int? amount, out IEnumerable players, Script source, out ErrorTrace? trace) + { + input = input.RemoveWhitespace(); + List list; + + if (input.ToUpper() == "*") + { + Log($"Input {input.ToUpper()} specifies all players on the server."); + list = Player.List.ToList(); + } + else if (VariableSystem.TryGetPlayersFromVariable(input, source, out var result, false, out trace)) + { + list = result.ToList(); + Log($"Input {input} is a valid variable. Fetch got {list.Count} players."); + } + else + { + trace!.Append(Error("Invalid player reference", $"Input '{input}' is not a valid reference to a list of players (like '*') and is not a variable.")); + players = Array.Empty(); + return false; + } + + if (MainPlugin.Configs.IgnoreOverwatch) + list.RemoveAll(p => p.Role.Type is RoleTypeId.Overwatch); + + if (amount is > 0 && list.Count > 0) + { + Log("Amount of fetched players bigger than limit, removing players."); + while (list.Count > amount.Value) + { + list.PullRandomItem(); + } + } + + // Return + Log($"Complete! Returning {list.Count} players."); + players = list; + trace = default; + return true; + + void Log(string msg) + { + Logger.Debug($"[SEParser] [TryGetPlayers] {msg}", source); + } + } + + /// + /// Isolates all values like. + /// + /// The input string. + /// The script source. + /// Should capture variables. + /// Should capture dynamic action results. + /// Should capture accessors. + /// The variables used within the string. + public static (Match[] variables, Match[] dynamicActions, Match[] accessors) IsolateValueSyntax(string input, Script source, bool captureVariables = true, bool captureDynActs = true, bool captureAccessors = true) + { + var empty = Array.Empty(); + var variables = empty; + var dynamicActions = empty; + var accessors = empty; + + if (captureVariables && input.Length >= 2) + { + variables = Regex.Matches(input, @"@\w+").Cast() + .Concat(Regex.Matches(input, @"\$\w+").Cast()) + .ToArray(); + } + + if (captureDynActs && input.Length >= 3) + { + dynamicActions = Regex.Matches(input, @"\{[^{}\s]*\}").Cast().ToArray(); + } + + if (captureAccessors && input.Length >= 3) + { + accessors = Regex.Matches(input, @"<[^<>\s]*>").Cast().ToArray(); + } + + Logger.Debug($"[SEParser] [IsolateValueSyntax] From '{input}' retreived: {variables.Length} VARS, {dynamicActions.Length} DNCTS, {accessors.Length} ACSRS", source); + return (variables, dynamicActions, accessors); + } + + public static bool TryGetAccessor(string initialInput, Script source, out string result, out ErrorTrace? error) + { + void Log(string msg) + { + Logger.Debug($"[SEParser] [TryGetAccessor] {msg}", source); + } + + // "" + // "" + result = string.Empty; + + // "PLR:ROLE" + // "PLR" + string input = initialInput.Substring(1, initialInput.Length - 2); + + // ["PLR", "ROLE"] + // ["PLR"] + var parts = input.Split(':'); + + switch (parts.Length) + { + case > 2: + error = Error( + $"Failed parsing the '{initialInput}' accessor", + $"Accessor structure looks like '', where parts of it are 'PlayerVariable' and 'Property', but provided input '{initialInput}' defines an accessor with more than 2 parts ('{string.Join("', '", parts)}').") + .ToTrace(); + return false; + case 1: + // ["PLR"] -> ["PLR", "NAME"] + parts = parts.Append("NAME").ToArray(); + break; + } + + if (!VariableSystem.TryGetPlayersFromVariable(parts[0], source, out var plrRes, true, out var trace)) + { + trace!.Append(Error($"Failed parsing the '{initialInput}' accessor", $"Can't retreive a player from variable {parts[0]}.")); + error = trace; + return false; + } + + var players = plrRes.ToArray(); + + if (players.Length != 1) + { + error = Error( + $"Failed parsing the '{initialInput}' accessor", + $"Provided player variable '{parts[0]}' contains {players.Length} players, but accessors require a variable with exactly 1 player.") + .ToTrace(); + return false; + } + + var ply = players.First(); + + if (ply is null) + throw new ArgumentException("Invalid player variable", nameof(initialInput)); + + try + { + switch (parts[1]) + { + case "NAME": + result = ply.Nickname; + break; + + case "DISPLAYNAME" or "DPNAME": + result = ply.DisplayNickname; + break; + + case "USERID" or "UID": + result = ply.UserId; + break; + + case "PLAYERID" or "PID": + result = ply.Id.ToString(); + break; + + case "ROLE": + result = ply.Role.Type.ToString(); + break; + + case "TEAM": + result = ply.Role.Team.ToString(); + break; + + case "ROOM": + result = ply.CurrentRoom.Type.ToString(); + break; + + case "ZONE": + result = ply.Zone.ToString(); + break; + + case "HP" or "HEALTH": + result = ply.Health.ToString(); + break; + + case "ITEMCOUNT": + result = ply.Items.Count.ToString(); + break; + + case "ITEMS": + result = ply.Items.Count > 0 + ? string.Join( + "|", + ply.Items.Select(item => + CustomItem.TryGet(item, out var ci1) ? ci1?.Name : item.Type.ToString())) + : "NONE"; + break; + + case "HELDITEM": + result = (CustomItem.TryGet(ply.CurrentItem, out var ci) + ? ci?.Name ?? "NONE" + : ply.CurrentItem?.Type.ToString()) ?? "NONE"; + break; + + case "GOD": + result = ply.IsGodModeEnabled.ToUpper(); + break; + + case "POS": + result = $"{ply.Position.x} {ply.Position.y} {ply.Position.z}"; + break; + + case "POSX": + result = ply.Position.x.ToString(); + break; + + case "POSY": + result = ply.Position.y.ToString(); + break; + + case "POSZ": + result = ply.Position.z.ToString(); + break; + + case "TIER" when ply.Role is Scp079Role scp079Role: + result = scp079Role.Level.ToString(); + break; + + case "TIER": + result = "NONE"; + break; + + case "GROUP": + result = ply.GroupName ?? "NONE"; + break; + + case "CUFFED": + result = ply.IsCuffed.ToUpper(); + break; + + case "CUSTOMINFO" or "CINFO" or "CUSTOMI": + result = !string.IsNullOrEmpty(ply.CustomInfo) ? ply.CustomInfo : "NONE"; + break; + + case "XSIZE": + result = ply.Scale.x.ToString(); + break; + + case "YSIZE": + result = ply.Scale.y.ToString(); + break; + + case "ZSIZE": + result = ply.Scale.z.ToString(); + break; + + case "KILLS": + result = EventHandlingModule.Singleton!.PlayerKills.TryGetValue(ply, out int v) ? v.ToString() : "0"; + break; + + case "EFFECTS" when ply.ActiveEffects.Any(): + result = string.Join(", ", ply.ActiveEffects.Select(eff => eff.name)); + break; + + case "EFFECTS": + result = "NONE"; + break; + + case "USINGNOCLIP" when ply.Role is FpcRole role: + result = role.IsNoclipEnabled.ToUpper(); + break; + + case "USINGNOCLIP": + result = "FALSE"; + break; + + case "CANNOCLIP": + result = ply.IsNoclipPermitted.ToUpper(); + break; + + case "STAMINA": + result = ply.Stamina.ToString(); + break; + + case "ISSTAFF": + result = ply.RemoteAdminAccess.ToUpper(); + break; + + case "AHP": + result = ply.ArtificialHealth.ToString(); + break; + + case "IS096TARGET": + result = "FALSE"; + foreach (Player p in Player.Get(RoleTypeId.Scp096)) + { + if (p.Role is not Scp096Role scp096 || !scp096.Targets.Contains(ply)) continue; + result = "TRUE"; + break; + } + + break; + + case "ISWATCHING173": + result = "FALSE"; + foreach (Player p in Player.Get(RoleTypeId.Scp173)) + { + if (p.Role is not Scp173Role scp173 || !scp173.ObservingPlayers.Contains(ply)) continue; + result = "TRUE"; + break; + } + + break; + + default: + result = ply.PlayerDataVariables().ContainsKey(parts[1]) + ? ply.PlayerDataVariables()[parts[1]] + : "UNDEFINED"; + break; + } + } + catch (NullReferenceException) + { + result = "NONE"; + } + + Log("Success! Returning " + result); + error = default; + return true; + } + + /// + /// Replaces all the occurrences of a value syntax in a string with regular text. + /// + /// The string to perform the replacements on. + /// The script that is currently running to replace values. + /// The modified string. + public static string ReplaceContaminatedValueSyntax(string input, Script script) + { + void Log(string msg) + { + Logger.Debug($"[SEParser] [RCVS] {msg}", script); + } + + var values = IsolateValueSyntax(input, script); + StringBuilder output = new(input); + + // variables can appear in dynActs and it can do some wonky stuff + // so remove variables if they are fully encapsulated in a dynact + List filteredVariables = new(); + foreach (var variable in values.variables) + { + bool isEnclosed = false; + + foreach (var dynAct in values.dynamicActions) + { + if (dynAct.Index > variable.Index || dynAct.Index + dynAct.Length < variable.Index + variable.Length) + continue; + + isEnclosed = true; + break; + } + + if (!isEnclosed) + { + filteredVariables.Add(variable); + } + } + + // Handle accessors in reverse order + for (int i = values.accessors.Length - 1; i >= 0; i--) + { + var accssr = values.accessors[i]; + + if (!TryGetAccessor(accssr.Value, script, out var res, out var errorTrace)) + { + Logger.ScriptError(errorTrace!, script); + continue; + } + + // Perform the replacement if the index/length is valid + if (accssr.Index >= 0 && accssr.Index < output.Length && + accssr.Index + accssr.Length <= output.Length) + { + output.Replace(accssr.Value, res, accssr.Index, accssr.Length); + } + else + { + Logger.Warn($"Invalid index/length for accessor: Index={accssr.Index}, Length={accssr.Length}, OutputLength={output.Length}"); + } + } + + // Handle dynamic actions in reverse order + for (int i = values.dynamicActions.Length - 1; i >= 0; i--) + { + var dynact = values.dynamicActions[i]; + + if (!TryGetDynamicActionResult(dynact.Value, script, out var res, out var error)) + { + Logger.ScriptError($"[Dynamic action] {error}", script); + continue; + } + + // Perform the replacement if the index/length is valid + if (dynact.Index >= 0 && dynact.Index < output.Length && + dynact.Index + dynact.Length <= output.Length) + { + output.Replace(dynact.Value, res, dynact.Index, dynact.Length); + } + else + { + Logger.Warn($"Invalid index/length for dynamic action: Index={dynact.Index}, Length={dynact.Length}, OutputLength={output.Length}"); + } + } + + // Handle filtered variables in reverse order + for (int i = filteredVariables.Count - 1; i >= 0; i--) + { + var varbl = filteredVariables[i]; + + if (!VariableSystem.TryGetVariable(varbl.Value, script, out var res, false, out var errorTrace) || res is null) + { + Logger.ScriptError(errorTrace!, script); + continue; + } + + // Perform the replacement if the index/length is valid + if (varbl.Index >= 0 && varbl.Index < output.Length && + varbl.Index + varbl.Length <= output.Length) + { + output.Replace(varbl.Value, res.String(), varbl.Index, varbl.Length); + } + else + { + Logger.Warn($"Invalid index/length for variable: Index={varbl.Index}, Length={varbl.Length}, OutputLength={output.Length}"); + } + } + + return output.ToString(); + } + + private static bool TryGetDynamicActionResult(string rawInput, Script script, out T output, out ErrorTrace? errorTrace) + { + // "{LIMIT:@PLAYERS:2}" + var input = rawInput.Trim(); + if (typeof(T) == typeof(string)) + { + output = (T)(object)string.Empty; + } + else if (typeof(T) == typeof(Player[])) + { + output = (T)(object)Array.Empty(); + } + else + { + throw new ArgumentException(typeof(T).FullName); + } + + if (!input.StartsWith("{") || !input.EndsWith("}")) + { + errorTrace = Error( + "Dynamic action parsing error", + $"Provided dynamic action '{rawInput}' is not surrounded by '{{}}' brackets.") + .ToTrace(); + return false; + } + + // "LIMIT:@PLAYERS:2 + input = input.Substring(1, input.Length - 2); + + // ["LIMIT", "@PLAYERS", "2"] + var parts = input.Split(':'); + + // "LIMIT" + var actionName = parts[0]; + + // ["@PLAYERS", "2"] + var arguments = parts.Skip(1).ToArray(); + + if (!ScriptModule.Singleton!.TryGetActionType(actionName, out var actionToExtract, out var error)) + { + errorTrace = Error( + "Dynamic action parsing error", + $"Provided action '{actionName}' is not a valid action.") + .ToTrace(); + return false; + } + + switch (actionToExtract) + { + case null: + throw new ArgumentNullException(nameof(actionToExtract)); + + case ITimingAction: + errorTrace = Error( + "Dynamic action logic error", + $"Provided action '{actionName}' is a timing action, which is not supported.") + .ToTrace(); + return false; + } + + if (actionToExtract is not IReturnValueAction) + { + errorTrace = Error( + $"Dynamic action '{actionToExtract.Name}' does not return any values", + $"Provided action is not designated as an action returning values, which is required for dynamic actions.") + .ToTrace(); + return false; + } + + if (!ScriptModule.TryRunAction(script, actionToExtract, out var resp, out _, arguments)) + { + resp!.ErrorTrace!.Append(Error( + $"Dynamic action '{actionToExtract.Name}' failed while running", + "Provided action failed while running, see inner exception for details.")); + + errorTrace = resp.ErrorTrace; + return false; + } + + if (resp == null || resp.ResponseVariables.Length == 0) + { + errorTrace = Error( + $"Dynamic action '{actionToExtract.Name}' did not return any values", + "Provied action did not return any values, which is required for dynamic actions.") + .ToTrace(); + return false; + } + + if (resp.ResponseVariables.Length > 1) + { + // Log("Action returned more than 1 value. Using the first one as default."); + } + + var response = resp.ResponseVariables[0]; + + if (response is not T value) + { + errorTrace = Error( + $"Dynamic action '{actionToExtract}' returned an invalid value", + $"Action returned a value of type {response.GetType().Name}, which does not match the expected type {typeof(T).Name}.") + .ToTrace(); + return false; + } + + output = value; + errorTrace = null; + return true; + } + + private static ErrorInfo Error(string name, string description) + { + return new(name, description, "Parser"); + } + } +} \ No newline at end of file diff --git a/ScriptedEvents/API/Features/SEParser.cs b/ScriptedEvents/API/Features/SEParser.cs deleted file mode 100644 index 9e5374cd..00000000 --- a/ScriptedEvents/API/Features/SEParser.cs +++ /dev/null @@ -1,161 +0,0 @@ -namespace ScriptedEvents.API.Features -{ - using System; - - using ScriptedEvents.API.Extensions; - using ScriptedEvents.API.Modules; - using ScriptedEvents.Structures; - - /// - /// A class used to store and retrieve all variables. - /// - public static class SEParser - { - /// - /// Attempts to parse a string input into a . Functionally similar to , but also supports SE variables. - /// - /// The input string. - /// The source script. - /// If brackets are required to parse variables. - /// The result of the cast, or if the cast failed. - public static float Parse(string input, Script source, bool requireBrackets = true) - { - if (float.TryParse(input, out float fl)) - return fl; - - if (VariableSystemV2.TryGetVariable(input, source, out VariableResult result, requireBrackets) && result.ProcessorSuccess) - { - return Parse(result.String(), source, requireBrackets); - } - - return float.NaN; - } - - /// - /// Attempts to parse a string input into a . Functionally similar to , but also supports SE variables. - /// - /// The input string. - /// The source script. - /// If brackets are required to parse variables. - /// The result of the cast, or if the cast failed. - public static int ParseInt(string input, Script source, bool requireBrackets = true) - { - if (int.TryParse(input, out int fl)) - return fl; - - if (VariableSystemV2.TryGetVariable(input, source, out VariableResult result, requireBrackets) && result.ProcessorSuccess) - { - return ParseInt(result.String(), source, requireBrackets); - } - - return int.MinValue; - } - - /// - /// Attempts to parse a string input into a . Functionally similar to , but also supports SE variables. - /// - /// The input string. - /// The source script. - /// If brackets are required to parse variables. - /// The result of the cast, or if the cast failed. - public static long ParseLong(string input, Script source, bool requireBrackets = true) - { - if (long.TryParse(input, out long fl)) - return fl; - - if (VariableSystemV2.TryGetVariable(input, source, out VariableResult result, requireBrackets) && result.ProcessorSuccess) - { - return ParseLong(result.String(), source, requireBrackets); - } - - return int.MinValue; - } - - /// - /// Attempts to parse a string input into a . Functionally similar to , but also supports SE variables. - /// - /// The input string. - /// The result of the parse. - /// The source script. - /// If brackets are required to parse variables. - /// Whether or not the parse was successful. - public static bool TryParse(string input, out float result, Script source, bool requireBrackets = true) - { - result = Parse(input, source, requireBrackets); - return result != float.NaN && result.ToString() != "NaN"; // Hacky but fixes it? - } - - /// - /// Attempts to parse a string input into a . Functionally similar to , but also supports SE variables. - /// - /// The input string. - /// The result of the parse. - /// The source script. - /// If brackets are required to parse variables. - /// Whether or not the parse was successful. - public static bool TryParse(string input, out int result, Script source, bool requireBrackets = true) - { - result = ParseInt(input, source, requireBrackets); - return result != int.MinValue; - } - - /// - /// Attempts to parse a string input into a . Functionally similar to , but also supports SE variables. - /// - /// The input string. - /// The result of the parse. - /// The source script. - /// If brackets are required to parse variables. - /// Whether or not the parse was successful. - public static bool TryParse(string input, out long result, Script source, bool requireBrackets = true) - { - result = ParseLong(input, source, requireBrackets); - return result != long.MinValue; - } - - /// - /// Attempts to parse a string input into , where T is an . Functionally similar to , but also supports SE variables. - /// - /// The input string. - /// The result of the parse. - /// The source script. - /// If brackets are required to parse variables. - /// The Enum type to cast to. - /// Whether or not the parse was successful. - public static bool TryParse(string input, out T result, Script source, bool requireBrackets = true) - where T : struct, Enum - { - input = input.Trim(); - if (Enum.TryParse(input, true, out result)) - { - return true; - } - - if (VariableSystemV2.TryGetVariable(input, source, out VariableResult vresult, requireBrackets) && vresult.ProcessorSuccess) - { - return TryParse(vresult.String(), out result, source, requireBrackets); - } - - return false; - } - - public static object Parse(string input, Type enumType, Script source, bool requireBrackets = true) - { - try - { - object result = Enum.Parse(enumType, input, true); - return result; - } - catch - { - } - - if (VariableSystemV2.TryGetVariable(input, source, out VariableResult vresult, requireBrackets) && vresult.ProcessorSuccess) - { - return Parse(vresult.String(), enumType, source, requireBrackets); - } - - return null; - } - } -} \ No newline at end of file diff --git a/ScriptedEvents/API/Features/ScriptHelpGenerator/Generator.cs b/ScriptedEvents/API/Features/ScriptHelpGenerator/Generator.cs index 5a4aa9b9..28c090c2 100644 --- a/ScriptedEvents/API/Features/ScriptHelpGenerator/Generator.cs +++ b/ScriptedEvents/API/Features/ScriptHelpGenerator/Generator.cs @@ -13,7 +13,6 @@ using ScriptedEvents.Actions; using ScriptedEvents.API.Constants; using ScriptedEvents.API.Extensions; - using ScriptedEvents.API.Interfaces; using ScriptedEvents.API.Modules; using ScriptedEvents.Structures; using ScriptedEvents.Tutorials; @@ -68,7 +67,7 @@ public static bool GenerateConfig(out string message) } catch (Exception) { - message = $"Config file was created successfully, but an error occurred when opening external text editor (likely due to permissions). File is located at: {ConfigPath}."; + message = $"Config file was created successfully, but an error occurred when opening external text editor (likely due to permissions). File is located at: {ConfigPath}. Please fill out the generated config file using 'y' for yes and 'n' for no. When done, run the 'shelp GDONE' command."; return false; } @@ -112,7 +111,7 @@ public static bool CreateDocumentation(out string message) return false; } - if (!sections[1].IsBool(out bool doGenerate)) + if (!sections[1].IsBool(out bool doGenerate, out _)) { message = $"Malformed file. Line {i} does not have proper y/n value."; return false; @@ -130,7 +129,7 @@ public static bool CreateDocumentation(out string message) // Documentation data string metaPath = Path.Combine(BasePath, "DocInfo.txt"); - File.WriteAllText(metaPath, $"Documentation Generator\nGenerated at: {DateTime.UtcNow:f}\nSE version: {MainPlugin.Singleton.Version}\nExperimental DLL: {(MainPlugin.IsExperimental ? "YES" : "NO")}\n\n\n-- DO NOT MODIFY BELOW THIS LINE --\n!_V{MainPlugin.Singleton.Version}"); + File.WriteAllText(metaPath, $"Documentation Generator\nGenerated at: {DateTime.Now:f}\nSE version: {MainPlugin.Singleton.Version}\nExperimental DLL: {(MainPlugin.IsExperimental ? "YES" : "NO")}\n\n\n-- DO NOT MODIFY BELOW THIS LINE --\n!_V{MainPlugin.Singleton.Version}"); // Delete old folders if (Directory.Exists(ActionPath)) @@ -163,12 +162,13 @@ public static bool CreateDocumentation(out string message) Directory.Delete(TutorialsPath, true); } + /* if (config.generate_actions) { Directory.CreateDirectory(ActionPath); Stopwatch watch = Stopwatch.StartNew(); - foreach (var actionData in MainPlugin.ScriptModule.ActionTypes) + foreach (var actionData in ScriptModule.Singleton!.ActionTypes) { IAction action = Activator.CreateInstance(actionData.Value) as IAction; @@ -191,12 +191,13 @@ public static bool CreateDocumentation(out string message) Logger.Info($"Completed generating documentation for actions. Elapsed time: {watch.ElapsedMilliseconds}ms"); } + if (config.generate_variables) { Directory.CreateDirectory(VariablePath); Stopwatch watch = Stopwatch.StartNew(); - var conditionList = VariableSystemV2.Groups.OrderBy(group => group.GroupName); + var conditionList = VariableSystem.Groups.OrderBy(group => group.GroupName); foreach (IVariableGroup group in conditionList) { string groupPath = Path.Combine(VariablePath, group.GroupName); @@ -243,30 +244,7 @@ public static bool CreateDocumentation(out string message) Logger.Info($"Completed generating documentation for enums. Elapsed time: {watch.ElapsedMilliseconds}ms"); } - - if (config.generate_error_codes) - { - Directory.CreateDirectory(ErrorsPath); - - Stopwatch watch = Stopwatch.StartNew(); - foreach (ErrorInfo errorInfo in ErrorList.Errors) - { - Logger.Debug("Creating documentation for error: " + errorInfo.Id); - - ActionResponse text = HelpAction.GenerateText(errorInfo.Id.ToString()); - - if (text.Success) - { - string path = Path.Combine(ErrorsPath, "SE-" + errorInfo.Id.ToString() + ".txt"); - File.WriteAllText(path, text.Message); - } - } - - watch.Stop(); - - Logger.Info($"Completed generating documentation for enums. Elapsed time: {watch.ElapsedMilliseconds}ms"); - } - + */ if (config.generate_tutorials) { Directory.CreateDirectory(TutorialsPath); diff --git a/ScriptedEvents/API/Features/VariableStorage.cs b/ScriptedEvents/API/Features/VariableStorage.cs deleted file mode 100644 index 30a8aa29..00000000 --- a/ScriptedEvents/API/Features/VariableStorage.cs +++ /dev/null @@ -1,100 +0,0 @@ -namespace ScriptedEvents.API.Features -{ - using System; - using System.IO; - - using ScriptedEvents.API.Enums; - using ScriptedEvents.API.Extensions; - using ScriptedEvents.API.Features.Exceptions; - using ScriptedEvents.Variables.Interfaces; - - /// - /// Class to save variables long-term. - /// - public static class VariableStorage - { - /// - /// Path to the variable storage folder. - /// - public static readonly string DirPath = Path.Combine(MainPlugin.BaseFilePath, MainPlugin.Configs.StorageFoldername); - - /// - /// Save variable to variable storage. - /// - /// The variable to save. - public static void Save(IConditionVariable var) - { - InternalSave(var.Name, var.String()); - } - - /// - /// Save variable to variable storage. - /// - /// The name of the variable. - /// The value of the variable. - public static void Save(string varName, string varValue) - { - InternalSave(varName, varValue); - } - - /// - /// Read from the variable storage. - /// - /// The variable name to read. - /// Variable value that has been saved before. - public static string Read(string varName) - { - try - { - varName = StripBrackets(varName); - using StreamReader reader = new(GetFilePath(varName)); - return reader.ReadLine(); - } - catch (FileNotFoundException) - { - Logger.Error($"Trying to read {varName} from storage, but it hasn't been saved in the storage folder."); - return "INVALID - VARIABLE DOESN'T EXIST"; - } - catch (Exception ex) - { - throw new Exception($"Error trying to read from storage: {ex}"); - } - } - - private static void InternalSave(string varName, string varValue) - { - try - { - varName = StripBrackets(varName); - if (!Directory.Exists(DirPath)) - { - Directory.CreateDirectory(DirPath); - Logger.Warn("Storage folder was absent; a new folder has been created."); - } - - string filePath = GetFilePath(varName); - - using StreamWriter writer = new(filePath, false, System.Text.Encoding.UTF8); - writer.WriteLine(varValue); - } - catch (UnauthorizedAccessException ex) - { - throw new ScriptedEventsException(ErrorGen.Get(ErrorCode.IOPermissionError) + $": {ex}"); - } - catch (Exception ex) - { - throw new ScriptedEventsException(ErrorGen.Get(ErrorCode.IOError) + $": {ex}"); - } - } - - private static string GetFilePath(string varName) => Path.Combine(DirPath, $"{varName}.txt"); - - private static string StripBrackets(string varName) - { - if (varName.Length >= 2 && varName[0] == '{' && varName[varName.Length - 1] == '}') - return varName.Substring(1, varName.Length - 2); - - return varName; - } - } -} diff --git a/ScriptedEvents/API/Interfaces/IIgnoresIfActionBlock.cs b/ScriptedEvents/API/Interfaces/IIgnoresIfActionBlock.cs deleted file mode 100644 index a875d3eb..00000000 --- a/ScriptedEvents/API/Interfaces/IIgnoresIfActionBlock.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace ScriptedEvents.API.Interfaces -{ - /// - /// Indicates an action that ignores the property, allowing the action to run even if the value is true. - /// - internal interface IIgnoresIfActionBlock - { - } -} diff --git a/ScriptedEvents/API/Modules/CountdownModule.cs b/ScriptedEvents/API/Modules/CountdownModule.cs index df869583..047a5827 100644 --- a/ScriptedEvents/API/Modules/CountdownModule.cs +++ b/ScriptedEvents/API/Modules/CountdownModule.cs @@ -16,21 +16,23 @@ public class CountdownModule : SEModule { public override string Name => "CountdownModule"; + + public static CountdownModule? Singleton { get; private set; } /// /// Gets or sets the main coroutine handle. /// - public CoroutineHandle? MainHandle { get; set; } + private CoroutineHandle? MainHandle { get; set; } /// /// Gets a of active countdowns. /// - public Dictionary Countdowns { get; } = new(); + private Dictionary Countdowns { get; } = new(); /// /// Gets the default countdown text. /// - public string BroadcastString => MainPlugin.Singleton.Config.CountdownString ?? "Countdown"; + private static string BroadcastString => MainPlugin.Singleton.Config.CountdownString; /// /// Begins the countdown coroutine. @@ -38,6 +40,7 @@ public class CountdownModule : SEModule public override void Init() { base.Init(); + Singleton = this; Exiled.Events.Handlers.Server.RestartingRound += OnRestarting; Exiled.Events.Handlers.Server.WaitingForPlayers += WaitingForPlayers; } @@ -45,21 +48,20 @@ public override void Init() public override void Kill() { base.Kill(); + Singleton = null; Exiled.Events.Handlers.Server.RestartingRound -= OnRestarting; Exiled.Events.Handlers.Server.WaitingForPlayers -= WaitingForPlayers; } - public void OnRestarting() + private void OnRestarting() { - if (MainHandle is not null && MainHandle.Value.IsRunning) - { - Timing.KillCoroutines(MainHandle.Value); - MainHandle = null; - Countdowns.Clear(); - } + if (MainHandle is null || !MainHandle.Value.IsRunning) return; + Timing.KillCoroutines(MainHandle.Value); + MainHandle = null; + Countdowns.Clear(); } - public void WaitingForPlayers() + private void WaitingForPlayers() { MainHandle = Timing.RunCoroutine(InternalCoroutine()); } @@ -69,7 +71,7 @@ public void WaitingForPlayers() /// /// The time span. /// String format. - public static string Display(TimeSpan time) + private static string Display(TimeSpan time) { int seconds = Mathf.RoundToInt((float)time.TotalSeconds); @@ -110,7 +112,7 @@ public static string Display(TimeSpan time) /// The text to show. /// The time on the countdown. /// The script that started the countdown. - public void AddCountdown(Player player, string text, TimeSpan ts, Script source = null) + public void AddCountdown(Player player, string text, TimeSpan ts, Script? source = null) { if (Countdowns.ContainsKey(player)) Countdowns.Remove(player); @@ -134,11 +136,9 @@ private IEnumerator InternalCoroutine() if (!Countdowns.TryGetValue(ply, out Countdown ct)) continue; - if (!ct.Expired) - { - ply.ClearBroadcasts(); - ply.Broadcast(1, BroadcastString.Replace("{TEXT}", ct.Text).Replace("{TIME}", Display(ct.TimeLeft))); - } + if (ct.Expired) continue; + ply.ClearBroadcasts(); + ply.Broadcast(1, BroadcastString.Replace("{TEXT}", ct.Text).Replace("{TIME}", Display(ct.TimeLeft))); } yield return Timing.WaitForSeconds(1); diff --git a/ScriptedEvents/API/Modules/EventHandlingModule.cs b/ScriptedEvents/API/Modules/EventHandlingModule.cs index 32ecb5c3..d91f589e 100644 --- a/ScriptedEvents/API/Modules/EventHandlingModule.cs +++ b/ScriptedEvents/API/Modules/EventHandlingModule.cs @@ -1,38 +1,32 @@ -namespace ScriptedEvents +using System; +using System.Collections.Generic; +using System.Linq; +using Exiled.API.Enums; +using Exiled.API.Extensions; +using Exiled.API.Features; +using Exiled.API.Features.Pickups; +using Exiled.Events.EventArgs.Interfaces; +using Exiled.Events.EventArgs.Map; +using Exiled.Events.EventArgs.Player; +using Exiled.Events.EventArgs.Scp049; +using Exiled.Events.EventArgs.Scp0492; +using Exiled.Events.EventArgs.Scp079; +using Exiled.Events.EventArgs.Scp096; +using Exiled.Events.EventArgs.Scp106; +using Exiled.Events.EventArgs.Scp173; +using Exiled.Events.EventArgs.Scp3114; +using Exiled.Events.EventArgs.Scp939; +using Exiled.Events.EventArgs.Server; +using Exiled.Events.EventArgs.Warhead; +using MapGeneration.Distributors; +using MEC; +using PlayerRoles; +using Respawning; +using ScriptedEvents.Structures; +using UnityEngine; + +namespace ScriptedEvents.API.Modules { - using System; - using System.Collections.Generic; - using System.Data; - using System.Linq; - - using Exiled.API.Enums; - using Exiled.API.Features; - using Exiled.API.Features.Pickups; - using Exiled.Events.EventArgs.Interfaces; - using Exiled.Events.EventArgs.Map; - using Exiled.Events.EventArgs.Player; - using Exiled.Events.EventArgs.Scp049; - using Exiled.Events.EventArgs.Scp0492; - using Exiled.Events.EventArgs.Scp079; - using Exiled.Events.EventArgs.Scp096; - using Exiled.Events.EventArgs.Scp106; - using Exiled.Events.EventArgs.Scp173; - using Exiled.Events.EventArgs.Scp3114; - using Exiled.Events.EventArgs.Scp939; - using Exiled.Events.EventArgs.Server; - using Exiled.Events.EventArgs.Warhead; - - using MapGeneration.Distributors; - using MEC; - using PlayerRoles; - - using Respawning; - - using ScriptedEvents.API.Modules; - using ScriptedEvents.Structures; - - using UnityEngine; - using MapHandler = Exiled.Events.Handlers.Map; using PlayerHandler = Exiled.Events.Handlers.Player; using Scp0492Handler = Exiled.Events.Handlers.Scp0492; @@ -49,19 +43,22 @@ public class EventHandlingModule : SEModule { - private DateTime lastRespawnWave = DateTime.MinValue; + private DateTime _lastRespawnWave = DateTime.MinValue; + private readonly Dictionary> _spawnLoadoutRule = new(); public override string Name => "EventHandlingModule"; + + public static EventHandlingModule? Singleton { get; private set; } /// /// Gets or sets the amount of respawn waves since the round started. /// - public int RespawnWaves { get; set; } = 0; + public int RespawnWaves { get; private set; } = 0; /// /// Gets the amount of time since the last wave. /// - public TimeSpan TimeSinceWave => DateTime.UtcNow - lastRespawnWave; + public TimeSpan TimeSinceWave => DateTime.Now - _lastRespawnWave; /// /// Gets a value indicating whether or not a wave just spawned. @@ -71,7 +68,7 @@ public class EventHandlingModule : SEModule /// /// Gets or sets the most recent respawn type. /// - public SpawnableTeamType MostRecentSpawn { get; set; } + public SpawnableTeamType MostRecentSpawn { get; private set; } /// /// Gets or sets the spawns by team. @@ -157,15 +154,26 @@ public class EventHandlingModule : SEModule public Dictionary> PermTeamEffects { get; } = new(); /// - /// Gets a dictionary of permanent role-specific effects. + /// Gets a dictionary of permanent role-specific effects. /// public Dictionary> PermRoleEffects { get; } = new(); + /// + /// Gets a dictionary of effect immunity for specific players. + /// + public Dictionary> PlayerEffectImmunity { get; } = new(); + + /// + /// Gets a dictionary of itemTypes to add on role change. + /// + public Dictionary> SpawnLoadoutRule => _spawnLoadoutRule; + public List DamageRules { get; } = new(); public override void Init() { base.Init(); + Singleton = this; PlayerHandler.ChangingRole += OnChangingRole; PlayerHandler.Hurting += OnHurting; PlayerHandler.Died += OnDied; @@ -181,12 +189,13 @@ public override void Init() PlayerHandler.InteractingElevator += OnInteractingElevator; PlayerHandler.Escaping += OnEscaping; PlayerHandler.Spawned += OnSpawned; + PlayerHandler.ReceivingEffect += OnReceivingEffect; PlayerHandler.PickingUpItem += OnPickingUpItem; PlayerHandler.ChangingRadioPreset += OnChangingRadioPreset; PlayerHandler.ActivatingWarheadPanel += OnActivatingWarheadPanel; - Exiled.Events.Handlers.Warhead.Starting += OnStartingWarhead; // why is this located specially?? + Exiled.Events.Handlers.Warhead.Starting += OnStartingWarhead; PlayerHandler.ActivatingGenerator += GeneratorEvent; PlayerHandler.OpeningGenerator += GeneratorEvent; @@ -252,6 +261,7 @@ public override void Init() public override void Kill() { base.Kill(); + Singleton = null; PlayerHandler.ChangingRole -= OnChangingRole; PlayerHandler.Hurting -= OnHurting; PlayerHandler.Died -= OnDied; @@ -267,6 +277,7 @@ public override void Kill() PlayerHandler.InteractingElevator -= OnInteractingElevator; PlayerHandler.Escaping -= OnEscaping; PlayerHandler.Spawned -= OnSpawned; + PlayerHandler.ReceivingEffect -= OnReceivingEffect; PlayerHandler.PickingUpItem -= OnPickingUpItem; PlayerHandler.ChangingRadioPreset -= OnChangingRadioPreset; @@ -339,28 +350,22 @@ public override void Kill() // Helpful method public PlayerDisable? GetPlayerDisableRule(string key) { - foreach (PlayerDisable playerDisable in DisabledPlayerKeys) + foreach (var playerDisable in DisabledPlayerKeys.Where(playerDisable => key.Equals(playerDisable.Key))) { - if (key.Equals(playerDisable.Key)) - { - return playerDisable; - } + return playerDisable; } return null; } - public PlayerDisable? GetPlayerDisableRule(string key, Player player) + private PlayerDisable? GetPlayerDisableRule(string key, Player? player) { if (player is null) return null; - foreach (PlayerDisable playerDisable in DisabledPlayerKeys) + foreach (var playerDisable in DisabledPlayerKeys.Where(playerDisable => key.Equals(playerDisable.Key) && playerDisable.Players.Contains(player))) { - if (key.Equals(playerDisable.Key) && playerDisable.Players.Contains(player)) - { - return playerDisable; - } + return playerDisable; } return null; @@ -368,33 +373,27 @@ public override void Kill() public PlayerDisable? GetPlayerDisableEvent(string key) { - foreach (PlayerDisable playerDisable in DisabledPlayerEvents) + foreach (var playerDisable in DisabledPlayerEvents.Where(playerDisable => key == playerDisable.Key)) { - if (key == playerDisable.Key) - { - return playerDisable; - } + return playerDisable; } return null; } - public PlayerDisable? GetPlayerDisableEvent(string key, Player player) + public PlayerDisable? GetPlayerDisableEvent(string key, Player? player) { if (player is null) return null; - foreach (PlayerDisable playerDisable in DisabledPlayerEvents) + foreach (var playerDisable in DisabledPlayerEvents.Where(playerDisable => key == playerDisable.Key && playerDisable.Players.Contains(player))) { - if (key == playerDisable.Key && playerDisable.Players.Contains(player)) - { - return playerDisable; - } + return playerDisable; } return null; } - public bool DisabledForPlayer(string key, Player player) + private bool DisabledForPlayer(string key, Player player) { return GetPlayerDisableRule(key, player).HasValue; } @@ -402,7 +401,7 @@ public bool DisabledForPlayer(string key, Player player) public void OnRestarting() { RespawnWaves = 0; - lastRespawnWave = DateTime.MinValue; + _lastRespawnWave = DateTime.MinValue; TeslasDisabled = false; MostRecentSpawnUnit = string.Empty; @@ -419,8 +418,8 @@ public void OnRestarting() cmd.ResetCooldowns(); } - MainPlugin.ScriptModule.StopAllScripts(); - VariableSystemV2.ClearVariables(); + ScriptModule.Singleton!.StopAllScripts(); + VariableSystem.ClearVariables(); Kills.Clear(); PlayerKills.Clear(); LockedRadios.Clear(); @@ -439,6 +438,8 @@ public void OnRestarting() RecentlyRespawned.Clear(); MostRecentSpawn = SpawnableTeamType.None; + + PlayerEffectImmunity.Clear(); } public void OnRoundStarted() @@ -490,14 +491,32 @@ public void OnRoundStarted() } } - public void OnRespawningTeam(RespawningTeamEventArgs ev) + // effect immunity action + private void OnReceivingEffect(ReceivingEffectEventArgs ev) + { + if (ev.Player == null) return; + + if (!PlayerEffectImmunity.TryGetValue(ev.Player, out var effects)) + { + return; + } + + if (!effects.Contains(ev.Effect.GetEffectType())) + { + return; + } + + ev.IsAllowed = false; + } + + private void OnRespawningTeam(RespawningTeamEventArgs ev) { if (DisabledKeys.Contains("RESPAWNS")) ev.IsAllowed = false; if (!ev.IsAllowed) return; RespawnWaves++; - lastRespawnWave = DateTime.UtcNow; + _lastRespawnWave = DateTime.Now; MostRecentSpawn = ev.NextKnownTeam; SpawnsByTeam[ev.NextKnownTeam]++; @@ -507,7 +526,7 @@ public void OnRespawningTeam(RespawningTeamEventArgs ev) } // Perm Effects: Spawned - public void OnSpawned(SpawnedEventArgs ev) + private void OnSpawned(SpawnedEventArgs ev) { if (PermPlayerEffects.TryGetValue(ev.Player, out var effects)) { @@ -526,12 +545,12 @@ public void OnSpawned(SpawnedEventArgs ev) } // Infection - public void OnDied(DiedEventArgs ev) + private void OnDied(DiedEventArgs ev) { if (ev.Player is null || ev.Attacker is null || ev.DamageHandler.Attacker is null) return; - if (!InfectionRules.Any(r => r.OldRole == ev.TargetOldRole)) + if (InfectionRules.All(r => r.OldRole != ev.TargetOldRole)) return; InfectRule? ruleNullable = InfectionRules.FirstOrDefault(r => r.OldRole == ev.TargetOldRole); @@ -541,7 +560,7 @@ public void OnDied(DiedEventArgs ev) Timing.CallDelayed(0.5f, () => { - ev.Player.Role.Set(rule.NewRole); + ev.Player.Role.Set(rule.NewRole, SpawnReason.ForceClass, RoleSpawnFlags.None); if (rule.MovePlayer) ev.Player.Teleport(pos); @@ -549,7 +568,7 @@ public void OnDied(DiedEventArgs ev) } // Tesla - public void OnTriggeringTesla(TriggeringTeslaEventArgs ev) + private void OnTriggeringTesla(TriggeringTeslaEventArgs ev) { if (TeslasDisabled || DisabledKeys.Contains("TESLAS") || DisabledForPlayer("TESLAS", ev.Player)) { @@ -558,7 +577,7 @@ public void OnTriggeringTesla(TriggeringTeslaEventArgs ev) } // Locked Radios - public void OnChangingRole(ChangingRoleEventArgs ev) + private void OnChangingRole(ChangingRoleEventArgs ev) { if (!ev.IsAllowed) return; @@ -569,7 +588,7 @@ public void OnChangingRole(ChangingRoleEventArgs ev) } // Disable Stuff - public void OnDying(DyingEventArgs ev) + private void OnDying(DyingEventArgs ev) { if (DisabledKeys.Contains("DYING") || DisabledForPlayer("DYING", ev.Player)) ev.IsAllowed = false; @@ -577,21 +596,20 @@ public void OnDying(DyingEventArgs ev) if (!ev.IsAllowed) return; - if (ev.Attacker is not null) - { - if (Kills.ContainsKey(ev.Attacker.Role.Type)) - Kills[ev.Attacker.Role.Type]++; - else - Kills.Add(ev.Attacker.Role.Type, 1); - - if (PlayerKills.ContainsKey(ev.Attacker)) - PlayerKills[ev.Attacker]++; - else - PlayerKills.Add(ev.Attacker, 1); - } + if (ev.Attacker is null) return; + + if (Kills.ContainsKey(ev.Attacker.Role.Type)) + Kills[ev.Attacker.Role.Type]++; + else + Kills.Add(ev.Attacker.Role.Type, 1); + + if (PlayerKills.ContainsKey(ev.Attacker)) + PlayerKills[ev.Attacker]++; + else + PlayerKills.Add(ev.Attacker, 1); } - public void OnHurting(HurtingEventArgs ev) + private void OnHurting(HurtingEventArgs ev) { if (DisabledKeys.Contains("HURTING") || DisabledForPlayer("HURTING", ev.Player)) ev.IsAllowed = false; @@ -610,31 +628,29 @@ public void OnHurting(HurtingEventArgs ev) ev.IsAllowed = false; // Damage Rules - foreach (DamageRule rule in DamageRules) + foreach (var multiplier in DamageRules.Select(rule => rule.DetermineMultiplier(ev.Attacker, ev.Player))) { - float multiplier = rule.DetermineMultiplier(ev.Attacker, ev.Player); ev.Amount *= multiplier; } } - public void GeneratorEvent(IGeneratorEvent ev) + private void GeneratorEvent(IGeneratorEvent ev) { - if (ev is IDeniableEvent deniable) - { - if (DisabledKeys.Contains("GENERATORS")) - deniable.IsAllowed = false; - if (ev is IPlayerEvent plrEvent && DisabledForPlayer("GENERATORS", plrEvent.Player)) - deniable.IsAllowed = false; - } + if (ev is not IDeniableEvent deniable) return; + + if (DisabledKeys.Contains("GENERATORS")) + deniable.IsAllowed = false; + if (ev is IPlayerEvent plrEvent && DisabledForPlayer("GENERATORS", plrEvent.Player)) + deniable.IsAllowed = false; } - public void OnShooting(ShootingEventArgs ev) + private void OnShooting(ShootingEventArgs ev) { if (DisabledKeys.Contains("SHOOTING") || DisabledForPlayer("SHOOTING", ev.Player)) ev.IsAllowed = false; } - public void OnDroppingItem(IDeniableEvent ev) + private void OnDroppingItem(IDeniableEvent ev) { if (DisabledKeys.Contains("DROPPING")) ev.IsAllowed = false; @@ -642,7 +658,7 @@ public void OnDroppingItem(IDeniableEvent ev) ev.IsAllowed = false; } - public void OnSearchingPickup(SearchingPickupEventArgs ev) + private void OnSearchingPickup(SearchingPickupEventArgs ev) { if (DisabledKeys.Contains("ITEMPICKUPS") || DisabledForPlayer("ITEMPICKUPS", ev.Player)) ev.IsAllowed = false; @@ -651,19 +667,19 @@ public void OnSearchingPickup(SearchingPickupEventArgs ev) ev.IsAllowed = false; } - public void OnInteractingDoor(InteractingDoorEventArgs ev) + private void OnInteractingDoor(InteractingDoorEventArgs ev) { if (DisabledKeys.Contains("DOORS") || DisabledForPlayer("DOORS", ev.Player)) ev.IsAllowed = false; } - public void OnHandcuffing(HandcuffingEventArgs ev) + private void OnHandcuffing(HandcuffingEventArgs ev) { if (DisabledKeys.Contains("CUFFING") || DisabledForPlayer("CUFFING", ev.Player)) ev.IsAllowed = false; } - public void OnInteractingLocker(InteractingLockerEventArgs ev) + private void OnInteractingLocker(InteractingLockerEventArgs ev) { if (ev.Locker is PedestalScpLocker && (DisabledKeys.Contains("PEDESTALS") || DisabledForPlayer("PEDESTALS", ev.Player))) { @@ -675,7 +691,7 @@ public void OnInteractingLocker(InteractingLockerEventArgs ev) } } - public void OnEscaping(EscapingEventArgs ev) + private void OnEscaping(EscapingEventArgs ev) { if (DisabledKeys.Contains("ESCAPING") || DisabledForPlayer("ESCAPING", ev.Player)) ev.IsAllowed = false; @@ -689,7 +705,7 @@ public void OnEscaping(EscapingEventArgs ev) } // Radio locks - public void OnPickingUpItem(PickingUpItemEventArgs ev) + private void OnPickingUpItem(PickingUpItemEventArgs ev) { if (!ev.IsAllowed) return; @@ -699,19 +715,19 @@ public void OnPickingUpItem(PickingUpItemEventArgs ev) } } - public void OnChangingRadioPreset(ChangingRadioPresetEventArgs ev) + private void OnChangingRadioPreset(ChangingRadioPresetEventArgs ev) { if (LockedRadios.ContainsKey(ev.Player)) ev.IsAllowed = false; } - public void OnInteractingElevator(InteractingElevatorEventArgs ev) + private void OnInteractingElevator(InteractingElevatorEventArgs ev) { if (DisabledKeys.Contains("ELEVATORS") || DisabledForPlayer("ELEVATORS", ev.Player)) ev.IsAllowed = false; } - public void OnHazardEvent(IHazardEvent ev) + private void OnHazardEvent(IHazardEvent ev) { if (ev is IDeniableEvent deny) { @@ -722,7 +738,7 @@ public void OnHazardEvent(IHazardEvent ev) } } - public void OnWorkStationEvent(IDeniableEvent ev) + private void OnWorkStationEvent(IDeniableEvent ev) { if (DisabledKeys.Contains("WORKSTATIONS")) ev.IsAllowed = false; @@ -730,7 +746,7 @@ public void OnWorkStationEvent(IDeniableEvent ev) ev.IsAllowed = false; } - public void OnScp330Event(IDeniableEvent ev) + private void OnScp330Event(IDeniableEvent ev) { if (DisabledKeys.Contains("SCP330")) ev.IsAllowed = false; @@ -738,7 +754,7 @@ public void OnScp330Event(IDeniableEvent ev) ev.IsAllowed = false; } - public void OnScp914Event(IDeniableEvent ev) + private void OnScp914Event(IDeniableEvent ev) { if (DisabledKeys.Contains("SCP914")) ev.IsAllowed = false; diff --git a/ScriptedEvents/API/Modules/EventScriptModule.cs b/ScriptedEvents/API/Modules/EventScriptModule.cs index 80c6067b..569b65a0 100644 --- a/ScriptedEvents/API/Modules/EventScriptModule.cs +++ b/ScriptedEvents/API/Modules/EventScriptModule.cs @@ -1,6 +1,7 @@ namespace ScriptedEvents.API.Modules { using System; + using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -11,44 +12,40 @@ using Exiled.Events.EventArgs.Interfaces; using Exiled.Events.Features; using Exiled.Loader; - - using ScriptedEvents.API.Enums; using ScriptedEvents.API.Extensions; using ScriptedEvents.API.Features; - using ScriptedEvents.API.Features.Exceptions; - using ScriptedEvents.Structures; + using Structures; public class EventScriptModule : SEModule { /// /// Gets an array of Event "Handler" types defined by Exiled. /// - public static Type[] HandlerTypes { get; private set; } + private static Type[] HandlerTypes { get; set; } - public static EventScriptModule Singleton { get; private set; } + public static EventScriptModule? Singleton { get; private set; } public override string Name => "EventScriptModule"; - public List> StoredDelegates { get; } = new(); - - public Dictionary> CurrentEventData { get; set; } + private List> StoredDelegates { get; } = new(); - public Dictionary> CurrentCustomEventData { get; set; } + private Dictionary> CurrentEventData { get; set; } = new(); - public List DynamicallyConnectedEvents { get; set; } = new(); + public Dictionary> CurrentCustomEventData { get; private set; } = new(); - // Connection methods - public static void OnArgumentedEvent(T ev) + private List DynamicallyConnectedEvents { get; set; } = new(); + + private void OnArgumentedEvent(T ev) where T : IExiledEvent { Type evType = typeof(T); string evName = evType.Name.Replace("EventArgs", string.Empty); - Singleton.OnAnyEvent(evName, ev); + Singleton!.OnAnyEvent(evName, ev); } - public static void OnNonArgumentedEvent() + private void OnNonArgumentedEvent() { - Singleton.OnAnyEvent(new StackFrame(2).GetMethod().Name); + Singleton!.OnAnyEvent(new StackFrame(2).GetMethod().Name); } public override void Init() @@ -56,8 +53,16 @@ public override void Init() base.Init(); Singleton = this; - HandlerTypes = Loader.Plugins.First(plug => plug.Name == "Exiled.Events") - .Assembly.GetTypes().Where(t => t.FullName.Equals($"Exiled.Events.Handlers.{t.Name}")).ToArray(); + try + { + HandlerTypes = Loader.Plugins.First(plug => plug.Name == "Exiled.Events") + .Assembly.GetTypes() + .Where(t => t?.FullName != null && t.FullName.Equals($"Exiled.Events.Handlers.{t.Name}")).ToArray(); + } + catch (Exception) + { + Logger.Error($"Fetching HandlerTypes failed! Exiled.Events does not exist in loaded plugins:\n{string.Join(", ", Loader.Plugins.Select(x => x.Name))}"); + } // Events Exiled.Events.Handlers.Server.RestartingRound += TerminateConnections; @@ -77,15 +82,40 @@ public override void Kill() } // Methods to make and destroy connections - public void BeginConnections() + private void BeginConnections() { - if (CurrentEventData is not null) - return; - CurrentEventData = new(); CurrentCustomEventData = new(); - foreach (Script scr in MainPlugin.ScriptModule.ListScripts()) + var scripts = ScriptModule.Singleton!.ListScripts().ToArray(); + RegisterEventScripts(scripts, out var eventScripts); + + scripts = scripts.Where(scr => !eventScripts.Contains(scr)).ToArray(); + + foreach (var script in eventScripts) + { + script.Dispose(); + } + + ScriptModule.Singleton.RegisterAutorunScripts(scripts, out var autoRunScripts); + + foreach (var script in scripts.Where(scr => !autoRunScripts.Contains(scr))) + { + script.Dispose(); + } + + if (ShouldGenerateFiles) + { + Logger.Info($"Thank you for installing Scripted Events! View the README file located at {Path.Combine(MainPlugin.BaseFilePath, "README.txt")} for information on how to use and get the most out of this plugin."); + } + } + + private void RegisterEventScripts(Script[] allScripts, out List