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