From 287a85867077f661d5a149d18ec62885094526c9 Mon Sep 17 00:00:00 2001 From: Luke Spragg Date: Mon, 13 Nov 2017 23:47:40 -0500 Subject: [PATCH] Initial commit of split/migrated base --- .editorconfig | 9 + .gitattributes | 46 ++ .gitignore | 65 +++ LICENSE.md | 21 + NuGet.config | 16 + Oxide.CSharp.sln | 30 ++ Oxide.CSharp/CSharpExtension.cs | 113 ++++ Oxide.CSharp/CSharpPlugin.cs | 444 ++++++++++++++++ Oxide.CSharp/CSharpPluginLoader.cs | 258 +++++++++ Oxide.CSharp/CompilableFile.cs | 176 +++++++ Oxide.CSharp/CompilablePlugin.cs | 137 +++++ Oxide.CSharp/Compilation.cs | 423 +++++++++++++++ Oxide.CSharp/CompiledAssembly.cs | 316 +++++++++++ Oxide.CSharp/Covalence/Plugin.cs | 100 ++++ Oxide.CSharp/DirectCallMethod.cs | 396 ++++++++++++++ Oxide.CSharp/ExtensionMethods.cs | 9 + .../ObjectStream/Data/CompilerData.cs | 27 + .../ObjectStream/Data/CompilerFile.cs | 30 ++ .../Data/CompilerLanguageVersion.cs | 18 + .../ObjectStream/Data/CompilerMessage.cs | 16 + .../ObjectStream/Data/CompilerMessageType.cs | 14 + .../ObjectStream/Data/CompilerPlatform.cs | 15 + .../ObjectStream/Data/CompilerTarget.cs | 10 + Oxide.CSharp/ObjectStream/IO/BindChanger.cs | 22 + .../ObjectStream/IO/ObjectStreamWrapper.cs | 132 +++++ .../ObjectStream/ObjectStreamClient.cs | 94 ++++ .../ObjectStream/ObjectStreamConnection.cs | 99 ++++ Oxide.CSharp/ObjectStream/Threading/Worker.cs | 49 ++ Oxide.CSharp/Oxide.CSharp.csproj | 36 ++ Oxide.CSharp/PluginCompiler.cs | 498 ++++++++++++++++++ Oxide.CSharp/PluginTimers.cs | 120 +++++ Oxide.CSharp/Utility.cs | 60 +++ 32 files changed, 3799 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 LICENSE.md create mode 100644 NuGet.config create mode 100644 Oxide.CSharp.sln create mode 100644 Oxide.CSharp/CSharpExtension.cs create mode 100644 Oxide.CSharp/CSharpPlugin.cs create mode 100644 Oxide.CSharp/CSharpPluginLoader.cs create mode 100644 Oxide.CSharp/CompilableFile.cs create mode 100644 Oxide.CSharp/CompilablePlugin.cs create mode 100644 Oxide.CSharp/Compilation.cs create mode 100644 Oxide.CSharp/CompiledAssembly.cs create mode 100644 Oxide.CSharp/Covalence/Plugin.cs create mode 100644 Oxide.CSharp/DirectCallMethod.cs create mode 100644 Oxide.CSharp/ExtensionMethods.cs create mode 100644 Oxide.CSharp/ObjectStream/Data/CompilerData.cs create mode 100644 Oxide.CSharp/ObjectStream/Data/CompilerFile.cs create mode 100644 Oxide.CSharp/ObjectStream/Data/CompilerLanguageVersion.cs create mode 100644 Oxide.CSharp/ObjectStream/Data/CompilerMessage.cs create mode 100644 Oxide.CSharp/ObjectStream/Data/CompilerMessageType.cs create mode 100644 Oxide.CSharp/ObjectStream/Data/CompilerPlatform.cs create mode 100644 Oxide.CSharp/ObjectStream/Data/CompilerTarget.cs create mode 100644 Oxide.CSharp/ObjectStream/IO/BindChanger.cs create mode 100644 Oxide.CSharp/ObjectStream/IO/ObjectStreamWrapper.cs create mode 100644 Oxide.CSharp/ObjectStream/ObjectStreamClient.cs create mode 100644 Oxide.CSharp/ObjectStream/ObjectStreamConnection.cs create mode 100644 Oxide.CSharp/ObjectStream/Threading/Worker.cs create mode 100644 Oxide.CSharp/Oxide.CSharp.csproj create mode 100644 Oxide.CSharp/PluginCompiler.cs create mode 100644 Oxide.CSharp/PluginTimers.cs create mode 100644 Oxide.CSharp/Utility.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9573baf --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +# EditorConfig is awesome: http://editorconfig.org +root = true + +[*.cs] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f8ef0c1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,46 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.csproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.modelproj merge=binary +#*.sln merge=binary +#*.sqlproj merge=binary +#*.vbproj merge=binary +#*.vcproj merge=binary +#*.vcxproj merge=binary +#*.wixproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# Behavior for image files +# +# Image files are treated as binary by default. +############################################################################### +*.gif binary +*.ico binary +*.jpg binary +*.png binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..222f5cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,65 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ins. + +# Build output +[Bb]in/ +[Dd]ebug/ +[Dd]ebugPublic/ +[Oo]bj/ +[Rr]elease/ +[Rr]eleases/ + +# Build testing +## MSTest +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* +## NUnit +*.VisualState.xml +TestResult.xml + +# NuGet packages +*.nupkg +**/packages/* +!**/packages/build/ +!**/packages/repositories.config + +# OS-specific files +.*DS_Store +._* + +# Roslyn caches +*.ide/ + +# User-specific files +.idea/ +*.sln.docstates +*.suo +*.user +*.userosscache +*.userprefs + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio +.vs/ +*.psess +*.vsp +*.vspx + +# Visual Studio add-ins +## DotCover code coverage tool +*.dotCover +## JustCode add-in +.JustCode +## ReSharper add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user +## TeamCity add-in +_TeamCity* diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..8c8d2e9 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014-2017 Oxide Team and Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/NuGet.config b/NuGet.config new file mode 100644 index 0000000..7973e6f --- /dev/null +++ b/NuGet.config @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/Oxide.CSharp.sln b/Oxide.CSharp.sln new file mode 100644 index 0000000..1b814c3 --- /dev/null +++ b/Oxide.CSharp.sln @@ -0,0 +1,30 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27004.2008 +MinimumVisualStudioVersion = 15.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Oxide.CSharp", "Oxide.CSharp\Oxide.CSharp.csproj", "{9103D682-D1AA-4A95-A499-896F551AAA62}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9103D682-D1AA-4A95-A499-896F551AAA62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9103D682-D1AA-4A95-A499-896F551AAA62}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9103D682-D1AA-4A95-A499-896F551AAA62}.Debug|x64.ActiveCfg = Debug|Any CPU + {9103D682-D1AA-4A95-A499-896F551AAA62}.Debug|x64.Build.0 = Debug|Any CPU + {9103D682-D1AA-4A95-A499-896F551AAA62}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9103D682-D1AA-4A95-A499-896F551AAA62}.Release|Any CPU.Build.0 = Release|Any CPU + {9103D682-D1AA-4A95-A499-896F551AAA62}.Release|x64.ActiveCfg = Release|Any CPU + {9103D682-D1AA-4A95-A499-896F551AAA62}.Release|x64.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {E0FBE60B-9AEB-45B1-9BB0-2116D724C65F} + EndGlobalSection +EndGlobal diff --git a/Oxide.CSharp/CSharpExtension.cs b/Oxide.CSharp/CSharpExtension.cs new file mode 100644 index 0000000..98d4b2c --- /dev/null +++ b/Oxide.CSharp/CSharpExtension.cs @@ -0,0 +1,113 @@ +using Oxide.Core; +using Oxide.Core.Extensions; +using Oxide.Core.Plugins.Watchers; +using System; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace Oxide.Plugins +{ + /// + /// The extension class that represents this extension + /// + public class CSharpExtension : Extension + { + internal static Assembly Assembly = Assembly.GetExecutingAssembly(); + internal static AssemblyName AssemblyName = Assembly.GetName(); + internal static VersionNumber AssemblyVersion = new VersionNumber(AssemblyName.Version.Major, AssemblyName.Version.Minor, AssemblyName.Version.Build); + internal static string AssemblyAuthors = ((AssemblyCompanyAttribute)Attribute.GetCustomAttribute(Assembly, typeof(AssemblyCompanyAttribute), false)).Company; + + /// + /// Gets whether this extension is a core extension + /// + public override bool IsCoreExtension => true; + + /// + /// Gets the name of this extension + /// + public override string Name => "CSharp"; + + /// + /// Gets the author of this extension + /// + public override string Author => AssemblyAuthors; + + /// + /// Gets the version of this extension + /// + public override VersionNumber Version => AssemblyVersion; + + public FSWatcher Watcher { get; private set; } + + // The .cs plugin loader + private CSharpPluginLoader loader; + + /// + /// Initializes a new instance of the CSharpExtension class + /// + /// + public CSharpExtension(ExtensionManager manager) : base(manager) + { + if (Environment.OSVersion.Platform == PlatformID.Unix) + { + Cleanup.Add(Path.Combine(Interface.Oxide.ExtensionDirectory, "Mono.Posix.dll.config")); + + var extDir = Interface.Oxide.ExtensionDirectory; + var configPath = Path.Combine(extDir, "Oxide.References.dll.config"); + if (File.Exists(configPath) && !(new[] { "target=\"x64", "target=\"./x64" }.Any(File.ReadAllText(configPath).Contains))) return; + + File.WriteAllText(configPath, $"\n\n" + + $"\n"); + } + } + + /// + /// Loads this extension + /// + public override void Load() + { + // Register our loader + loader = new CSharpPluginLoader(this); + Manager.RegisterPluginLoader(loader); + + // Register engine frame callback + Interface.Oxide.OnFrame(OnFrame); + } + + /// + /// Loads plugin watchers used by this extension + /// + /// + public override void LoadPluginWatchers(string pluginDirectory) + { + // Register the watcher + Watcher = new FSWatcher(pluginDirectory, "*.cs"); + Manager.RegisterPluginChangeWatcher(Watcher); + } + + /// + /// Called when all other extensions have been loaded + /// + public override void OnModLoad() => loader.OnModLoaded(); + + public override void OnShutdown() + { + base.OnShutdown(); + loader.OnShutdown(); + } + + /// + /// Called by engine every server frame + /// + private void OnFrame(float delta) + { + var args = new object[] { delta }; + foreach (var kv in loader.LoadedPlugins) + { + var plugin = kv.Value as CSharpPlugin; + if (plugin != null && plugin.HookedOnFrame) plugin.CallHook("OnFrame", args); + } + } + } +} diff --git a/Oxide.CSharp/CSharpPlugin.cs b/Oxide.CSharp/CSharpPlugin.cs new file mode 100644 index 0000000..cb2132b --- /dev/null +++ b/Oxide.CSharp/CSharpPlugin.cs @@ -0,0 +1,444 @@ +using Oxide.Core; +using Oxide.Core.Libraries.Covalence; +using Oxide.Core.Plugins; +using Oxide.Core.Plugins.Watchers; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; + +namespace Oxide.Plugins +{ + public class PluginLoadFailure : Exception + { + public PluginLoadFailure(string reason) + { + } + } + + /// + /// Allows configuration of plugin info using an attribute above the plugin class + /// + [AttributeUsage(AttributeTargets.Class)] + public class InfoAttribute : Attribute + { + public string Title { get; } + public string Author { get; } + public VersionNumber Version { get; private set; } + public int ResourceId { get; set; } + + public InfoAttribute(string Title, string Author, string Version) + { + this.Title = Title; + this.Author = Author; + SetVersion(Version); + } + + public InfoAttribute(string Title, string Author, double Version) + { + this.Title = Title; + this.Author = Author; + SetVersion(Version.ToString()); + } + + private void SetVersion(string version) + { + var versionParts = version.Split('.').Select(part => + { + ushort number; + if (!ushort.TryParse(part, out number)) number = 0; + return number; + }).ToList(); + while (versionParts.Count < 3) versionParts.Add(0); + if (versionParts.Count > 3) Interface.Oxide.LogWarning($"Version `{version}` is invalid for {Title}, should be `major.minor.patch`"); + Version = new VersionNumber(versionParts[0], versionParts[1], versionParts[2]); + } + } + + /// + /// Allows plugins to specify a description of the plugin using an attribute above the plugin class + /// + [AttributeUsage(AttributeTargets.Class)] + public class DescriptionAttribute : Attribute + { + public string Description { get; } + + public DescriptionAttribute(string description) + { + Description = description; + } + } + + /// + /// Indicates that the specified field should be a reference to another plugin when it is loaded + /// + [AttributeUsage(AttributeTargets.Field)] + public class PluginReferenceAttribute : Attribute + { + public string Name { get; } + + public PluginReferenceAttribute() + { + } + + public PluginReferenceAttribute(string name) + { + Name = name; + } + } + + /// + /// Indicates that the specified method should be a handler for a console command + /// + [AttributeUsage(AttributeTargets.Method)] + public class ConsoleCommandAttribute : Attribute + { + public string Command { get; private set; } + + public ConsoleCommandAttribute(string command) + { + Command = command.Contains('.') ? command : ("global." + command); + } + } + + /// + /// Indicates that the specified method should be a handler for a chat command + /// + [AttributeUsage(AttributeTargets.Method)] + public class ChatCommandAttribute : Attribute + { + public string Command { get; private set; } + + public ChatCommandAttribute(string command) + { + Command = command; + } + } + + /// + /// Indicates that the specified Hash field should be used to automatically track online players + /// + [AttributeUsage(AttributeTargets.Field)] + public class OnlinePlayersAttribute : Attribute + { + } + + /// + /// Base class which all dynamic CSharp plugins must inherit + /// + public abstract class CSharpPlugin : CSPlugin + { + /// + /// Wrapper for dynamically managed plugin fields + /// + public class PluginFieldInfo + { + public Plugin Plugin; + public FieldInfo Field; + public Type FieldType; + public Type[] GenericArguments; + public Dictionary Methods = new Dictionary(); + + public PluginFieldInfo(Plugin plugin, FieldInfo field) + { + Plugin = plugin; + Field = field; + FieldType = field.FieldType; + GenericArguments = FieldType.GetGenericArguments(); + } + + public bool HasValidConstructor(params Type[] argument_types) + { + var type = GenericArguments[1]; + return type.GetConstructor(new Type[0]) != null || type.GetConstructor(argument_types) != null; + } + + public object Value => Field.GetValue(Plugin); + + public bool LookupMethod(string method_name, params Type[] argument_types) + { + var method = FieldType.GetMethod(method_name, argument_types); + if (method == null) return false; + Methods[method_name] = method; + return true; + } + + public object Call(string method_name, params object[] args) + { + MethodInfo method; + if (!Methods.TryGetValue(method_name, out method)) + { + method = FieldType.GetMethod(method_name, BindingFlags.Instance | BindingFlags.Public); + Methods[method_name] = method; + } + if (method == null) throw new MissingMethodException(FieldType.Name, method_name); + return method.Invoke(Value, args); + } + } + + public FSWatcher Watcher; + + protected Covalence covalence = Interface.Oxide.GetLibrary(); + protected Core.Libraries.Lang lang = Interface.Oxide.GetLibrary(); + protected Core.Libraries.Plugins plugins = Interface.Oxide.GetLibrary(); + protected Core.Libraries.Permission permission = Interface.Oxide.GetLibrary(); + protected Core.Libraries.WebRequests webrequest = Interface.Oxide.GetLibrary(); + protected PluginTimers timer; + + protected HashSet onlinePlayerFields = new HashSet(); + private Dictionary pluginReferenceFields = new Dictionary(); + + private bool hookDispatchFallback; + + public bool HookedOnFrame + { + get; private set; + } + + public CSharpPlugin() + { + timer = new PluginTimers(this); + + var type = GetType(); + foreach (var field in type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance)) + { + var reference_attributes = field.GetCustomAttributes(typeof(PluginReferenceAttribute), true); + if (reference_attributes.Length > 0) + { + var pluginReference = reference_attributes[0] as PluginReferenceAttribute; + pluginReferenceFields[pluginReference.Name ?? field.Name] = field; + } + } + foreach (var method in type.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)) + { + var info_attributes = method.GetCustomAttributes(typeof(HookMethodAttribute), true); + if (info_attributes.Length > 0) continue; + + if (method.Name.Equals("OnFrame")) HookedOnFrame = true; + // Assume all private instance methods which are not explicitly hooked could be hooks + if (method.DeclaringType.Name == type.Name) AddHookMethod(method.Name, method); + } + } + + public virtual bool SetPluginInfo(string name, string path) + { + Name = name; + Filename = path; + + var infoAttributes = GetType().GetCustomAttributes(typeof(InfoAttribute), true); + if (infoAttributes.Length > 0) + { + var info = infoAttributes[0] as InfoAttribute; + Title = info.Title; + Author = info.Author; + Version = info.Version; + ResourceId = info.ResourceId; + } + else + { + Interface.Oxide.LogWarning($"Failed to load {name}: Info attribute missing"); + return false; + } + + var descriptionAttributes = GetType().GetCustomAttributes(typeof(DescriptionAttribute), true); + if (descriptionAttributes.Length > 0) + { + var info = descriptionAttributes[0] as DescriptionAttribute; + Description = info.Description; + } + + var config = GetType().GetMethod("LoadDefaultConfig", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + HasConfig = config.DeclaringType != typeof(Plugin); + + var messages = GetType().GetMethod("LoadDefaultMessages", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + HasMessages = messages.DeclaringType != typeof(Plugin); + + return true; + } + + public override void HandleAddedToManager(PluginManager manager) + { + base.HandleAddedToManager(manager); + + if (Filename != null) Watcher.AddMapping(Name); + + foreach (var name in pluginReferenceFields.Keys) pluginReferenceFields[name].SetValue(this, manager.GetPlugin(name)); + + /*var compilable_plugin = CSharpPluginLoader.GetCompilablePlugin(Interface.Oxide.PluginDirectory, Name); + if (compilable_plugin != null && compilable_plugin.CompiledAssembly != null) + { + System.IO.File.WriteAllBytes(Interface.Oxide.PluginDirectory + "\\" + Name + ".dump", compilable_plugin.CompiledAssembly.PatchedAssembly); + Interface.Oxide.LogWarning($"The raw assembly has been dumped to Plugins/{Name}.dump"); + }*/ + + try + { + OnCallHook("Loaded", null); + } + catch (Exception ex) + { + Interface.Oxide.LogException($"Failed to initialize plugin '{Name} v{Version}'", ex); + Loader.PluginErrors[Name] = ex.Message; + } + } + + public override void HandleRemovedFromManager(PluginManager manager) + { + if (IsLoaded) CallHook("Unload", null); + + Watcher.RemoveMapping(Name); + + foreach (var name in pluginReferenceFields.Keys) pluginReferenceFields[name].SetValue(this, null); + + base.HandleRemovedFromManager(manager); + } + + public virtual bool DirectCallHook(string name, out object ret, object[] args) + { + ret = null; + return false; + } + + protected override object InvokeMethod(HookMethod method, object[] args) + { + // TODO: Ignore base_ methods for now + if (!hookDispatchFallback && !method.IsBaseHook) + { + if (args != null && args.Length > 0) + { + var parameters = method.Parameters; + for (var i = 0; i < args.Length; i++) + { + var value = args[i]; + if (value == null) continue; + var parameter_type = parameters[i].ParameterType; + if (!parameter_type.IsValueType) continue; + var argument_type = value.GetType(); + if (parameter_type != typeof(object) && argument_type != parameter_type) + args[i] = Convert.ChangeType(value, parameter_type); + } + } + try + { + object ret; + if (DirectCallHook(method.Name, out ret, args)) return ret; + PrintWarning("Unable to call hook directly: " + method.Name); + } + catch (InvalidProgramException ex) + { + Interface.Oxide.LogError("Hook dispatch failure detected, falling back to reflection based dispatch. " + ex); + var compilablePlugin = CSharpPluginLoader.GetCompilablePlugin(Interface.Oxide.PluginDirectory, Name); + if (compilablePlugin?.CompiledAssembly != null) + { + File.WriteAllBytes(Interface.Oxide.PluginDirectory + "\\" + Name + ".dump", compilablePlugin.CompiledAssembly.PatchedAssembly); + Interface.Oxide.LogWarning($"The invalid raw assembly has been dumped to Plugins/{Name}.dump"); + } + hookDispatchFallback = true; + } + } + + return method.Method.Invoke(this, args); + } + + /// + /// Called from Init/Loaded callback to set a failure reason and unload the plugin + /// + /// + public void SetFailState(string reason) + { + throw new PluginLoadFailure(reason); + } + + [HookMethod("OnPluginLoaded")] + private void base_OnPluginLoaded(Plugin plugin) + { + FieldInfo field; + if (pluginReferenceFields.TryGetValue(plugin.Name, out field)) field.SetValue(this, plugin); + } + + [HookMethod("OnPluginUnloaded")] + private void base_OnPluginUnloaded(Plugin plugin) + { + FieldInfo field; + if (pluginReferenceFields.TryGetValue(plugin.Name, out field)) field.SetValue(this, null); + } + + /// + /// Print an info message using the oxide root logger + /// + /// + /// + protected void Puts(string format, params object[] args) + { + Interface.Oxide.LogInfo("[{0}] {1}", Title, args.Length > 0 ? string.Format(format, args) : format); + } + + /// + /// Print a warning message using the oxide root logger + /// + /// + /// + protected void PrintWarning(string format, params object[] args) + { + Interface.Oxide.LogWarning("[{0}] {1}", Title, args.Length > 0 ? string.Format(format, args) : format); + } + + /// + /// Print an error message using the oxide root logger + /// + /// + /// + protected void PrintError(string format, params object[] args) + { + Interface.Oxide.LogError("[{0}] {1}", Title, args.Length > 0 ? string.Format(format, args) : format); + } + + /// + /// Logs a string of text to a named file + /// + /// + /// + /// + /// + protected void LogToFile(string filename, string text, Plugin plugin, bool timeStamp = true) + { + var path = Path.Combine(Interface.Oxide.LogDirectory, plugin.Name); + if (!Directory.Exists(path)) Directory.CreateDirectory(path); + filename = $"{plugin.Name.ToLower()}_{filename.ToLower()}{(timeStamp ? $"-{DateTime.Now:yyyy-MM-dd}" : "")}.txt"; + using (var writer = new StreamWriter(Path.Combine(path, Utility.CleanPath(filename)), true)) writer.WriteLine(text); + } + + /// + /// Queue a callback to be called in the next server frame + /// + /// + protected void NextFrame(Action callback) => Interface.Oxide.NextTick(callback); + + /// + /// Queue a callback to be called in the next server frame + /// + /// + protected void NextTick(Action callback) => Interface.Oxide.NextTick(callback); + + /// + /// Queues a callback to be called from a thread pool worker thread + /// + /// + protected void QueueWorkerThread(Action callback) + { + ThreadPool.QueueUserWorkItem(context => + { + try + { + callback(context); + } + catch (Exception ex) + { + RaiseError($"Exception in '{Name} v{Version}' plugin worker thread: {ex.ToString()}"); + } + }); + } + } +} diff --git a/Oxide.CSharp/CSharpPluginLoader.cs b/Oxide.CSharp/CSharpPluginLoader.cs new file mode 100644 index 0000000..e6adf92 --- /dev/null +++ b/Oxide.CSharp/CSharpPluginLoader.cs @@ -0,0 +1,258 @@ +using Oxide.Core; +using Oxide.Core.Plugins; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Oxide.Plugins +{ + public class CSharpPluginLoader : PluginLoader + { + public static string[] DefaultReferences = { "mscorlib", "Oxide.Core", "Oxide.CSharp", "System", "System.Core", "System.Data" }; + public static HashSet PluginReferences = new HashSet(DefaultReferences); + public static CSharpPluginLoader Instance; + private static Dictionary plugins = new Dictionary(); + private static CSharpExtension extension; + + public static CompilablePlugin GetCompilablePlugin(string directory, string name) + { + var className = Regex.Replace(name, "_", ""); + CompilablePlugin plugin; + if (!plugins.TryGetValue(className, out plugin)) + { + plugin = new CompilablePlugin(extension, Instance, directory, name); + plugins[className] = plugin; + } + return plugin; + } + + public override string FileExtension => ".cs"; + + private List compilationQueue = new List(); + private PluginCompiler compiler; + + public CSharpPluginLoader(CSharpExtension extension) + { + Instance = this; + CSharpPluginLoader.extension = extension; + PluginCompiler.CheckCompilerBinary(); + compiler = new PluginCompiler(); + } + + public void OnModLoaded() + { + // Include references to all loaded game extensions and any assemblies they reference + foreach (var extension in Interface.Oxide.GetAllExtensions()) + { + if (extension == null || !extension.IsCoreExtension && !extension.IsGameExtension) continue; + + var assembly = extension.GetType().Assembly; + PluginReferences.Add(assembly.GetName().Name); + foreach (var reference in assembly.GetReferencedAssemblies()) + if (reference != null) PluginReferences.Add(reference.Name); + } + } + + public override IEnumerable ScanDirectory(string directory) + { + if (PluginCompiler.BinaryPath == null) yield break; + + var enumerable = base.ScanDirectory(directory); + foreach (var file in enumerable) yield return file; + } + + /// + /// Attempt to asynchronously compile and load plugin + /// + /// + /// + /// + public override Plugin Load(string directory, string name) + { + var compilablePlugin = GetCompilablePlugin(directory, name); + if (compilablePlugin.IsLoading) + { + Interface.Oxide.LogDebug($"Load requested for plugin which is already loading: {compilablePlugin.Name}"); + return null; + } + + // Attempt to compile the plugin before unloading the old version + Load(compilablePlugin); + + return null; + } + + /// + /// Attempt to asynchronously compile plugin and only reload if successful + /// + /// + /// + public override void Reload(string directory, string name) + { + if (Regex.Match(directory, @"\\include\b", RegexOptions.IgnoreCase).Success) + { + name = $"Oxide.{name}"; + foreach (var plugin in plugins.Values) + { + if (!plugin.References.Contains(name)) continue; + Interface.Oxide.LogInfo($"Reloading {plugin.Name} because it references updated include file: {name}"); + plugin.LastModifiedAt = DateTime.Now; + Load(plugin); + } + return; + } + + var compilablePlugin = GetCompilablePlugin(directory, name); + if (compilablePlugin.IsLoading) + { + Interface.Oxide.LogDebug($"Reload requested for plugin which is already loading: {compilablePlugin.Name}"); + return; + } + + // Attempt to compile the plugin before unloading the old version + Load(compilablePlugin); + } + + /// + /// Called when the plugin manager is unloading a plugin that was loaded by this plugin loader + /// + /// + public override void Unloading(Plugin pluginBase) + { + var plugin = pluginBase as CSharpPlugin; + if (plugin == null) return; + + LoadedPlugins.Remove(plugin.Name); + + // Unload plugins which require this plugin first + foreach (var compilablePlugin in plugins.Values) + if (compilablePlugin.Requires.Contains(plugin.Name)) Interface.Oxide.UnloadPlugin(compilablePlugin.Name); + } + + public void Load(CompilablePlugin plugin) + { + plugin.Compile(compiled => + { + if (!compiled) + { + PluginLoadingCompleted(plugin); + return; + } + + var loadedLoadingRequirements = plugin.Requires.Where(r => LoadedPlugins.ContainsKey(r) && LoadingPlugins.Contains(r)); + foreach (var loadedPlugin in loadedLoadingRequirements) Interface.Oxide.UnloadPlugin(loadedPlugin); + + var missingRequirements = plugin.Requires.Where(r => !LoadedPlugins.ContainsKey(r)); + if (missingRequirements.Any()) + { + var loadingRequirements = plugin.Requires.Where(r => LoadingPlugins.Contains(r)); + if (loadingRequirements.Any()) + { + Interface.Oxide.LogDebug($"{plugin.Name} plugin is waiting for requirements to be loaded: {loadingRequirements.ToSentence()}"); + } + else + { + Interface.Oxide.LogError($"{plugin.Name} plugin requires missing dependencies: {missingRequirements.ToSentence()}"); + PluginErrors[plugin.Name] = $"Missing dependencies: {missingRequirements.ToSentence()}"; + PluginLoadingCompleted(plugin); + } + } + else + { + Interface.Oxide.UnloadPlugin(plugin.Name); + plugin.LoadPlugin(pl => + { + if (pl != null) LoadedPlugins[pl.Name] = pl; + PluginLoadingCompleted(plugin); + }); + } + }); + } + + /// + /// Called when a CompilablePlugin wants to be compiled + /// + /// + public void CompilationRequested(CompilablePlugin plugin) + { + if (Compilation.Current != null) + { + //Interface.Oxide.LogDebug("Adding plugin to outstanding compilation: {0}", plugin.Name); + Compilation.Current.Add(plugin); + return; + } + if (compilationQueue.Count < 1) + { + Interface.Oxide.NextTick(() => + { + CompileAssembly(compilationQueue.ToArray()); + compilationQueue.Clear(); + }); + } + compilationQueue.Add(plugin); + } + + public void PluginLoadingStarted(CompilablePlugin plugin) + { + // Let the Oxide core know that this plugin will be loading asynchronously + LoadingPlugins.Add(plugin.Name); + plugin.IsLoading = true; + } + + private void PluginLoadingCompleted(CompilablePlugin plugin) + { + LoadingPlugins.Remove(plugin.Name); + plugin.IsLoading = false; + foreach (var loadingName in LoadingPlugins.ToArray()) + { + var loadingPlugin = GetCompilablePlugin(plugin.Directory, loadingName); + if (loadingPlugin.IsLoading && loadingPlugin.Requires.Contains(plugin.Name)) + Load(loadingPlugin); + } + } + + private void CompileAssembly(CompilablePlugin[] plugins) + { + compiler.Compile(plugins, compilation => + { + if (compilation.compiledAssembly == null) + { + foreach (var plugin in compilation.plugins) + { + plugin.OnCompilationFailed(); + PluginErrors[plugin.Name] = $"Failed to compile: {plugin.CompilerErrors}"; + Interface.Oxide.LogError($"Error while compiling: {plugin.CompilerErrors}"); + //RemoteLogger.Warning($"{plugin.ScriptName} plugin failed to compile!\n{plugin.CompilerErrors}"); + } + } + else + { + if (compilation.plugins.Count > 0) + { + var compiledNames = compilation.plugins.Where(pl => string.IsNullOrEmpty(pl.CompilerErrors)).Select(pl => pl.Name).ToArray(); + var verb = compiledNames.Length > 1 ? "were" : "was"; + Interface.Oxide.LogInfo($"{compiledNames.ToSentence()} {verb} compiled successfully in {Math.Round(compilation.duration * 1000f)}ms"); + } + + foreach (var plugin in compilation.plugins) + { + if (plugin.CompilerErrors == null) + { + Interface.Oxide.UnloadPlugin(plugin.Name); + plugin.OnCompilationSucceeded(compilation.compiledAssembly); + } + else + { + plugin.OnCompilationFailed(); + PluginErrors[plugin.Name] = $"Failed to compile: {plugin.CompilerErrors}"; + Interface.Oxide.LogError($"Error while compiling: {plugin.CompilerErrors}"); + } + } + } + }); + } + + public void OnShutdown() => compiler.Shutdown(); + } +} diff --git a/Oxide.CSharp/CompilableFile.cs b/Oxide.CSharp/CompilableFile.cs new file mode 100644 index 0000000..abb935b --- /dev/null +++ b/Oxide.CSharp/CompilableFile.cs @@ -0,0 +1,176 @@ +using Oxide.Core; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace Oxide.Plugins +{ + public class CompilableFile + { + private static Core.Libraries.Timer timer = Interface.Oxide.GetLibrary(); + private static object compileLock = new object(); + + public CSharpExtension Extension; + public CSharpPluginLoader Loader; + public string Name; + public string Directory; + public string ScriptName; + public string ScriptPath; + public string[] ScriptLines; + public Encoding ScriptEncoding; + public HashSet Requires = new HashSet(); + public HashSet References = new HashSet(); + public HashSet IncludePaths = new HashSet(); + public string CompilerErrors; + public CompiledAssembly CompiledAssembly; + public DateTime LastModifiedAt; + public DateTime LastCachedScriptAt; + public DateTime LastCompiledAt; + public bool IsCompilationNeeded; + + protected Action LoadCallback; + protected Action CompileCallback; + protected float CompilationQueuedAt; + + private Core.Libraries.Timer.TimerInstance timeoutTimer; + + public byte[] ScriptSource => ScriptEncoding.GetBytes(string.Join(Environment.NewLine, ScriptLines)); + + public CompilableFile(CSharpExtension extension, CSharpPluginLoader loader, string directory, string name) + { + Extension = extension; + Loader = loader; + Directory = directory; + ScriptName = name; + ScriptPath = Path.Combine(Directory, $"{ScriptName}.cs"); + Name = Regex.Replace(ScriptName, "_", ""); + CheckLastModificationTime(); + } + + internal void Compile(Action callback) + { + lock (compileLock) + { + if (CompilationQueuedAt > 0f) + { + var ago = Interface.Oxide.Now - CompilationQueuedAt; + Interface.Oxide.LogDebug($"Plugin compilation is already queued: {ScriptName} ({ago:0.000} ago)"); + //RemoteLogger.Debug($"Plugin compilation is already queued: {ScriptName} ({ago:0.000} ago)"); + return; + } + OnLoadingStarted(); + if (CompiledAssembly != null && !HasBeenModified()) + { + if (CompiledAssembly.IsLoading || !CompiledAssembly.IsBatch || CompiledAssembly.CompilablePlugins.All(pl => pl.IsLoading)) + { + //Interface.Oxide.LogDebug("Plugin is already compiled: {0}", Name); + callback(true); + return; + } + } + IsCompilationNeeded = true; + CompileCallback = callback; + CompilationQueuedAt = Interface.Oxide.Now; + OnCompilationRequested(); + } + } + + internal virtual void OnCompilationStarted() + { + //Interface.Oxide.LogDebug("Compiling plugin: {0}", Name); + LastCompiledAt = LastModifiedAt; + timeoutTimer?.Destroy(); + timeoutTimer = null; + Interface.Oxide.NextTick(() => + { + timeoutTimer?.Destroy(); + timeoutTimer = timer.Once(60f, OnCompilationTimeout); + }); + } + + internal void OnCompilationSucceeded(CompiledAssembly compiledAssembly) + { + if (timeoutTimer == null) + { + Interface.Oxide.LogWarning($"Ignored unexpected plugin compilation: {Name}"); + return; + } + timeoutTimer?.Destroy(); + timeoutTimer = null; + IsCompilationNeeded = false; + CompilationQueuedAt = 0f; + CompiledAssembly = compiledAssembly; + CompileCallback?.Invoke(true); + } + + internal void OnCompilationFailed() + { + if (timeoutTimer == null) + { + Interface.Oxide.LogWarning($"Ignored unexpected plugin compilation failure: {Name}"); + return; + } + timeoutTimer?.Destroy(); + timeoutTimer = null; + CompilationQueuedAt = 0f; + LastCompiledAt = default(DateTime); + CompileCallback?.Invoke(false); + IsCompilationNeeded = false; + } + + internal void OnCompilationTimeout() + { + Interface.Oxide.LogError("Timed out waiting for plugin to be compiled: " + Name); + CompilerErrors = "Timed out waiting for compilation"; + OnCompilationFailed(); + } + + internal bool HasBeenModified() + { + var lastModifiedAt = LastModifiedAt; + CheckLastModificationTime(); + return LastModifiedAt != lastModifiedAt; + } + + internal void CheckLastModificationTime() + { + if (!File.Exists(ScriptPath)) + { + LastModifiedAt = default(DateTime); + return; + } + var modifiedTime = GetLastModificationTime(); + if (modifiedTime != default(DateTime)) LastModifiedAt = modifiedTime; + } + + internal DateTime GetLastModificationTime() + { + try + { + return File.GetLastWriteTime(ScriptPath); + } + catch (IOException ex) + { + Interface.Oxide.LogError("IOException while checking plugin: {0} ({1})", ScriptName, ex.Message); + return default(DateTime); + } + } + + protected virtual void OnLoadingStarted() + { + } + + protected virtual void OnCompilationRequested() + { + } + + protected virtual void InitFailed(string message = null) + { + if (message != null) Interface.Oxide.LogError(message); + LoadCallback?.Invoke(null); + } + } +} diff --git a/Oxide.CSharp/CompilablePlugin.cs b/Oxide.CSharp/CompilablePlugin.cs new file mode 100644 index 0000000..e0a3795 --- /dev/null +++ b/Oxide.CSharp/CompilablePlugin.cs @@ -0,0 +1,137 @@ +using Oxide.Core; +using System; +using System.Reflection; + +namespace Oxide.Plugins +{ + public class CompilablePlugin : CompilableFile + { + private static object compileLock = new object(); + + public CompiledAssembly LastGoodAssembly; + public bool IsLoading; + + public CompilablePlugin(CSharpExtension extension, CSharpPluginLoader loader, string directory, string name) : base(extension, loader, directory, name) + { + } + + protected override void OnLoadingStarted() => Loader.PluginLoadingStarted(this); + + protected override void OnCompilationRequested() => Loader.CompilationRequested(this); + + internal void LoadPlugin(Action callback = null) + { + if (CompiledAssembly == null) + { + Interface.Oxide.LogError("Load called before a compiled assembly exists: {0}", Name); + //RemoteLogger.Error($"Load called before a compiled assembly exists: {Name}"); + return; + } + + LoadCallback = callback; + + CompiledAssembly.LoadAssembly(loaded => + { + if (!loaded) + { + callback?.Invoke(null); + return; + } + + if (CompilerErrors != null) + { + InitFailed($"Unable to load {ScriptName}. {CompilerErrors}"); + return; + } + + var type = CompiledAssembly.LoadedAssembly.GetType($"Oxide.Plugins.{Name}"); + if (type == null) + { + InitFailed($"Unable to find main plugin class: {Name}"); + return; + } + + CSharpPlugin plugin; + try + { + plugin = Activator.CreateInstance(type) as CSharpPlugin; + } + catch (MissingMethodException) + { + InitFailed($"Main plugin class should not have a constructor defined: {Name}"); + return; + } + catch (TargetInvocationException invocationException) + { + var ex = invocationException.InnerException; + InitFailed($"Unable to load {ScriptName}. {ex.ToString()}"); + return; + } + catch (Exception ex) + { + InitFailed($"Unable to load {ScriptName}. {ex.ToString()}"); + return; + } + + if (plugin == null) + { + //RemoteLogger.Error($"Plugin assembly failed to load: {ScriptName}"); + InitFailed($"Plugin assembly failed to load: {ScriptName}"); + return; + } + + if (!plugin.SetPluginInfo(ScriptName, ScriptPath)) + { + InitFailed(); + return; + } + + plugin.Watcher = Extension.Watcher; + plugin.Loader = Loader; + + if (!Interface.Oxide.PluginLoaded(plugin)) + { + InitFailed(); + return; + } + + if (!CompiledAssembly.IsBatch) LastGoodAssembly = CompiledAssembly; + callback?.Invoke(plugin); + }); + } + + internal override void OnCompilationStarted() + { + base.OnCompilationStarted(); + + // Enqueue compilation of any plugins which depend on this plugin + foreach (var plugin in Interface.Oxide.RootPluginManager.GetPlugins()) + { + if (!(plugin is CSharpPlugin)) continue; + var compilablePlugin = CSharpPluginLoader.GetCompilablePlugin(Directory, plugin.Name); + if (!compilablePlugin.Requires.Contains(Name)) continue; + compilablePlugin.CompiledAssembly = null; + Loader.Load(compilablePlugin); + } + } + + protected override void InitFailed(string message = null) + { + base.InitFailed(message); + if (LastGoodAssembly == null) + { + Interface.Oxide.LogInfo("No previous version to rollback plugin: {0}", ScriptName); + return; + } + if (CompiledAssembly == LastGoodAssembly) + { + Interface.Oxide.LogInfo("Previous version of plugin failed to load: {0}", ScriptName); + return; + } + Interface.Oxide.LogInfo("Rolling back plugin to last good version: {0}", ScriptName); + CompiledAssembly = LastGoodAssembly; + CompilerErrors = null; + LoadPlugin(); + } + } +} diff --git a/Oxide.CSharp/Compilation.cs b/Oxide.CSharp/Compilation.cs new file mode 100644 index 0000000..f59a547 --- /dev/null +++ b/Oxide.CSharp/Compilation.cs @@ -0,0 +1,423 @@ +using ObjectStream.Data; +using Oxide.Core; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Threading; + +namespace Oxide.Plugins +{ + internal class Compilation + { + public static Compilation Current; + + internal int id; + internal string name; + internal Action callback; + internal ConcurrentHashSet queuedPlugins; + internal HashSet plugins = new HashSet(); + internal float startedAt; + internal float endedAt; + internal Hash references = new Hash(); + internal HashSet referencedPlugins = new HashSet(); + internal CompiledAssembly compiledAssembly; + internal float duration => endedAt - startedAt; + + private string includePath; + private string[] extensionNames; + private string gameExtensionName; + private string gameExtensionNamespace; + + internal Compilation(int id, Action callback, CompilablePlugin[] plugins) + { + this.id = id; + this.callback = callback; + this.queuedPlugins = new ConcurrentHashSet(plugins); + + if (Current == null) Current = this; + + foreach (var plugin in plugins) + { + plugin.CompilerErrors = null; + plugin.OnCompilationStarted(); + } + + includePath = Path.Combine(Interface.Oxide.PluginDirectory, "include"); + extensionNames = Interface.Oxide.GetAllExtensions().Select(ext => ext.Name).ToArray(); + var gameExtension = Interface.Oxide.GetAllExtensions().SingleOrDefault(ext => ext.IsGameExtension); + gameExtensionName = gameExtension?.Name.ToUpper(); + gameExtensionNamespace = gameExtension?.GetType().Namespace; + } + + internal void Started() + { + startedAt = Interface.Oxide.Now; + name = (plugins.Count < 2 ? plugins.First().Name : "plugins_") + Math.Round(Interface.Oxide.Now * 10000000f) + ".dll"; + } + + internal void Completed(byte[] rawAssembly = null) + { + endedAt = Interface.Oxide.Now; + if (plugins.Count > 0 && rawAssembly != null) + compiledAssembly = new CompiledAssembly(name, plugins.ToArray(), rawAssembly, duration); + Interface.Oxide.NextTick(() => callback(this)); + } + + internal void Add(CompilablePlugin plugin) + { + if (!queuedPlugins.Add(plugin)) return; + + plugin.Loader.PluginLoadingStarted(plugin); + plugin.CompilerErrors = null; + plugin.OnCompilationStarted(); + + foreach (var pl in Interface.Oxide.RootPluginManager.GetPlugins().Where(pl => pl is CSharpPlugin)) + { + var loadedPlugin = CSharpPluginLoader.GetCompilablePlugin(plugin.Directory, pl.Name); + if (!loadedPlugin.Requires.Contains(plugin.Name)) continue; + + AddDependency(loadedPlugin); + } + } + + internal bool IncludesRequiredPlugin(string name) + { + if (referencedPlugins.Contains(name)) return true; + + var compilablePlugin = plugins.SingleOrDefault(pl => pl.Name == name); + return compilablePlugin != null && compilablePlugin.CompilerErrors == null; + } + + internal void Prepare(Action callback) + { + ThreadPool.QueueUserWorkItem(_ => + { + try + { + referencedPlugins.Clear(); + references.Clear(); + + // Include references made by the CSharpPlugins project + foreach (var filename in CSharpPluginLoader.PluginReferences) + { + if (File.Exists(Path.Combine(Interface.Oxide.ExtensionDirectory, filename + ".dll"))) + references[filename + ".dll"] = new CompilerFile(Interface.Oxide.ExtensionDirectory, filename + ".dll"); + if (File.Exists(Path.Combine(Interface.Oxide.ExtensionDirectory, filename + ".exe"))) + references[filename + ".exe"] = new CompilerFile(Interface.Oxide.ExtensionDirectory, filename + ".exe"); + } + + //Interface.Oxide.LogDebug("Preparing compilation"); + + CompilablePlugin plugin; + while (queuedPlugins.TryDequeue(out plugin)) + { + if (Current == null) Current = this; + + if (!CacheScriptLines(plugin) || plugin.ScriptLines.Length < 1) + { + plugin.References.Clear(); + plugin.IncludePaths.Clear(); + plugin.Requires.Clear(); + Interface.Oxide.LogWarning("Plugin script is empty: " + plugin.Name); + RemovePlugin(plugin); + } + else if (plugins.Add(plugin)) + { + //Interface.Oxide.LogDebug("Adding plugin to compilation: " + plugin.Name); + PreparseScript(plugin); + ResolveReferences(plugin); + } + else + { + //Interface.Oxide.LogDebug("Plugin is already part of compilation: " + plugin.Name); + } + + CacheModifiedScripts(); + + // We don't want the main thread to be able to add more plugins which could be missed + if (queuedPlugins.Count == 0 && Current == this) + { + Current = null; + //Interface.Oxide.LogDebug("Probably done preparing compilation: " + plugins.Select(p => p.Name).ToSentence()); + } + } + + //Interface.Oxide.LogDebug("Done preparing compilation: " + plugins.Select(p => p.Name).ToSentence()); + + callback(); + } + catch (Exception ex) + { + Interface.Oxide.LogException("Exception while resolving plugin references", ex); + //RemoteLogger.Exception("Exception while resolving plugin references", ex); + } + }); + } + + private void PreparseScript(CompilablePlugin plugin) + { + plugin.References.Clear(); + plugin.IncludePaths.Clear(); + plugin.Requires.Clear(); + + var parsingNamespace = false; + for (var i = 0; i < plugin.ScriptLines.Length; i++) + { + var line = plugin.ScriptLines[i].Trim(); + if (line.Length < 1) continue; + + Match match; + if (parsingNamespace) + { + // Skip blank lines and opening brace at the top of the namespace block + match = Regex.Match(line, @"^\s*\{?\s*$", RegexOptions.IgnoreCase); + if (match.Success) continue; + + // Skip class custom attributes + match = Regex.Match(line, @"^\s*\[", RegexOptions.IgnoreCase); + if (match.Success) continue; + + // Detect main plugin class name + match = Regex.Match(line, @"^\s*(?:public|private|protected|internal)?\s*class\s+(\S+)\s+\:\s+\S+Plugin\s*$", RegexOptions.IgnoreCase); + if (!match.Success) break; + + var className = match.Groups[1].Value; + if (className != plugin.Name) + { + Interface.Oxide.LogError($"Plugin filename {plugin.ScriptName}.cs must match the main class {className} (should be {className}.cs)"); + plugin.CompilerErrors = $"Plugin filename {plugin.ScriptName}.cs must match the main class {className} (should be {className}.cs)"; + RemovePlugin(plugin); + } + + break; + } + + // Include explicit plugin dependencies defined by magic comments in script + match = Regex.Match(line, @"^//\s*Requires:\s*(\S+?)(\.cs)?\s*$", RegexOptions.IgnoreCase); + if (match.Success) + { + var dependencyName = match.Groups[1].Value; + plugin.Requires.Add(dependencyName); + if (!File.Exists(Path.Combine(plugin.Directory, dependencyName + ".cs"))) + { + Interface.Oxide.LogError($"{plugin.Name} plugin requires missing dependency: {dependencyName}"); + plugin.CompilerErrors = $"Missing dependency: {dependencyName}"; + RemovePlugin(plugin); + return; + } + + //Interface.Oxide.LogDebug(plugin.Name + " plugin requires dependency: " + dependency_name); + var dependencyPlugin = CSharpPluginLoader.GetCompilablePlugin(plugin.Directory, dependencyName); + AddDependency(dependencyPlugin); + continue; + } + + // Include explicit references defined by magic comments in script + match = Regex.Match(line, @"^//\s*Reference:\s*(\S+)\s*$", RegexOptions.IgnoreCase); + if (match.Success) + { + var result = match.Groups[1].Value; + // TODO: Fix Oxide.References to avoid these and other dependency conflicts + if (!result.StartsWith("Oxide.") && !result.StartsWith("Newtonsoft.Json") && !result.StartsWith("Rust.Workshop")) + { + AddReference(plugin, result); + Interface.Oxide.LogInfo("Added '// Reference: {0}' in plugin '{1}'", result, plugin.Name); + } + else + Interface.Oxide.LogWarning("Ignored unnecessary '// Reference: {0}' in plugin '{1}'", result, plugin.Name); + continue; + } + + // Include implicit references detected from using statements in script + match = Regex.Match(line, @"^\s*using\s+(Oxide\.(?:Core|Ext|Game)\.(?:[^\.]+))[^;]*;.*$", RegexOptions.IgnoreCase); + if (match.Success) + { + var result = match.Groups[1].Value; + var newResult = Regex.Replace(result, @"Oxide\.[\w]+\.([\w]+)", "Oxide.$1"); + if (!string.IsNullOrEmpty(newResult) && File.Exists(Path.Combine(Interface.Oxide.ExtensionDirectory, newResult + ".dll"))) + AddReference(plugin, newResult); + else + AddReference(plugin, result); + continue; + } + + // Start parsing the Oxide.Plugins namespace contents + match = Regex.Match(line, @"^\s*namespace Oxide\.Plugins\s*(\{\s*)?$", RegexOptions.IgnoreCase); + if (match.Success) parsingNamespace = true; + } + } + + private void ResolveReferences(CompilablePlugin plugin) + { + foreach (var reference in plugin.References) + { + var match = Regex.Match(reference, @"^(Oxide\.(?:Ext|Game)\.(.+))$", RegexOptions.IgnoreCase); + if (!match.Success) continue; + + var fullName = match.Groups[1].Value; + var name = match.Groups[2].Value; + if (extensionNames.Contains(name)) continue; + + if (Directory.Exists(includePath)) + { + var includeFilePath = Path.Combine(includePath, $"Ext.{name}.cs"); + if (File.Exists(includeFilePath)) + { + plugin.IncludePaths.Add(includeFilePath); + continue; + } + } + + var message = $"{fullName} is referenced by {plugin.Name} plugin but is not loaded! An appropriate include file needs to be saved to plugins\\include\\Ext.{name}.cs if this extension is not required."; + Interface.Oxide.LogError(message); + plugin.CompilerErrors = message; + RemovePlugin(plugin); + } + } + + private void AddDependency(CompilablePlugin plugin) + { + if (plugin.IsLoading || plugins.Contains(plugin) || queuedPlugins.Contains(plugin)) return; + + var compiledDependency = plugin.CompiledAssembly; + if (compiledDependency != null && !compiledDependency.IsOutdated()) + { + // The dependency already has a compiled assembly which is up to date + referencedPlugins.Add(plugin.Name); + if (!references.ContainsKey(compiledDependency.Name)) + references[compiledDependency.Name] = new CompilerFile(compiledDependency.Name, compiledDependency.RawAssembly); + } + else + { + // The dependency needs to be compiled + Add(plugin); + } + } + + private void AddReference(CompilablePlugin plugin, string assemblyName) + { + var path = Path.Combine(Interface.Oxide.ExtensionDirectory, assemblyName + ".dll"); + if (!File.Exists(path)) + { + if (assemblyName.StartsWith("Oxide.")) + { + plugin.References.Add(assemblyName); + return; + } + + Interface.Oxide.LogError($"Assembly referenced by {plugin.Name} plugin does not exist: {assemblyName}.dll"); + plugin.CompilerErrors = $"Referenced assembly does not exist: {assemblyName}"; + RemovePlugin(plugin); + return; + } + + Assembly assembly; + try + { + assembly = Assembly.Load(assemblyName); + } + catch (FileNotFoundException) + { + Interface.Oxide.LogError($"Assembly referenced by {plugin.Name} plugin is invalid: {assemblyName}.dll"); + plugin.CompilerErrors = $"Referenced assembly is invalid: {assemblyName}"; + RemovePlugin(plugin); + return; + } + + AddReference(plugin, assembly.GetName()); + + // Include references made by the referenced assembly + foreach (var reference in assembly.GetReferencedAssemblies()) + { + // TODO: Fix Oxide.References to avoid these and other dependency conflicts + if (reference.Name.StartsWith("Newtonsoft.Json") || reference.Name.StartsWith("Rust.Workshop")) continue; + + var referencePath = Path.Combine(Interface.Oxide.ExtensionDirectory, reference.Name + ".dll"); + if (!File.Exists(referencePath)) + { + Interface.Oxide.LogWarning($"Reference {reference.Name}.dll from {assembly.GetName().Name}.dll not found"); + continue; + } + + AddReference(plugin, reference); + } + } + + private void AddReference(CompilablePlugin plugin, AssemblyName reference) + { + var filename = reference.Name + ".dll"; + if (!references.ContainsKey(filename)) references[filename] = new CompilerFile(Interface.Oxide.ExtensionDirectory, filename); + plugin.References.Add(reference.Name); + } + + private bool CacheScriptLines(CompilablePlugin plugin) + { + var waitingForAccess = false; + while (true) + { + try + { + if (!File.Exists(plugin.ScriptPath)) + { + Interface.Oxide.LogWarning("Script no longer exists: {0}", plugin.Name); + plugin.CompilerErrors = "Plugin file was deleted"; + RemovePlugin(plugin); + return false; + } + + plugin.CheckLastModificationTime(); + if (plugin.LastCachedScriptAt != plugin.LastModifiedAt) + { + using (var reader = File.OpenText(plugin.ScriptPath)) + { + var lines = new List(); + while (!reader.EndOfStream) lines.Add(reader.ReadLine()); + if (!string.IsNullOrEmpty(gameExtensionName)) lines.Insert(0, $"#define {gameExtensionName}"); + plugin.ScriptLines = lines.ToArray(); + plugin.ScriptEncoding = reader.CurrentEncoding; + } + plugin.LastCachedScriptAt = plugin.LastModifiedAt; + if (plugins.Remove(plugin)) + queuedPlugins.Add(plugin); + } + return true; + } + catch (IOException) + { + if (!waitingForAccess) + { + waitingForAccess = true; + Interface.Oxide.LogWarning("Waiting for another application to stop using script: {0}", plugin.Name); + } + Thread.Sleep(50); + } + } + } + + private void CacheModifiedScripts() + { + var modifiedPlugins = plugins.Where(pl => pl.ScriptLines == null || pl.HasBeenModified() || pl.LastCachedScriptAt != pl.LastModifiedAt).ToArray(); + if (modifiedPlugins.Length < 1) return; + + foreach (var plugin in modifiedPlugins) CacheScriptLines(plugin); + Thread.Sleep(100); + CacheModifiedScripts(); + } + + private void RemovePlugin(CompilablePlugin plugin) + { + if (plugin.LastCompiledAt == default(DateTime)) return; + + queuedPlugins.Remove(plugin); + plugins.Remove(plugin); + plugin.OnCompilationFailed(); + + // Remove plugins which are required by this plugin if they are only being compiled for this requirement + foreach (var requiredPlugin in plugins.Where(pl => !pl.IsCompilationNeeded && plugin.Requires.Contains(pl.Name)).ToArray()) + if (!plugins.Any(pl => pl.Requires.Contains(requiredPlugin.Name))) RemovePlugin(requiredPlugin); + } + } +} diff --git a/Oxide.CSharp/CompiledAssembly.cs b/Oxide.CSharp/CompiledAssembly.cs new file mode 100644 index 0000000..128d37c --- /dev/null +++ b/Oxide.CSharp/CompiledAssembly.cs @@ -0,0 +1,316 @@ +extern alias Oxide; + +using Oxide.Core; +using Oxide.Core.CSharp; +using Oxide::Mono.Cecil; +using Oxide::Mono.Cecil.Cil; +using Oxide::Mono.Cecil.Rocks; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; + +using MethodAttributes = Oxide::Mono.Cecil.MethodAttributes; +using MethodBody = Oxide::Mono.Cecil.Cil.MethodBody; + +namespace Oxide.Plugins +{ + public class CompiledAssembly + { + public CompilablePlugin[] CompilablePlugins; + public string[] PluginNames; + public string Name; + public DateTime CompiledAt; + public byte[] RawAssembly; + public byte[] PatchedAssembly; + public float Duration; + public Assembly LoadedAssembly; + public bool IsLoading; + public bool IsBatch => CompilablePlugins.Length > 1; + + private List> loadCallbacks = new List>(); + private bool isPatching; + private bool isLoaded; + + private static IEnumerable BlacklistedNamespaces => new[] { + "Oxide.Core.ServerConsole", "System.IO", "System.Net", "System.Xml", "System.Reflection.Assembly", "System.Reflection.Emit", "System.Threading", + "System.Runtime.InteropServices", "System.Diagnostics", "System.Security", "System.Timers", "Mono.CSharp", "Mono.Cecil", + "ServerFileSystem" + }; + + private static IEnumerable WhitelistedNamespaces => new[] { + "System.Diagnostics.Stopwatch", "System.IO.MemoryStream", "System.IO.Stream", "System.IO.BinaryReader", "System.IO.BinaryWriter", + "System.Net.Dns.GetHostEntry", "System.Net.Sockets.SocketFlags", "System.Net.IPEndPoint", "System.Security.Cryptography", "System.Threading.Interlocked" + }; + + public CompiledAssembly(string name, CompilablePlugin[] plugins, byte[] rawAssembly, float duration) + { + Name = name; + CompilablePlugins = plugins; + RawAssembly = rawAssembly; + Duration = duration; + PluginNames = CompilablePlugins.Select(pl => pl.Name).ToArray(); + } + + public void LoadAssembly(Action callback) + { + if (isLoaded) + { + callback(true); + return; + } + + IsLoading = true; + loadCallbacks.Add(callback); + if (isPatching) return; + + PatchAssembly(rawAssembly => + { + if (rawAssembly == null) + { + foreach (var cb in loadCallbacks) cb(true); + loadCallbacks.Clear(); + IsLoading = false; + return; + } + + LoadedAssembly = Assembly.Load(rawAssembly); + isLoaded = true; + + foreach (var cb in loadCallbacks) cb(true); + loadCallbacks.Clear(); + + IsLoading = false; + }); + } + + private void PatchAssembly(Action callback) + { + if (isPatching) + { + Interface.Oxide.LogWarning("Already patching plugin assembly: {0} (ignoring)", PluginNames.ToSentence()); + //RemoteLogger.Warning($"Already patching plugin assembly: {PluginNames.ToSentence()}"); + return; + } + + var startedAt = Interface.Oxide.Now; + + isPatching = true; + ThreadPool.QueueUserWorkItem(_ => + { + try + { + AssemblyDefinition definition; + using (var stream = new MemoryStream(RawAssembly)) + definition = AssemblyDefinition.ReadAssembly(stream); + + var exceptionConstructor = typeof(UnauthorizedAccessException).GetConstructor(new[] { typeof(string) }); + var securityException = definition.MainModule.Import(exceptionConstructor); + + Action patchModuleType = null; + patchModuleType = type => + { + foreach (var method in type.Methods) + { + var changedMethod = false; + + if (method.Body == null) + { + if (method.HasPInvokeInfo) + { + method.Attributes &= ~MethodAttributes.PInvokeImpl; + var body = new MethodBody(method); + body.Instructions.Add(Instruction.Create(OpCodes.Ldstr, "PInvoke access is restricted, you are not allowed to use PInvoke")); + body.Instructions.Add(Instruction.Create(OpCodes.Newobj, securityException)); + body.Instructions.Add(Instruction.Create(OpCodes.Throw)); + method.Body = body; + } + } + else + { + var replacedMethod = false; + foreach (var variable in method.Body.Variables) + { + if (!IsNamespaceBlacklisted(variable.VariableType.FullName)) continue; + + var body = new MethodBody(method); + body.Instructions.Add(Instruction.Create(OpCodes.Ldstr, $"System access is restricted, you are not allowed to use {variable.VariableType.FullName}")); + body.Instructions.Add(Instruction.Create(OpCodes.Newobj, securityException)); + body.Instructions.Add(Instruction.Create(OpCodes.Throw)); + method.Body = body; + replacedMethod = true; + break; + } + if (replacedMethod) continue; + + var instructions = method.Body.Instructions; + var ilProcessor = method.Body.GetILProcessor(); + var first = instructions.First(); + + var i = 0; + while (i < instructions.Count) + { + if (changedMethod) break; + var instruction = instructions[i]; + if (instruction.OpCode == OpCodes.Ldtoken) + { + var operand = instruction.Operand as IMetadataTokenProvider; + var token = operand?.ToString(); + if (IsNamespaceBlacklisted(token)) + { + ilProcessor.InsertBefore(first, Instruction.Create(OpCodes.Ldstr, $"System access is restricted, you are not allowed to use {token}")); + ilProcessor.InsertBefore(first, Instruction.Create(OpCodes.Newobj, securityException)); + ilProcessor.InsertBefore(first, Instruction.Create(OpCodes.Throw)); + changedMethod = true; + } + } + else if (instruction.OpCode == OpCodes.Call || instruction.OpCode == OpCodes.Calli || instruction.OpCode == OpCodes.Callvirt || instruction.OpCode == OpCodes.Ldftn) + { + var methodCall = instruction.Operand as MethodReference; + var fullNamespace = methodCall?.DeclaringType.FullName; + + if ((fullNamespace == "System.Type" && methodCall.Name == "GetType") || IsNamespaceBlacklisted(fullNamespace)) + { + ilProcessor.InsertBefore(first, Instruction.Create(OpCodes.Ldstr, $"System access is restricted, you are not allowed to use {fullNamespace}")); + ilProcessor.InsertBefore(first, Instruction.Create(OpCodes.Newobj, securityException)); + ilProcessor.InsertBefore(first, Instruction.Create(OpCodes.Throw)); + changedMethod = true; + } + } + else if (instruction.OpCode == OpCodes.Ldfld) + { + var fieldType = instruction.Operand as FieldReference; + var fullNamespace = fieldType?.FieldType.FullName; + if (IsNamespaceBlacklisted(fullNamespace)) + { + ilProcessor.InsertBefore(first, Instruction.Create(OpCodes.Ldstr, $"System access is restricted, you are not allowed to use {fullNamespace}")); + ilProcessor.InsertBefore(first, Instruction.Create(OpCodes.Newobj, securityException)); + ilProcessor.InsertBefore(first, Instruction.Create(OpCodes.Throw)); + changedMethod = true; + } + } + i++; + } + } + + if (changedMethod) + { + method.Body?.OptimizeMacros(); + /*//Interface.Oxide.LogDebug("Updating {0} instruction offsets: {1}", instructions.Count, method.FullName); + int curoffset = 0; + for (var i = 0; i < instructions.Count; i++) + { + var instruction = instructions[i]; + instruction.Previous = (i == 0) ? null : instructions[i - 1]; + instruction.Next = (i == instructions.Count - 1) ? null : instructions[i + 1]; + instruction.Offset = curoffset; + curoffset += instruction.GetSize(); + //Interface.Oxide.LogDebug(" {0}", instruction.ToString()); + }*/ + } + } + foreach (var nestedType in type.NestedTypes) + patchModuleType(nestedType); + }; + + foreach (var type in definition.MainModule.Types) + { + patchModuleType(type); + + if (IsCompilerGenerated(type)) continue; + + if (type.Namespace == "Oxide.Plugins") + { + if (PluginNames.Contains(type.Name)) + { + var constructor = + type.Methods.FirstOrDefault( + m => !m.IsStatic && m.IsConstructor && !m.HasParameters && !m.IsPublic); + if (constructor != null) + { + var plugin = CompilablePlugins.SingleOrDefault(p => p.Name == type.Name); + if (plugin != null) + plugin.CompilerErrors = "Primary constructor in main class must be public"; + } + else + { + new DirectCallMethod(definition.MainModule, type); + } + } + else + { + Interface.Oxide.LogWarning(PluginNames.Length == 1 + ? $"{PluginNames[0]} has polluted the global namespace by defining {type.Name}" + : $"A plugin has polluted the global namespace by defining {type.Name}"); + //RemoteLogger.Info($"A plugin has polluted the global namespace by defining {type.Name}: {PluginNames.ToSentence()}"); + } + } + else if (type.FullName != "") + { + if (!PluginNames.Any(plugin => type.FullName.StartsWith($"Oxide.Plugins.{plugin}"))) + Interface.Oxide.LogWarning(PluginNames.Length == 1 + ? $"{PluginNames[0]} has polluted the global namespace by defining {type.FullName}" + : $"A plugin has polluted the global namespace by defining {type.FullName}"); + } + } + + // TODO: Why is there no error on boot using this? + foreach (var type in definition.MainModule.Types) + { + if (type.Namespace != "Oxide.Plugins" || !PluginNames.Contains(type.Name)) continue; + foreach (var m in type.Methods.Where(m => !m.IsStatic && !m.HasGenericParameters && !m.ReturnType.IsGenericParameter && !m.IsSetter && !m.IsGetter)) + { + foreach (var parameter in m.Parameters) + { + foreach (var attribute in parameter.CustomAttributes) + { + //Interface.Oxide.LogInfo($"{m.FullName} - {parameter.Name} - {attribute.Constructor.FullName}"); + } + } + } + } + + using (var stream = new MemoryStream()) + { + definition.Write(stream); + PatchedAssembly = stream.ToArray(); + } + + Interface.Oxide.NextTick(() => + { + isPatching = false; + //Interface.Oxide.LogDebug("Patching {0} assembly took {1:0.00} ms", ScriptName, Interface.Oxide.Now - startedAt); + callback(PatchedAssembly); + }); + } + catch (Exception ex) + { + Interface.Oxide.NextTick(() => + { + isPatching = false; + Interface.Oxide.LogException($"Exception while patching: {PluginNames.ToSentence()}", ex); + //RemoteLogger.Exception($"Exception while patching: {PluginNames.ToSentence()}", ex); + callback(null); + }); + } + }); + } + + public bool IsOutdated() => CompilablePlugins.Any(pl => pl.GetLastModificationTime() != CompiledAt); + + private bool IsCompilerGenerated(TypeDefinition type) => type.CustomAttributes.Any(attr => attr.Constructor.DeclaringType.ToString().Contains("CompilerGeneratedAttribute")); + + private static bool IsNamespaceBlacklisted(string fullNamespace) + { + foreach (var namespaceName in BlacklistedNamespaces) + { + if (!fullNamespace.StartsWith(namespaceName)) continue; + if (WhitelistedNamespaces.Any(fullNamespace.StartsWith)) continue; + return true; + } + return false; + } + } +} diff --git a/Oxide.CSharp/Covalence/Plugin.cs b/Oxide.CSharp/Covalence/Plugin.cs new file mode 100644 index 0000000..a5d1ac9 --- /dev/null +++ b/Oxide.CSharp/Covalence/Plugin.cs @@ -0,0 +1,100 @@ +using Oxide.Core; +using Oxide.Core.Libraries.Covalence; +using Oxide.Core.Plugins; +using System; +using System.Reflection; + +namespace Oxide.Plugins +{ + /// + /// Indicates that the specified method should be a handler for a covalence command + /// + [AttributeUsage(AttributeTargets.Method)] + public class CommandAttribute : Attribute + { + public string[] Commands { get; } + + public CommandAttribute(params string[] commands) + { + Commands = commands; + } + } + + /// + /// Indicates that the specified method requires a specific permission + /// + [AttributeUsage(AttributeTargets.Method)] + public class PermissionAttribute : Attribute + { + public string[] Permission { get; } + + public PermissionAttribute(string permission) + { + Permission = new[] { permission }; + } + } + + public class CovalencePlugin : CSharpPlugin + { + private new static readonly Covalence covalence = Interface.Oxide.GetLibrary(); + + protected string game = covalence.Game; + protected IPlayerManager players = covalence.Players; + protected IServer server = covalence.Server; + + /// + /// Print an info message using the oxide root logger + /// + /// + /// + protected void Log(string format, params object[] args) + { + Interface.Oxide.LogInfo("[{0}] {1}", Title, args.Length > 0 ? string.Format(format, args) : format); + } + + /// + /// Print a warning message using the oxide root logger + /// + /// + /// + protected void LogWarning(string format, params object[] args) + { + Interface.Oxide.LogWarning("[{0}] {1}", Title, args.Length > 0 ? string.Format(format, args) : format); + } + + /// + /// Print an error message using the oxide root logger + /// + /// + /// + protected void LogError(string format, params object[] args) + { + Interface.Oxide.LogError("[{0}] {1}", Title, args.Length > 0 ? string.Format(format, args) : format); + } + + /// + /// Called when this plugin has been added to the specified manager + /// + /// + public override void HandleAddedToManager(PluginManager manager) + { + foreach (var method in GetType().GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)) + { + var commandAttribute = method.GetCustomAttributes(typeof(CommandAttribute), true); + var permissionAttribute = method.GetCustomAttributes(typeof(PermissionAttribute), true); + if (commandAttribute.Length <= 0) continue; + + var cmd = commandAttribute[0] as CommandAttribute; + var perm = permissionAttribute.Length <= 0 ? null : permissionAttribute[0] as PermissionAttribute; + if (cmd == null) continue; + AddCovalenceCommand(cmd.Commands, perm?.Permission, (caller, command, args) => + { + CallHook(method.Name, caller, command, args); + return true; + }); + } + + base.HandleAddedToManager(manager); + } + } +} diff --git a/Oxide.CSharp/DirectCallMethod.cs b/Oxide.CSharp/DirectCallMethod.cs new file mode 100644 index 0000000..d577030 --- /dev/null +++ b/Oxide.CSharp/DirectCallMethod.cs @@ -0,0 +1,396 @@ +extern alias Oxide; + +using Oxide.Core.Plugins; +using Oxide::Mono.Cecil; +using Oxide::Mono.Cecil.Cil; +using Oxide::Mono.Cecil.Rocks; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Oxide.Core.CSharp +{ + public class DirectCallMethod + { + public class Node + { + public char Char; + public string Name; + public Dictionary Edges = new Dictionary(); + public Node Parent; + public Instruction FirstInstruction; + } + + private ModuleDefinition module; + private TypeDefinition type; + private MethodDefinition method; + private MethodBody body; + private Instruction endInstruction; + + private Dictionary jumpToEdgePlaceholderTargets = new Dictionary(); + private List jumpToEndPlaceholders = new List(); + + private Dictionary hookMethods = new Dictionary(); + + private MethodReference getLength; + private MethodReference getChars; + private MethodReference isNullOrEmpty; + private MethodReference stringEquals; + + private string hook_attribute = typeof(HookMethodAttribute).FullName; + + public DirectCallMethod(ModuleDefinition module, TypeDefinition type) + { + this.module = module; + this.type = type; + + getLength = module.Import(typeof(string).GetMethod("get_Length", new Type[0])); + getChars = module.Import(typeof(string).GetMethod("get_Chars", new[] { typeof(int) })); + isNullOrEmpty = module.Import(typeof(string).GetMethod("IsNullOrEmpty", new[] { typeof(string) })); + stringEquals = module.Import(typeof(string).GetMethod("Equals", new[] { typeof(string) })); + + // Copy method definition from base class + var base_assembly = AssemblyDefinition.ReadAssembly(Path.Combine(Interface.Oxide.ExtensionDirectory, "Oxide.CSharp.dll")); + var base_module = base_assembly.MainModule; + var base_type = module.Import(base_assembly.MainModule.GetType("Oxide.Plugins.CSharpPlugin")).Resolve(); + var base_method = module.Import(base_type.Methods.First(method => method.Name == "DirectCallHook")).Resolve(); + + // Create method override based on virtual method signature + method = new MethodDefinition(base_method.Name, base_method.Attributes, base_module.Import(base_method.ReturnType)) { DeclaringType = type }; + foreach (var parameter in base_method.Parameters) + { + var new_param = new ParameterDefinition(parameter.Name, parameter.Attributes, base_module.Import(parameter.ParameterType)) + { + IsOut = parameter.IsOut, + Constant = parameter.Constant, + MarshalInfo = parameter.MarshalInfo, + IsReturnValue = parameter.IsReturnValue + }; + foreach (var attribute in parameter.CustomAttributes) + new_param.CustomAttributes.Add(new CustomAttribute(module.Import(attribute.Constructor))); + method.Parameters.Add(new_param); + } + + foreach (var attribute in base_method.CustomAttributes) + method.CustomAttributes.Add(new CustomAttribute(module.Import(attribute.Constructor))); + + method.ImplAttributes = base_method.ImplAttributes; + method.SemanticsAttributes = base_method.SemanticsAttributes; + + // Replace the NewSlot attribute with ReuseSlot + method.Attributes &= ~MethodAttributes.NewSlot; + method.Attributes |= MethodAttributes.ReuseSlot; + + // Create new method body + body = new MethodBody(method); + body.SimplifyMacros(); + method.Body = body; + type.Methods.Add(method); + + // Create variables + body.Variables.Add(new VariableDefinition("name_size", module.TypeSystem.Int32)); + body.Variables.Add(new VariableDefinition("i", module.TypeSystem.Int32)); + + // Initialize return value to null + AddInstruction(OpCodes.Ldarg_2); + AddInstruction(OpCodes.Ldnull); + AddInstruction(OpCodes.Stind_Ref); + + // Check for name null or empty + AddInstruction(OpCodes.Ldarg_1); + AddInstruction(OpCodes.Call, isNullOrEmpty); + var empty = AddInstruction(OpCodes.Brfalse, body.Instructions[0]); + Return(false); + + // Get method name length + empty.Operand = AddInstruction(OpCodes.Ldarg_1); + AddInstruction(OpCodes.Callvirt, getLength); + AddInstruction(OpCodes.Stloc_0); + + // Initialize i counter variable to 0 + AddInstruction(OpCodes.Ldc_I4_0); + AddInstruction(OpCodes.Stloc_1); + + // Find all hook methods defined by the plugin + foreach (var m in type.Methods.Where(m => !m.IsStatic && (m.IsPrivate || IsHookMethod(m)) && !m.HasGenericParameters && !m.ReturnType.IsGenericParameter && m.DeclaringType == type && !m.IsSetter && !m.IsGetter)) + { + //ignore compiler generated + if (m.Name.Contains("<")) continue; + + var name = m.Name; + if (m.Parameters.Count > 0) + name += $"({string.Join(", ", m.Parameters.Select(x => x.ParameterType.ToString().Replace("/", "+").Replace("<", "[").Replace(">", "]")).ToArray())})"; + + if (!hookMethods.ContainsKey(name)) hookMethods[name] = m; + } + + // Build a hook method name trie + var root_node = new Node(); + foreach (var method_name in hookMethods.Keys) + { + var current_node = root_node; + for (var i = 1; i <= method_name.Length; i++) + { + var letter = method_name[i - 1]; + Node next_node; + if (!current_node.Edges.TryGetValue(letter, out next_node)) + { + next_node = new Node { Parent = current_node, Char = letter }; + current_node.Edges[letter] = next_node; + } + if (i == method_name.Length) next_node.Name = method_name; + current_node = next_node; + } + } + + // Build conditional method call logic from trie nodes + var n = 1; + foreach (var edge in root_node.Edges.Keys) + BuildNode(root_node.Edges[edge], n++); + + // No valid method was found + endInstruction = Return(false); + + foreach (var instruction in jumpToEdgePlaceholderTargets.Keys) + { + instruction.Operand = jumpToEdgePlaceholderTargets[instruction].FirstInstruction; + } + + foreach (var instruction in jumpToEndPlaceholders) + { + instruction.Operand = endInstruction; + } + + body.OptimizeMacros(); + } + + private bool IsHookMethod(MethodDefinition method) + { + foreach (var attribute in method.CustomAttributes) + { + if (attribute.AttributeType.FullName == hook_attribute) + return true; + } + return false; + } + + private void BuildNode(Node node, int edge_number) + { + // Check the char index lower than length on first edge + if (edge_number == 1) + { + node.FirstInstruction = AddInstruction(OpCodes.Ldloc_1); + AddInstruction(OpCodes.Ldloc_0); + jumpToEndPlaceholders.Add(AddInstruction(OpCodes.Bge, body.Instructions[0])); + } + + // Check the char at the current position + if (edge_number == 1) + AddInstruction(OpCodes.Ldarg_1); //method_name + else + node.FirstInstruction = AddInstruction(OpCodes.Ldarg_1); + AddInstruction(OpCodes.Ldloc_1); // i + AddInstruction(OpCodes.Callvirt, getChars); // method_name[i] + AddInstruction(Ldc_I4_n(node.Char)); + + if (node.Parent.Edges.Count > edge_number) + { + // If char does not match and there are more edges to check + JumpToEdge(node.Parent.Edges.Values.ElementAt(edge_number)); + } + else + { + // If char does not match and there are no more edges to check + JumpToEnd(); + } + + if (node.Edges.Count == 1 && node.Name == null) + { + var last_edge = node; + while (last_edge.Edges.Count == 1 && last_edge.Name == null) + last_edge = last_edge.Edges.Values.First(); + if (last_edge.Edges.Count == 0 && last_edge.Name != null) + { + // There is only one remaining possible hook on this path + AddInstruction(OpCodes.Ldarg_1); + AddInstruction(Instruction.Create(OpCodes.Ldstr, last_edge.Name)); + AddInstruction(OpCodes.Callvirt, stringEquals); + // If the full method name does not match the only remaining possible hook, return false + jumpToEndPlaceholders.Add(AddInstruction(OpCodes.Brfalse, body.Instructions[0])); + + // Method has been found + CallMethod(hookMethods[last_edge.Name]); + Return(true); + + return; + } + } + + // Method continuing with this char exists, increment position + AddInstruction(OpCodes.Ldloc_1); + AddInstruction(OpCodes.Ldc_I4_1); + AddInstruction(OpCodes.Add); + AddInstruction(OpCodes.Stloc_1); + + if (node.Name != null) + { + // Check if we are at the end of the method name + AddInstruction(OpCodes.Ldloc_1); + AddInstruction(OpCodes.Ldloc_0); + // If the method name is longer than the current position + if (node.Edges.Count > 0) + JumpToEdge(node.Edges.Values.First()); + else + JumpToEnd(); + + // Method has been found + CallMethod(hookMethods[node.Name]); + Return(true); + } + + var n = 1; + foreach (var edge in node.Edges.Keys) + BuildNode(node.Edges[edge], n++); + } + + private void CallMethod(MethodDefinition method) + { + var paramDict = new Dictionary(); + //check for ref/out param + for (var i = 0; i < method.Parameters.Count; i++) + { + var parameter = method.Parameters[i]; + var param = parameter.ParameterType as ByReferenceType; + if (param != null) + { + var refParam = AddVariable(module.Import(param.ElementType)); + AddInstruction(OpCodes.Ldarg_3); // object[] params + AddInstruction(Ldc_I4_n(i)); // param_number + AddInstruction(OpCodes.Ldelem_Ref); + AddInstruction(OpCodes.Unbox_Any, module.Import(param.ElementType)); + AddInstruction(OpCodes.Stloc_S, refParam); + paramDict[parameter] = refParam; + } + } + + if (method.ReturnType.Name != "Void") AddInstruction(OpCodes.Ldarg_2); // out object ret + + AddInstruction(OpCodes.Ldarg_0); // this + for (var i = 0; i < method.Parameters.Count; i++) + { + var parameter = method.Parameters[i]; + var param = parameter.ParameterType as ByReferenceType; + if (param != null) + { + AddInstruction(OpCodes.Ldloca, paramDict[parameter]); + } + else + { + // TODO: Handle params array? + AddInstruction(OpCodes.Ldarg_3); // object[] params + AddInstruction(Ldc_I4_n(i)); // param_number + AddInstruction(OpCodes.Ldelem_Ref); + AddInstruction(OpCodes.Unbox_Any, module.Import(parameter.ParameterType)); + } + } + AddInstruction(OpCodes.Call, module.Import(method)); + + //handle ref/out params + for (var i = 0; i < method.Parameters.Count; i++) + { + var parameter = method.Parameters[i]; + var param = parameter.ParameterType as ByReferenceType; + if (param != null) + { + AddInstruction(OpCodes.Ldarg_3); // object[] params + AddInstruction(Ldc_I4_n(i)); // param_number + AddInstruction(OpCodes.Ldloc_S, paramDict[parameter]); + AddInstruction(OpCodes.Box, module.Import(param.ElementType)); + AddInstruction(OpCodes.Stelem_Ref); + } + } + + if (method.ReturnType.Name != "Void") + { + if (method.ReturnType.Name != "Object") AddInstruction(OpCodes.Box, module.Import(method.ReturnType)); + AddInstruction(OpCodes.Stind_Ref); + } + } + + private Instruction Return(bool value) + { + var instruction = AddInstruction(Ldc_I4_n(value ? 1 : 0)); + AddInstruction(OpCodes.Ret); + return instruction; + } + + private void JumpToEdge(Node node) + { + var instruction = AddInstruction(OpCodes.Bne_Un, body.Instructions[1]); + jumpToEdgePlaceholderTargets[instruction] = node; + } + + private void JumpToEnd() + { + jumpToEndPlaceholders.Add(AddInstruction(OpCodes.Bne_Un, body.Instructions[0])); + } + + private Instruction AddInstruction(OpCode opcode) + { + return AddInstruction(Instruction.Create(opcode)); + } + + private Instruction AddInstruction(OpCode opcode, Instruction instruction) + { + return AddInstruction(Instruction.Create(opcode, instruction)); + } + + private Instruction AddInstruction(OpCode opcode, MethodReference method_reference) + { + return AddInstruction(Instruction.Create(opcode, method_reference)); + } + + private Instruction AddInstruction(OpCode opcode, TypeReference type_reference) + { + return AddInstruction(Instruction.Create(opcode, type_reference)); + } + + private Instruction AddInstruction(OpCode opcode, int value) + { + return AddInstruction(Instruction.Create(opcode, value)); + } + + private Instruction AddInstruction(OpCode opcode, VariableDefinition value) + { + return AddInstruction(Instruction.Create(opcode, value)); + } + + private Instruction AddInstruction(Instruction instruction) + { + body.Instructions.Add(instruction); + return instruction; + } + + public VariableDefinition AddVariable(TypeReference typeRef, string name = "") + { + var def = new VariableDefinition(name, typeRef); + body.Variables.Add(def); + return def; + } + + private Instruction Ldc_I4_n(int n) + { + if (n == 0) return Instruction.Create(OpCodes.Ldc_I4_0); + if (n == 1) return Instruction.Create(OpCodes.Ldc_I4_1); + if (n == 2) return Instruction.Create(OpCodes.Ldc_I4_2); + if (n == 3) return Instruction.Create(OpCodes.Ldc_I4_3); + if (n == 4) return Instruction.Create(OpCodes.Ldc_I4_4); + if (n == 5) return Instruction.Create(OpCodes.Ldc_I4_5); + if (n == 6) return Instruction.Create(OpCodes.Ldc_I4_6); + if (n == 7) return Instruction.Create(OpCodes.Ldc_I4_7); + if (n == 8) return Instruction.Create(OpCodes.Ldc_I4_8); + return Instruction.Create(OpCodes.Ldc_I4_S, (sbyte)n); + } + } +} diff --git a/Oxide.CSharp/ExtensionMethods.cs b/Oxide.CSharp/ExtensionMethods.cs new file mode 100644 index 0000000..cfdfb33 --- /dev/null +++ b/Oxide.CSharp/ExtensionMethods.cs @@ -0,0 +1,9 @@ +namespace Oxide.Plugins +{ + /// + /// Useful extension methods which are added to base types + /// + public static class ExtensionMethods + { + } +} diff --git a/Oxide.CSharp/ObjectStream/Data/CompilerData.cs b/Oxide.CSharp/ObjectStream/Data/CompilerData.cs new file mode 100644 index 0000000..8b259b8 --- /dev/null +++ b/Oxide.CSharp/ObjectStream/Data/CompilerData.cs @@ -0,0 +1,27 @@ +using System; + +namespace ObjectStream.Data +{ + [Serializable] + internal class CompilerData + { + public CompilerData() + { + StdLib = false; + Target = CompilerTarget.Library; + Platform = CompilerPlatform.AnyCPU; + Version = CompilerLanguageVersion.Default; + LoadDefaultReferences = false; + SdkVersion = "2"; + } + public bool LoadDefaultReferences { get; set; } + public string OutputFile { get; set; } + public CompilerPlatform Platform { get; set; } + public CompilerFile[] ReferenceFiles { get; set; } + public string SdkVersion { get; set; } + public CompilerFile[] SourceFiles { get; set; } + public bool StdLib { get; set; } + public CompilerTarget Target { get; set; } + public CompilerLanguageVersion Version { get; set; } + } +} diff --git a/Oxide.CSharp/ObjectStream/Data/CompilerFile.cs b/Oxide.CSharp/ObjectStream/Data/CompilerFile.cs new file mode 100644 index 0000000..35a6355 --- /dev/null +++ b/Oxide.CSharp/ObjectStream/Data/CompilerFile.cs @@ -0,0 +1,30 @@ +using System; +using System.IO; + +namespace ObjectStream.Data +{ + [Serializable] + internal class CompilerFile + { + public string Name { get; set; } + public byte[] Data { get; set; } + + internal CompilerFile(string name, byte[] data) + { + Name = name; + Data = data; + } + + internal CompilerFile(string directory, string name) + { + Name = name; + Data = File.ReadAllBytes(Path.Combine(directory, Name)); + } + + internal CompilerFile(string path) + { + Name = Path.GetFileName(path); + Data = File.ReadAllBytes(path); + } + } +} diff --git a/Oxide.CSharp/ObjectStream/Data/CompilerLanguageVersion.cs b/Oxide.CSharp/ObjectStream/Data/CompilerLanguageVersion.cs new file mode 100644 index 0000000..a960fa9 --- /dev/null +++ b/Oxide.CSharp/ObjectStream/Data/CompilerLanguageVersion.cs @@ -0,0 +1,18 @@ +using System; + +namespace ObjectStream.Data +{ + [Serializable] + internal enum CompilerLanguageVersion + { + ISO_1 = 1, + ISO_2 = 2, + V_3 = 3, + V_4 = 4, + V_5 = 5, + V_6 = 6, + Experimental = 100, + + Default = V_6 + } +} diff --git a/Oxide.CSharp/ObjectStream/Data/CompilerMessage.cs b/Oxide.CSharp/ObjectStream/Data/CompilerMessage.cs new file mode 100644 index 0000000..0caaf71 --- /dev/null +++ b/Oxide.CSharp/ObjectStream/Data/CompilerMessage.cs @@ -0,0 +1,16 @@ +using System; + +namespace ObjectStream.Data +{ + [Serializable] + internal class CompilerMessage + { + public object Data { get; set; } + + public object ExtraData { get; set; } + + public int Id { get; set; } + + public CompilerMessageType Type { get; set; } + } +} diff --git a/Oxide.CSharp/ObjectStream/Data/CompilerMessageType.cs b/Oxide.CSharp/ObjectStream/Data/CompilerMessageType.cs new file mode 100644 index 0000000..e836dc1 --- /dev/null +++ b/Oxide.CSharp/ObjectStream/Data/CompilerMessageType.cs @@ -0,0 +1,14 @@ +using System; + +namespace ObjectStream.Data +{ + [Serializable] + internal enum CompilerMessageType + { + Assembly, + Compile, + Error, + Exit, + Ready + } +} diff --git a/Oxide.CSharp/ObjectStream/Data/CompilerPlatform.cs b/Oxide.CSharp/ObjectStream/Data/CompilerPlatform.cs new file mode 100644 index 0000000..54c0b98 --- /dev/null +++ b/Oxide.CSharp/ObjectStream/Data/CompilerPlatform.cs @@ -0,0 +1,15 @@ +using System; + +namespace ObjectStream.Data +{ + [Serializable] + internal enum CompilerPlatform + { + AnyCPU, + AnyCPU32Preferred, + Arm, + X86, + X64, + IA64 + } +} diff --git a/Oxide.CSharp/ObjectStream/Data/CompilerTarget.cs b/Oxide.CSharp/ObjectStream/Data/CompilerTarget.cs new file mode 100644 index 0000000..cb20bfa --- /dev/null +++ b/Oxide.CSharp/ObjectStream/Data/CompilerTarget.cs @@ -0,0 +1,10 @@ +using System; + +namespace ObjectStream.Data +{ + [Serializable] + internal enum CompilerTarget + { + Library, Exe, Module, WinExe + } +} diff --git a/Oxide.CSharp/ObjectStream/IO/BindChanger.cs b/Oxide.CSharp/ObjectStream/IO/BindChanger.cs new file mode 100644 index 0000000..2c91c02 --- /dev/null +++ b/Oxide.CSharp/ObjectStream/IO/BindChanger.cs @@ -0,0 +1,22 @@ +using System; +using System.Reflection; +using System.Runtime.Serialization; + +namespace ObjectStream.IO +{ + public class BindChanger : SerializationBinder + { + public override Type BindToType(string assemblyName, string typeName) + { + /*if (typeName.Contains(".")) typeName = typeName.Substring(typeName.LastIndexOf(".", StringComparison.Ordinal) + 1); + if (typeName.Contains("+")) typeName = typeName.Substring(typeName.LastIndexOf("+", StringComparison.Ordinal) + 1); + + var type = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(x => x.GetTypes()) + .FirstOrDefault(x => x.Name.Equals(typeName)); + + return type != null ? Type.GetType(string.Format("{0}, {1}", type.FullName, Assembly.GetExecutingAssembly().FullName)) : null;*/ + return Type.GetType(string.Format("{0}, {1}", typeName, Assembly.GetExecutingAssembly().FullName)); + } + } +} diff --git a/Oxide.CSharp/ObjectStream/IO/ObjectStreamWrapper.cs b/Oxide.CSharp/ObjectStream/IO/ObjectStreamWrapper.cs new file mode 100644 index 0000000..2a3c34a --- /dev/null +++ b/Oxide.CSharp/ObjectStream/IO/ObjectStreamWrapper.cs @@ -0,0 +1,132 @@ +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Runtime.Serialization.Formatters; +using System.Runtime.Serialization.Formatters.Binary; +using System.Text; + +namespace ObjectStream.IO +{ + public class ObjectStreamWrapper : ObjectStreamWrapper + where TReadWrite : class + { + public ObjectStreamWrapper(Stream inStream, Stream outStream) : base(inStream, outStream) + { + } + } + + public class ObjectStreamWrapper + where TRead : class + where TWrite : class + { + private readonly BinaryFormatter _binaryFormatter = new BinaryFormatter { Binder = new BindChanger(), AssemblyFormat = FormatterAssemblyStyle.Simple }; + private readonly Stream _inStream; + private readonly Stream _outStream; + + private bool _run; + + public ObjectStreamWrapper(Stream inStream, Stream outStream) + { + _inStream = inStream; + _outStream = outStream; + _run = true; + } + + public bool CanRead => _run && _inStream.CanRead; + + public bool CanWrite => _run && _outStream.CanWrite; + + public void Close() + { + if (!_run) return; + _run = false; + try + { + _outStream.Close(); + } + catch (Exception) { } + try + { + _inStream.Close(); + } + catch (Exception) { } + } + + public TRead ReadObject() + { + var len = ReadLength(); + return len == 0 ? null : ReadObject(len); + } + + #region Private stream readers + + private int ReadLength() + { + const int lensize = sizeof(int); + var lenbuf = new byte[lensize]; + var bytesRead = _inStream.Read(lenbuf, 0, lensize); + if (bytesRead == 0) return 0; + if (bytesRead != lensize) + { + // TODO: Hack to ignore BOM + Array.Resize(ref lenbuf, Encoding.UTF8.GetPreamble().Length); + if (Encoding.UTF8.GetPreamble().SequenceEqual(lenbuf)) return ReadLength(); + throw new IOException(string.Format("Expected {0} bytes but read {1}", lensize, bytesRead)); + } + return IPAddress.NetworkToHostOrder(BitConverter.ToInt32(lenbuf, 0)); + } + + private TRead ReadObject(int len) + { + var data = new byte[len]; + int count; + int sum = 0; + while (len - sum > 0 && (count = _inStream.Read(data, sum, len - sum)) > 0) + sum += count; + using (var memoryStream = new MemoryStream(data)) + { + return (TRead)_binaryFormatter.Deserialize(memoryStream); + } + } + + #endregion Private stream readers + + public void WriteObject(TWrite obj) + { + var data = Serialize(obj); + WriteLength(data.Length); + WriteObject(data); + Flush(); + } + + #region Private stream writers + + private byte[] Serialize(TWrite obj) + { + using (var memoryStream = new MemoryStream()) + { + _binaryFormatter.Serialize(memoryStream, obj); + return memoryStream.ToArray(); + } + } + + private void WriteLength(int len) + { + var lenbuf = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(len)); + _outStream.Write(lenbuf, 0, lenbuf.Length); + } + + private void WriteObject(byte[] data) + { + _outStream.Write(data, 0, data.Length); + } + + private void Flush() + { + _outStream.Flush(); + } + + #endregion Private stream writers + } +} diff --git a/Oxide.CSharp/ObjectStream/ObjectStreamClient.cs b/Oxide.CSharp/ObjectStream/ObjectStreamClient.cs new file mode 100644 index 0000000..06167a1 --- /dev/null +++ b/Oxide.CSharp/ObjectStream/ObjectStreamClient.cs @@ -0,0 +1,94 @@ +using ObjectStream.IO; +using ObjectStream.Threading; +using System; +using System.IO; + +namespace ObjectStream +{ + public class ObjectStreamClient : ObjectStreamClient where TReadWrite : class + { + public ObjectStreamClient(Stream inStream, Stream outStream) : base(inStream, outStream) + { + } + } + + public delegate void StreamExceptionEventHandler(Exception exception); + + public class ObjectStreamClient + where TRead : class + where TWrite : class + { + private readonly Stream _inStream; + private readonly Stream _outStream; + private ObjectStreamConnection _connection; + + public ObjectStreamClient(Stream inStream, Stream outStream) + { + _inStream = inStream; + _outStream = outStream; + } + + public event ConnectionMessageEventHandler Message; + + public event StreamExceptionEventHandler Error; + + public void Start() + { + var worker = new Worker(); + worker.Error += OnError; + worker.DoWork(ListenSync); + } + + public void PushMessage(TWrite message) + { + if (_connection != null) + _connection.PushMessage(message); + } + + public void Stop() + { + if (_connection != null) + _connection.Close(); + } + + #region Private methods + + private void ListenSync() + { + // Create a Connection object for the data pipe + _connection = ConnectionFactory.CreateConnection(_inStream, _outStream); + _connection.ReceiveMessage += OnReceiveMessage; + _connection.Error += ConnectionOnError; + _connection.Open(); + } + + private void OnReceiveMessage(ObjectStreamConnection connection, TRead message) + { + if (Message != null) + Message(connection, message); + } + + private void ConnectionOnError(ObjectStreamConnection connection, Exception exception) + { + OnError(exception); + } + + private void OnError(Exception exception) + { + if (Error != null) + Error(exception); + } + + #endregion Private methods + } + + internal static class ObjectStreamClientFactory + { + public static ObjectStreamWrapper Connect(Stream inStream, Stream outStream) + where TRead : class + where TWrite : class + { + return new ObjectStreamWrapper(inStream, outStream); + } + } +} diff --git a/Oxide.CSharp/ObjectStream/ObjectStreamConnection.cs b/Oxide.CSharp/ObjectStream/ObjectStreamConnection.cs new file mode 100644 index 0000000..f9eb44d --- /dev/null +++ b/Oxide.CSharp/ObjectStream/ObjectStreamConnection.cs @@ -0,0 +1,99 @@ +using ObjectStream.IO; +using ObjectStream.Threading; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; + +namespace ObjectStream +{ + public class ObjectStreamConnection + where TRead : class + where TWrite : class + { + private readonly ObjectStreamWrapper _streamWrapper; + private readonly Queue _writeQueue = new Queue(); + private readonly AutoResetEvent _writeSignal = new AutoResetEvent(false); + + internal ObjectStreamConnection(Stream inStream, Stream outStream) + { + _streamWrapper = new ObjectStreamWrapper(inStream, outStream); + } + + public event ConnectionMessageEventHandler ReceiveMessage; + + public event ConnectionExceptionEventHandler Error; + + public void Open() + { + var readWorker = new Worker(); + readWorker.Error += OnError; + readWorker.DoWork(ReadStream); + + var writeWorker = new Worker(); + writeWorker.Error += OnError; + writeWorker.DoWork(WriteStream); + } + + public void PushMessage(TWrite message) + { + _writeQueue.Enqueue(message); + _writeSignal.Set(); + } + + public void Close() => CloseImpl(); + + private void CloseImpl() + { + Error = null; + _streamWrapper.Close(); + _writeSignal.Set(); + } + + private void OnError(Exception exception) + { + if (Error != null) + Error(this, exception); + } + + private void ReadStream() + { + while (_streamWrapper.CanRead) + { + var obj = _streamWrapper.ReadObject(); + ReceiveMessage?.Invoke(this, obj); + if (obj != null) continue; + CloseImpl(); + return; + } + } + + private void WriteStream() + { + while (_streamWrapper.CanWrite) + { + _writeSignal.WaitOne(); + while (_writeQueue.Count > 0) + _streamWrapper.WriteObject(_writeQueue.Dequeue()); + } + } + } + + internal static class ConnectionFactory + { + public static ObjectStreamConnection CreateConnection(Stream inStream, Stream outStream) + where TRead : class + where TWrite : class + { + return new ObjectStreamConnection(inStream, outStream); + } + } + + public delegate void ConnectionMessageEventHandler(ObjectStreamConnection connection, TRead message) + where TRead : class + where TWrite : class; + + public delegate void ConnectionExceptionEventHandler(ObjectStreamConnection connection, Exception exception) + where TRead : class + where TWrite : class; +} diff --git a/Oxide.CSharp/ObjectStream/Threading/Worker.cs b/Oxide.CSharp/ObjectStream/Threading/Worker.cs new file mode 100644 index 0000000..f052d87 --- /dev/null +++ b/Oxide.CSharp/ObjectStream/Threading/Worker.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading; + +namespace ObjectStream.Threading +{ + internal class Worker + { + public event WorkerExceptionEventHandler Error; + + public Worker() + { + } + + public void DoWork(Action action) + { + new Thread(DoWorkImpl) { IsBackground = true }.Start(action); + //new Task(DoWorkImpl, action, CancellationToken.None, TaskCreationOptions.LongRunning).Start(); + } + + private void DoWorkImpl(object oAction) + { + var action = (Action)oAction; + try + { + action(); + } + catch (Exception e) + { + Callback(() => Fail(e)); + } + } + + private void Fail(Exception exception) + { + if (Error != null) + Error(exception); + } + + private void Callback(Action action) + { + new Thread(new ThreadStart(action)) { IsBackground = true }.Start(); + //Task.Factory.StartNew(action, CancellationToken.None, TaskCreationOptions.None, _callbackThread); + } + } + + internal delegate void WorkerSucceededEventHandler(); + + internal delegate void WorkerExceptionEventHandler(Exception exception); +} diff --git a/Oxide.CSharp/Oxide.CSharp.csproj b/Oxide.CSharp/Oxide.CSharp.csproj new file mode 100644 index 0000000..67b2f66 --- /dev/null +++ b/Oxide.CSharp/Oxide.CSharp.csproj @@ -0,0 +1,36 @@ + + + + + 2.0.0.0 + Oxide.CSharp + Oxide and Contributors + C#/CSharp (.cs) plugin support for the Oxide modding framework + https://github.com/OxideMod/Oxide.CSharp + https://github.com/OxideMod/Oxide.CSharp/blob/develop/LICENSE.md + https://github.com/OxideMod/Oxide.CSharp + https://avatars1.githubusercontent.com/u/10712027?s=64 + Copyright (c) 2014-$([System.DateTime]::Now.Year) $(Authors) + api framework gaming modding plugins csharp c# + net461;net45;net40;net35 + True + + C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v3.5\Profile\Client + /Library/Frameworks/Mono.framework/Versions/Current/lib/mono/2.0-api + + + + + + + + + + Oxide + + + + diff --git a/Oxide.CSharp/PluginCompiler.cs b/Oxide.CSharp/PluginCompiler.cs new file mode 100644 index 0000000..c8bb1a8 --- /dev/null +++ b/Oxide.CSharp/PluginCompiler.cs @@ -0,0 +1,498 @@ +extern alias Oxide; + +using ObjectStream; +using ObjectStream.Data; +using Oxide.Core; +using Oxide::Mono.Unix.Native; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Security.Cryptography; +using System.Text.RegularExpressions; +using System.Threading; + +namespace Oxide.Plugins +{ + public class PluginCompiler + { + public static bool AutoShutdown = true; + public static bool TraceRan; + public static string FileName = "basic.exe"; + public static string BinaryPath; + public static string CompilerVersion; + + private static int downloadRetries = 0; + + public static void CheckCompilerBinary() + { + BinaryPath = null; + var rootDirectory = Interface.Oxide.RootDirectory; + var binaryPath = Path.Combine(rootDirectory, FileName); + if (File.Exists(binaryPath)) + { + BinaryPath = binaryPath; + return; + } + + switch (Environment.OSVersion.Platform) + { + case PlatformID.Win32NT: + case PlatformID.Win32S: + case PlatformID.Win32Windows: + FileName = "CSharpCompiler.exe"; + binaryPath = Path.Combine(rootDirectory, FileName); + UpdateCheck(); // TODO: Only check once on server startup + break; + + case PlatformID.Unix: + case PlatformID.MacOSX: + FileName = $"CSharpCompiler.{(IntPtr.Size != 8 ? "x86" : "x86_x64")}"; + binaryPath = Path.Combine(rootDirectory, FileName); + UpdateCheck(); // TODO: Only check once on server startup + try + { + if (Syscall.access(binaryPath, AccessModes.X_OK) == 0) break; + } + catch (Exception ex) + { + Interface.Oxide.LogError($"Unable to check {FileName} for executable permission"); + Interface.Oxide.LogError(ex.Message); + Interface.Oxide.LogError(ex.StackTrace); + } + try + { + Syscall.chmod(binaryPath, FilePermissions.S_IRWXU); + } + catch (Exception ex) + { + Interface.Oxide.LogError($"Could not set {FileName} as executable, please set manually"); + Interface.Oxide.LogError(ex.Message); + Interface.Oxide.LogError(ex.StackTrace); + } + break; + } + BinaryPath = binaryPath; + } + + private void DependencyTrace() + { + if (TraceRan || Environment.OSVersion.Platform != PlatformID.Unix) return; + + try + { + Interface.Oxide.LogWarning($"Running dependency trace for {FileName}"); + var trace = new Process + { + StartInfo = + { + WorkingDirectory = Interface.Oxide.RootDirectory, + FileName = "/bin/bash", + Arguments = $"-c \"LD_TRACE_LOADED_OBJECTS=1 {BinaryPath}\"", + CreateNoWindow = true, + UseShellExecute = false, + RedirectStandardInput = true, + RedirectStandardOutput = true + }, + EnableRaisingEvents = true + }; + trace.StartInfo.EnvironmentVariables["LD_LIBRARY_PATH"] = $"{Path.Combine(Interface.Oxide.ExtensionDirectory, IntPtr.Size == 8 ? "x64" : "x86")}"; + trace.ErrorDataReceived += (s, e) => Interface.Oxide.LogError(e.Data.TrimStart()); + trace.OutputDataReceived += (s, e) => Interface.Oxide.LogError(e.Data.TrimStart()); + trace.Start(); + trace.BeginOutputReadLine(); + trace.BeginErrorReadLine(); + trace.WaitForExit(); + } + catch (Exception) + { + //Interface.Oxide.LogError($"Couldn't run dependency trace"); // TODO: Fix this triggering sometimes + //Interface.Oxide.LogError(ex.Message); + } + TraceRan = true; + } + + private static void DownloadCompiler(WebResponse response, string remoteHash) + { + try + { + Interface.Oxide.LogInfo($"Downloading {FileName} for .cs (C#) plugin compilation"); + + var stream = response.GetResponseStream(); + var fs = new FileStream(FileName, FileMode.Create, FileAccess.Write, FileShare.None); + var bufferSize = 10000; + var buffer = new byte[bufferSize]; + + while (true) + { + var result = stream.Read(buffer, 0, bufferSize); + if (result == -1 || result == 0) break; + fs.Write(buffer, 0, result); + } + fs.Flush(); + fs.Close(); + stream.Close(); + response.Close(); + + if (downloadRetries >= 3) + { + Interface.Oxide.LogInfo($"Couldn't download {FileName}! Please download manually from: https://github.com/OxideMod/CSharpCompiler/releases/download/latest/{FileName}"); + return; + } + + var localHash = File.Exists(BinaryPath) ? GetHash(BinaryPath, Algorithms.MD5) : "0"; + if (remoteHash != localHash) + { + Interface.Oxide.LogInfo($"Local hash did not match remote hash for {FileName}, attempting download again"); + CheckCompilerBinary(); + downloadRetries++; + return; + } + + Interface.Oxide.LogInfo($"Download of {FileName} completed successfully"); + } + catch (Exception ex) + { + Interface.Oxide.LogError($"Couldn't download {FileName}! Please download manually from: https://github.com/OxideMod/CSharpCompiler/releases/download/latest/{FileName}"); + Interface.Oxide.LogError(ex.Message); + } + } + + private static void UpdateCheck() + { + try + { + var filePath = Path.Combine(Interface.Oxide.RootDirectory, FileName); + var request = (HttpWebRequest)WebRequest.Create($"https://github.com/OxideMod/CSharpCompiler/releases/download/latest/{FileName}"); + var response = (HttpWebResponse)request.GetResponse(); + var statusCode = (int)response.StatusCode; + if (statusCode != 200) Interface.Oxide.LogWarning($"Status code from download location was not okay (code {statusCode})"); + var remoteHash = response.Headers[HttpResponseHeader.ETag].Trim('"'); + var localHash = File.Exists(filePath) ? GetHash(filePath, Algorithms.MD5) : "0"; + Interface.Oxide.LogInfo($"Latest compiler MD5: {remoteHash}"); + Interface.Oxide.LogInfo($"Local compiler MD5: {localHash}"); + if (remoteHash != localHash) + { + Interface.Oxide.LogInfo("Compiler hashes did not match, downloading latest"); + DownloadCompiler(response, remoteHash); + } + } + catch (Exception ex) + { + Interface.Oxide.LogError($"Couldn't check for update to {FileName}"); + Interface.Oxide.LogError(ex.Message); + } + } + + private static void SetCompilerVersion() + { + var version = FileVersionInfo.GetVersionInfo(BinaryPath); + CompilerVersion = $"{version.FileMajorPart}.{version.FileMinorPart}.{version.FileBuildPart}.{version.FilePrivatePart}"; + RemoteLogger.SetTag("compiler version", CompilerVersion); + } + + private Process process; + private readonly Regex fileErrorRegex = new Regex(@"([\w\.]+)\(\d+\,\d+\+?\): error|error \w+: Source file `[\\\./]*([\w\.]+)", RegexOptions.Compiled); + private ObjectStreamClient client; + private Hash compilations; + private Queue messageQueue; + private volatile int lastId; + private volatile bool ready; + private Core.Libraries.Timer.TimerInstance idleTimer; + + public PluginCompiler() + { + compilations = new Hash(); + messageQueue = new Queue(); + } + + internal void Compile(CompilablePlugin[] plugins, Action callback) + { + var id = lastId++; + var compilation = new Compilation(id, callback, plugins); + compilations[id] = compilation; + compilation.Prepare(() => EnqueueCompilation(compilation)); + } + + public void Shutdown() + { + ready = false; + var endedProcess = process; + if (endedProcess != null) endedProcess.Exited -= OnProcessExited; + process = null; + if (client == null) return; + + client.Message -= OnMessage; + client.Error -= OnError; + client.PushMessage(new CompilerMessage { Type = CompilerMessageType.Exit }); + client.Stop(); + client = null; + if (endedProcess == null) return; + + ThreadPool.QueueUserWorkItem(_ => + { + Thread.Sleep(5000); + // Calling Close can block up to 60 seconds on certain machines + if (!endedProcess.HasExited) endedProcess.Close(); + }); + } + + private void EnqueueCompilation(Compilation compilation) + { + if (compilation.plugins.Count < 1) + { + //Interface.Oxide.LogDebug("EnqueueCompilation called for an empty compilation"); + return; + } + + if (!CheckCompiler()) + { + OnCompilerFailed($"compiler v{CompilerVersion} couldn't be started"); + return; + } + + compilation.Started(); + //Interface.Oxide.LogDebug("Compiling with references: {0}", compilation.references.Keys.ToSentence()); + var sourceFiles = compilation.plugins.SelectMany(plugin => plugin.IncludePaths).Distinct().Select(path => new CompilerFile(path)).ToList(); + sourceFiles.AddRange(compilation.plugins.Select(plugin => new CompilerFile($"{plugin.ScriptName}.cs", plugin.ScriptSource))); + //Interface.Oxide.LogDebug("Compiling files: {0}", sourceFiles.Select(f => f.Name).ToSentence()); + var data = new CompilerData + { + OutputFile = compilation.name, + SourceFiles = sourceFiles.ToArray(), + ReferenceFiles = compilation.references.Values.ToArray() + }; + var message = new CompilerMessage { Id = compilation.id, Data = data, Type = CompilerMessageType.Compile }; + if (ready) + client.PushMessage(message); + else + messageQueue.Enqueue(message); + } + + private void OnMessage(ObjectStreamConnection connection, CompilerMessage message) + { + if (message == null) + { + Interface.Oxide.NextTick(() => + { + OnCompilerFailed($"compiler v{CompilerVersion} disconnected"); + DependencyTrace(); + Shutdown(); + }); + return; + } + + switch (message.Type) + { + case CompilerMessageType.Assembly: + var compilation = compilations[message.Id]; + if (compilation == null) + { + Interface.Oxide.LogWarning("Compiler compiled an unknown assembly"); // TODO: Any way to clarify this? + return; + } + compilation.endedAt = Interface.Oxide.Now; + var stdOutput = (string)message.ExtraData; + if (stdOutput != null) + { + foreach (var line in stdOutput.Split('\r', '\n')) + { + var match = fileErrorRegex.Match(line.Trim()); + for (var i = 1; i < match.Groups.Count; i++) + { + var value = match.Groups[i].Value; + if (value.Trim() == string.Empty) continue; + var fileName = value.Basename(); + var scriptName = fileName.Substring(0, fileName.Length - 3); + var compilablePlugin = compilation.plugins.SingleOrDefault(pl => pl.ScriptName == scriptName); + if (compilablePlugin == null) + { + Interface.Oxide.LogError($"Unable to resolve script error to plugin: {line}"); + continue; + } + var missingRequirements = compilablePlugin.Requires.Where(name => !compilation.IncludesRequiredPlugin(name)); + if (missingRequirements.Any()) + compilablePlugin.CompilerErrors = $"Missing dependencies: {missingRequirements.ToSentence()}"; + else + compilablePlugin.CompilerErrors = line.Trim().Replace(Interface.Oxide.PluginDirectory + Path.DirectorySeparatorChar, string.Empty); + } + } + } + compilation.Completed((byte[])message.Data); + compilations.Remove(message.Id); + idleTimer?.Destroy(); + if (AutoShutdown) + { + Interface.Oxide.NextTick(() => + { + idleTimer?.Destroy(); + if (AutoShutdown) idleTimer = Interface.Oxide.GetLibrary().Once(60, Shutdown); + }); + } + break; + + case CompilerMessageType.Error: + Interface.Oxide.LogError("Compilation error: {0}", message.Data); + compilations[message.Id].Completed(); + compilations.Remove(message.Id); + idleTimer?.Destroy(); + if (AutoShutdown) + { + Interface.Oxide.NextTick(() => + { + idleTimer?.Destroy(); + idleTimer = Interface.Oxide.GetLibrary().Once(60, Shutdown); + }); + } + break; + + case CompilerMessageType.Ready: + connection.PushMessage(message); + if (!ready) + { + ready = true; + while (messageQueue.Count > 0) connection.PushMessage(messageQueue.Dequeue()); + } + break; + } + } + + private static void OnError(Exception exception) => Interface.Oxide.LogException("Compilation error: ", exception); + + private bool CheckCompiler() + { + CheckCompilerBinary(); + idleTimer?.Destroy(); + + if (BinaryPath == null) return false; + if (process != null && process.Handle != IntPtr.Zero && !process.HasExited) return true; + + SetCompilerVersion(); + PurgeOldLogs(); + Shutdown(); + + var args = new[] { "/service", "/logPath:" + EscapePath(Interface.Oxide.LogDirectory) }; + try + { + process = new Process + { + StartInfo = + { + FileName = BinaryPath, + Arguments = string.Join(" ", args), + CreateNoWindow = true, + UseShellExecute = false, + RedirectStandardInput = true, + RedirectStandardOutput = true + }, + EnableRaisingEvents = true + }; + switch (Environment.OSVersion.Platform) + { + case PlatformID.Win32S: + case PlatformID.Win32Windows: + case PlatformID.Win32NT: + process.StartInfo.EnvironmentVariables["PATH"] = $"{Path.Combine(Interface.Oxide.ExtensionDirectory, "x86")}"; + break; + + case PlatformID.Unix: + case PlatformID.MacOSX: + process.StartInfo.EnvironmentVariables["LD_LIBRARY_PATH"] = $"{Path.Combine(Interface.Oxide.ExtensionDirectory, IntPtr.Size == 8 ? "x64" : "x86")}"; + break; + } + process.Exited += OnProcessExited; + process.Start(); + } + catch (Exception ex) + { + process?.Dispose(); + process = null; + Interface.Oxide.LogException($"Exception while starting compiler v{CompilerVersion}: ", ex); + if (BinaryPath.Contains("'")) Interface.Oxide.LogWarning("Server directory path contains an apostrophe, compiler will not work until path is renamed"); + else if (Environment.OSVersion.Platform == PlatformID.Unix) Interface.Oxide.LogWarning("Compiler may not be set as executable; chmod +x or 0744/0755 required"); + if (ex.GetBaseException() != ex) Interface.Oxide.LogException("BaseException: ", ex.GetBaseException()); + var win32 = ex as Win32Exception; + if (win32 != null) Interface.Oxide.LogError("Win32 NativeErrorCode: {0} ErrorCode: {1} HelpLink: {2}", win32.NativeErrorCode, win32.ErrorCode, win32.HelpLink); + } + + if (process == null) return false; + + client = new ObjectStreamClient(process.StandardOutput.BaseStream, process.StandardInput.BaseStream); + client.Message += OnMessage; + client.Error += OnError; + client.Start(); + return true; + } + + private void OnProcessExited(object sender, EventArgs eventArgs) + { + Interface.Oxide.NextTick(() => + { + OnCompilerFailed($"compiler v{CompilerVersion} was closed unexpectedly"); + if (Environment.OSVersion.Platform == PlatformID.Unix) + Interface.Oxide.LogWarning("User running server may not have the proper permissions or install is missing files"); + else + Interface.Oxide.LogWarning("Compiler may have been closed by interference from security software"); + Shutdown(); + }); + } + + private void OnCompilerFailed(string reason) + { + foreach (var compilation in compilations.Values) + { + foreach (var plugin in compilation.plugins) plugin.CompilerErrors = reason; + compilation.Completed(); + } + compilations.Clear(); + } + + private static void PurgeOldLogs() + { + try + { + var filePaths = Directory.GetFiles(Interface.Oxide.LogDirectory, "*.txt").Where(f => + { + var fileName = Path.GetFileName(f); + return fileName != null && fileName.StartsWith("compiler_"); + }); + foreach (var filePath in filePaths) File.Delete(filePath); + } + catch (Exception) + { + // Ignored + } + } + + private static string EscapePath(string path) + { + if (string.IsNullOrEmpty(path)) return "\"\""; + + path = Regex.Replace(path, @"(\\*)" + "\"", @"$1\$0"); + path = Regex.Replace(path, @"^(.*\s.*?)(\\*)$", "\"$1$2$2\""); + return path; + } + + private static class Algorithms + { + public static readonly HashAlgorithm MD5 = new MD5CryptoServiceProvider(); + public static readonly HashAlgorithm SHA1 = new SHA1Managed(); + public static readonly HashAlgorithm SHA256 = new SHA256Managed(); + public static readonly HashAlgorithm SHA384 = new SHA384Managed(); + public static readonly HashAlgorithm SHA512 = new SHA512Managed(); + public static readonly HashAlgorithm RIPEMD160 = new RIPEMD160Managed(); + } + + private static string GetHash(string filePath, HashAlgorithm algorithm) + { + using (var stream = new BufferedStream(File.OpenRead(filePath), 100000)) + { + var hash = algorithm.ComputeHash(stream); + return BitConverter.ToString(hash).Replace("-", string.Empty).ToLower(); + } + } + } +} diff --git a/Oxide.CSharp/PluginTimers.cs b/Oxide.CSharp/PluginTimers.cs new file mode 100644 index 0000000..52dcf44 --- /dev/null +++ b/Oxide.CSharp/PluginTimers.cs @@ -0,0 +1,120 @@ +using Oxide.Core; +using Oxide.Core.Plugins; +using System; + +namespace Oxide.Plugins +{ + public class Timer + { + private Core.Libraries.Timer.TimerInstance instance; + + public Timer(Core.Libraries.Timer.TimerInstance instance) + { + this.instance = instance; + } + + /// + /// Gets the number of repetitions left on this timer + /// + public int Repetitions => instance.Repetitions; + + /// + /// Gets the delay between each repetition + /// + public float Delay => instance.Delay; + + /// + /// Gets the callback delegate + /// + public Action Callback => instance.Callback; + + /// + /// Gets if this timer has been destroyed + /// + public bool Destroyed => instance.Destroyed; + + /// + /// Gets the plugin to which this timer belongs, if any + /// + public Plugin Owner => instance.Owner; + + /// + /// Resets the timer optionally changing the delay setting a number of repetitions + /// + /// The new delay between repetitions + /// Number of repetitions before being destroyed + public void Reset(float delay = -1, int repetitions = 1) => instance.Reset(delay, repetitions); + + /// + /// Destroys this timer + /// + public void Destroy() => instance.Destroy(); + + /// + /// Destroys this timer and returns the instance to the pool + /// + public void DestroyToPool() => instance.DestroyToPool(); + } + + public class PluginTimers + { + private Core.Libraries.Timer timer = Interface.Oxide.GetLibrary("Timer"); + private Plugin plugin; + + public PluginTimers(Plugin plugin) + { + this.plugin = plugin; + } + + /// + /// Creates a timer which fires once after the specified delay + /// + /// + /// + public Timer Once(float seconds, Action callback) + { + return new Timer(timer.Once(seconds, callback, plugin)); + } + + /// + /// Creates a timer which fires once after the specified delay + /// + /// + /// + public Timer In(float seconds, Action callback) + { + return new Timer(timer.Once(seconds, callback, plugin)); + } + + /// + /// Creates a timer which continuously fires at the specified interval + /// + /// + /// + public Timer Every(float interval, Action callback) + { + return new Timer(timer.Repeat(interval, -1, callback, plugin)); + } + + /// + /// Creates a timer which fires a set number of times at the specified interval + /// + /// + /// + /// + public Timer Repeat(float interval, int repeats, Action callback) + { + return new Timer(timer.Repeat(interval, repeats, callback, plugin)); + } + + /// + /// Destroys a timer, returns the instance to the pool and sets the variable to null + /// + /// + public void Destroy(ref Timer timer) + { + timer?.DestroyToPool(); + timer = null; + } + } +} diff --git a/Oxide.CSharp/Utility.cs b/Oxide.CSharp/Utility.cs new file mode 100644 index 0000000..80ce3de --- /dev/null +++ b/Oxide.CSharp/Utility.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Oxide.Plugins +{ + /// + /// A dictionary which returns null for non-existant keys and removes keys when setting an index to null. + /// + /// + /// + public class Hash : IDictionary + { + private readonly IDictionary dictionary; + + public Hash() + { + dictionary = new Dictionary(); + } + + public TValue this[TKey key] + { + get + { + TValue value; + if (TryGetValue(key, out value)) + return value; + if (typeof(TValue).IsValueType) + return (TValue)Activator.CreateInstance(typeof(TValue)); + return default(TValue); + } + + set + { + if (value == null) + dictionary.Remove(key); + else + dictionary[key] = value; + } + } + + public ICollection Keys => dictionary.Keys; + public ICollection Values => dictionary.Values; + public int Count => dictionary.Count; + public bool IsReadOnly => dictionary.IsReadOnly; + + public IEnumerator> GetEnumerator() => dictionary.GetEnumerator(); + public bool ContainsKey(TKey key) => dictionary.ContainsKey(key); + public bool Contains(KeyValuePair item) => dictionary.Contains(item); + public void CopyTo(KeyValuePair[] array, int index) => dictionary.CopyTo(array, index); + public bool TryGetValue(TKey key, out TValue value) => dictionary.TryGetValue(key, out value); + public void Add(TKey key, TValue value) => dictionary.Add(key, value); + public void Add(KeyValuePair item) => dictionary.Add(item); + public bool Remove(TKey key) => dictionary.Remove(key); + public bool Remove(KeyValuePair item) => dictionary.Remove(item); + public void Clear() => dictionary.Clear(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +}