From 14585bb1b58112f9275a5cd10950b2c4e19612f4 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Tue, 1 Oct 2024 13:16:42 -0400 Subject: [PATCH 01/27] Code modification to use both files docs.json and docs_core.json docs_core.json contain the oxide core hook documentation --- util/hooks.ts | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/util/hooks.ts b/util/hooks.ts index ea207e7..5d3bf2c 100644 --- a/util/hooks.ts +++ b/util/hooks.ts @@ -2,14 +2,17 @@ import { readFileSync } from "fs"; import IDocs from "../entities/hooks/docs"; import IHook from "../entities/hooks/hook"; -export function getHookJson() { - const hookData = readFileSync("docs.json").toString(); +// Todo: improve code to merge both JSON files docs.json and docs_core.json +// this quick implementation is for test. +// docs_core.json contain hook info for oxide.code, oxide.csharp and oxide.rust +export function getHookJson(filename: string) { + const hookData = readFileSync(filename).toString(); const hooks = JSON.parse(hookData) as IDocs; return hooks.Hooks.filter(hook => hook.Category !== "_Patches" && !hook.HookName.includes("[")); } export function getGroupedHooks() { - const hooksJson = getHookJson(); + const hooksJson = getHookJson("docs.json"); var out = {} as { [key: string]: { [key: string]: IHook[] } }; @@ -24,6 +27,19 @@ export function getGroupedHooks() { out[hook.Category][hook.HookName].push(hook); }); + + const hooksCoreJson = getHookJson("docs_core.json"); + hooksCoreJson.forEach((hook) => { + if (!out[hook.Category]) { + out[hook.Category] = {}; + } + + if (!out[hook.Category][hook.HookName]) { + out[hook.Category][hook.HookName] = []; + } + + out[hook.Category][hook.HookName].push(hook); + }); // Sort categories, hooks and hooks by TargetType and MethodData.MethodName using tolocaleCompare Object.keys(out).forEach((category) => { From 04907ef78b995506ae1267e1f58b8e9ef2dbfeea Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Tue, 1 Oct 2024 13:19:58 -0400 Subject: [PATCH 02/27] Documention of the oxide core hooks from project Oxide.core, Oxide.csharp and Oxide.Rust --- docs_core.json | 1243 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1243 insertions(+) create mode 100644 docs_core.json diff --git a/docs_core.json b/docs_core.json new file mode 100644 index 0000000..a0b0f9e --- /dev/null +++ b/docs_core.json @@ -0,0 +1,1243 @@ +{ + "Hooks": [ + { + "Type": 0, + "Name": "OnFrame", + "HookName": "OnFrame", + "HookParameters": { + "args": "object[]" + }, + "ReturnBehavior": 0, + "TargetType": "CSharpExtension", + "Category": "Oxide.CSharp", + "HookDescription": "Called each frame", + "MethodData": { + "MethodName": "OnFrame", + "ReturnType": "void", + "Arguments": { + "delta": "float" + } + }, + "CodeAfterInjection": "private void OnFrame(float delta)\r\n{\r\n\tobject[] args = new object[] { delta };\r\n\tforeach (System.Collections.Generic.KeyValuePair kv in loader.LoadedPlugins)\r\n\t{\r\n\t\tCSharpPlugin plugin = kv.Value as CSharpPlugin;\r\n\t\tif (plugin != null && plugin.HookedOnFrame)\r\n\t\t{\r\n\t\t\tplugin.CallHook(\"OnFrame\", args);\r\n\t\t}\r\n\t}\r\n}" + }, + { + "Type": 0, + "Name": "Loaded", + "HookName": "Loaded", + "HookParameters": {}, + "ReturnBehavior": 0, + "TargetType": "CSharpPlugin", + "Category": "Oxide.CSharp", + "HookDescription": "Called when a plugin has finished loading\r\nOther plugins may or may not be present, dependant on load order", + "MethodData": { + "MethodName": "HandleAddedToManager", + "ReturnType": "void", + "Arguments": {} + }, + "CodeAfterInjection": "public override void HandleAddedToManager(PluginManager manager)\r\n{\r\n\tbase.HandleAddedToManager(manager);\r\n\r\n\tif (Filename != null)\r\n\t{\r\n\t\tWatcher.AddMapping(Name);\r\n\t}\r\n\r\n\tManager.OnPluginAdded += OnPluginLoaded;\r\n\tManager.OnPluginRemoved += OnPluginUnloaded;\r\n\r\n\tforeach (var member in pluginReferenceMembers)\r\n\t{\r\n\t\tif (member.Value.MemberType == MemberTypes.Property)\r\n\t\t{\r\n\t\t\t((PropertyInfo)member.Value).SetValue(this, manager.GetPlugin(member.Key), null);\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\t((FieldInfo)member.Value).SetValue(this, manager.GetPlugin(member.Key));\r\n\t\t}\r\n\t}\r\n\ttry\r\n\t{\r\n\t\tOnCallHook(\"Loaded\", null);\r\n\t}\r\n\tcatch (Exception ex)\r\n\t{\r\n\t\tInterface.Oxide.LogException($\"Failed to initialize plugin '{Name} v{Version}'\", ex);\r\n\t\tLoader.PluginErrors[Name] = ex.Message;\r\n\t}\r\n}" + }, + { + "Type": 0, + "Name": "Unload", + "HookName": "Unload", + "HookParameters": {}, + "ReturnBehavior": 0, + "TargetType": "CSharpPlugin", + "Category": "Oxide.CSharp", + "HookDescription": "Called when a plugin is being unloaded", + "MethodData": { + "MethodName": "HandleRemovedFromManager", + "ReturnType": "void", + "Arguments": { + "manager": "PluginManager" + } + }, + "CodeAfterInjection": "public override void HandleRemovedFromManager(PluginManager manager)\r\n{\r\n\tif (IsLoaded)\r\n\t{\r\n\t\tCallHook(\"Unload\", null);\r\n\t}\r\n\r\n\tWatcher.RemoveMapping(Name);\r\n\r\n\tManager.OnPluginAdded -= OnPluginLoaded;\r\n\tManager.OnPluginRemoved -= OnPluginUnloaded;\r\n\r\n\tforeach (var member in pluginReferenceMembers)\r\n\t{\r\n\t\tif (member.Value.MemberType == MemberTypes.Property)\r\n\t\t{\r\n\t\t\t((PropertyInfo)member.Value).SetValue(this, null, null);\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\t((FieldInfo)member.Value).SetValue(this, null);\r\n\t\t}\r\n\t}\r\n\r\n\tbase.HandleRemovedFromManager(manager);\r\n}" + }, + { + "Type": 0, + "Name": "OnPermissionRegistered", + "HookName": "OnPermissionRegistered", + "HookParameters": { + "name": "string", + "owner": "Plugin" + }, + "ReturnBehavior": 0, + "TargetType": "Permission", + "Category": "Oxide.Core", + "HookDescription": "Called when a permission has been registered", + "MethodData": { + "MethodName": "RegisterPermission", + "ReturnType": "void", + "Arguments": { + "permission": "permission", + "owner": "Plugin" + } + }, + "CodeAfterInjection": "public void RegisterPermission(c permission, Plugin owner)\r\n{\r\n\tif (string.IsNullOrEmpty(permission))\r\n\t{\r\n\t\treturn;\r\n\t}\r\n\tif (PermissionExists(permission))\r\n\t{\r\n\t\tInterface.Oxide.LogWarning(\"Duplicate permission registered '{0}' (by plugin '{1}')\", permission, owner.Title);\r\n\t\treturn;\r\n\t}\r\n\tif (!registeredPermissions.TryGetValue(owner, out HashSet set))\r\n\t{\r\n\t\tset = new HashSet(StringComparer.OrdinalIgnoreCase);\r\n\t\tregisteredPermissions.Add(owner, set);\r\n\t\towner.OnRemovedFromManager.Add(owner_OnRemovedFromManager);\r\n\t}\r\n\tset.Add(permission);\r\n\tInterface.CallHook(\"OnPermissionRegistered\", permission, owner);\r\n\tif (!permission.StartsWith($\"{owner.Name}.\", StringComparison.OrdinalIgnoreCase) && !owner.IsCorePlugin)\r\n\t{\r\n\t\tInterface.Oxide.LogWarning(\"Missing plugin name prefix '{0}' for permission '{1}' (by plugin '{2}')\", owner.Name.ToLower(), permission, owner.Title);\r\n\t}\r\n}" + }, + { + "Type": 0, + "Name": "OnUserNameUpdated", + "HookName": "OnUserNameUpdated", + "HookParameters": { + "playerId": "string", + "oldName": "string", + "newName": "string" + }, + "ReturnBehavior": 0, + "TargetType": "Permission", + "Category": "Oxide.Core", + "HookDescription": "Called when a player's stored nickname has been changed", + "MethodData": { + "MethodName": "UpdateNickname", + "ReturnType": "void", + "Arguments": { + "playerId": "string", + "playerName": "string" + } + }, + "CodeAfterInjection": "public void UpdateNickname(string playerId, string playerName)\r\n{\r\n\tif (UserExists(playerId))\r\n\t{\r\n\t\tUserData userData = GetUserData(playerId);\r\n\t\tstring oldName = userData.LastSeenNickname;\r\n\t\tstring newName = playerName.Sanitize();\r\n\t\tuserData.LastSeenNickname = playerName.Sanitize();\r\n\r\n\t\tInterface.CallHook(\"OnUserNameUpdated\", playerId, oldName, newName);\r\n\t}\r\n}" + }, + { + "Type": 0, + "Name": "OnGroupCreated", + "HookName": "OnGroupCreated", + "HookParameters": { + "name": "string" + }, + "ReturnBehavior": 0, + "TargetType": "Permission", + "Category": "Oxide.Core", + "HookDescription": "Called when a group has been created successfully", + "MethodData": { + "MethodName": "CreateGroup", + "ReturnType": "bool", + "Arguments": { + "groupName": "string", + "groupTitle": "string", + "groupRank": "int" + } + }, + "CodeAfterInjection": "public bool CreateGroup(string groupName, string groupTitle, int groupRank)\r\n{\r\n\t// Check if it already exists\r\n\tif (GroupExists(groupName) || string.IsNullOrEmpty(groupName))\r\n\t{\r\n\t\treturn false;\r\n\t}\r\n\t// Create the data\r\n\tGroupData groupData = new GroupData { Title = groupTitle, Rank = groupRank };\r\n\t// Add the group\r\n\tgroupsData.Add(groupName, groupData);\r\n\tInterface.CallHook(\"OnGroupCreated\", groupName, groupTitle, groupRank);\r\n\treturn true;\r\n}" + }, + { + "Type": 0, + "Name": "OnGroupDeleted", + "HookName": "OnGroupDeleted", + "HookParameters": { + "name": "string" + }, + "ReturnBehavior": 0, + "TargetType": "Permission", + "Category": "Oxide.Core", + "HookDescription": "Called when a group has been deleted successfully", + "MethodData": { + "MethodName": "RemoveGroup", + "ReturnType": "bool", + "Arguments": { + "groupName": "string" + } + }, + "CodeAfterInjection": "public bool RemoveGroup(string groupName)\r\n{\r\n\t// Check if it even exists\r\n\tif (!GroupExists(groupName))\r\n\t{\r\n\t\treturn false;\r\n\t}\r\n\t// Remove the group\r\n\tbool removed = groupsData.Remove(groupName);\r\n\tif (removed)\r\n\t{\r\n\t\t// Set children to having no parent group\r\n\t\tforeach (GroupData child in groupsData.Values.Where(g => g.ParentGroup == groupName))\r\n\t\t{\r\n\t\t\tchild.ParentGroup = string.Empty;\r\n\t\t}\r\n\t}\r\n\t// Remove group from users\r\n\tbool changed = usersData.Values.Aggregate(false, (current, userData) => current | userData.Groups.Remove(groupName));\r\n\tif (changed)\r\n\t{\r\n\t\tSaveUsers();\r\n\t}\r\n\tif (removed)\r\n\t{\r\n\t\tInterface.CallHook(\"OnGroupDeleted\", groupName);\r\n\t}\r\n\treturn true;\r\n}" + }, + { + "Type": 0, + "Name": "OnGroupTitleSet", + "HookName": "OnGroupTitleSet", + "HookParameters": { + "name": "string", + "title": "string" + }, + "ReturnBehavior": 0, + "TargetType": "Permission", + "Category": "Oxide.Core", + "HookDescription": "Called when a group title has been updated", + "MethodData": { + "MethodName": "SetGroupTitle", + "ReturnType": "bool", + "Arguments": { + "groupName": "string", + "groupTitle": "string" + } + }, + "CodeAfterInjection": "public bool SetGroupTitle(string groupName, string groupTitle)\r\n{\r\n\tif (!GroupExists(groupName))\r\n\t{\r\n\t\treturn false;\r\n\t}\r\n\r\n\t// First, get the group data\r\n\tif (!groupsData.TryGetValue(groupName, out GroupData groupData))\r\n\t{\r\n\t\treturn false;\r\n\t}\r\n\r\n\t// Change the title\r\n\tif (groupData.Title == groupTitle)\r\n\t{\r\n\t\treturn true;\r\n\t}\r\n\r\n\tgroupData.Title = groupTitle;\r\n\r\n\tInterface.CallHook(\"OnGroupTitleSet\", groupName, groupTitle);\r\n\r\n\treturn true;\r\n}" + }, + { + "Type": 0, + "Name": "OnGroupRankSet", + "HookName": "OnGroupRankSet", + "HookParameters": { + "name": "string", + "title": "string" + }, + "ReturnBehavior": 0, + "TargetType": "Permission", + "Category": "Oxide.Core", + "HookDescription": "Called when a group title has been updated", + "MethodData": { + "MethodName": "SetGroupRank", + "ReturnType": "bool", + "Arguments": { + "groupName": "string", + "groupRank": "int" + } + }, + "CodeAfterInjection": "public bool SetGroupRank(string groupName, int groupRank)\r\n{\r\n\tif (!GroupExists(groupName))\r\n\t{\r\n\t\treturn false;\r\n\t}\r\n\r\n\t// First, get the group data\r\n\tif (!groupsData.TryGetValue(groupName, out GroupData groupData))\r\n\t{\r\n\t\treturn false;\r\n\t}\r\n\r\n\t// Change the rank\r\n\tif (groupData.Rank == groupRank)\r\n\t{\r\n\t\treturn true;\r\n\t}\r\n\r\n\tgroupData.Rank = groupRank;\r\n\r\n\tInterface.CallHook(\"OnGroupRankSet\", groupName, groupRank);\r\n\r\n\treturn true;\r\n}" + }, + { + "Type": 0, + "Name": "OnGroupParentSet", + "HookName": "OnGroupParentSet", + "HookParameters": { + "name": "string", + "parent": "string" + }, + "ReturnBehavior": 0, + "TargetType": "Permission", + "Category": "Oxide.Core", + "HookDescription": "Called when a group parent has been updated", + "MethodData": { + "MethodName": "SetGroupParent", + "ReturnType": "bool", + "Arguments": { + "groupName": "string", + "parentGroupName": "string" + } + }, + "CodeAfterInjection": "public bool SetGroupParent(string groupName, string parentGroupName)\r\n{\r\n\tif (!GroupExists(groupName))\r\n\t{\r\n\t\treturn false;\r\n\t}\r\n\r\n\t// First, get the group data\r\n\tif (!groupsData.TryGetValue(groupName, out GroupData groupData))\r\n\t{\r\n\t\treturn false;\r\n\t}\r\n\r\n\tif (string.IsNullOrEmpty(parentGroupName))\r\n\t{\r\n\t\tgroupData.ParentGroup = null;\r\n\t\treturn true;\r\n\t}\r\n\r\n\tif (!GroupExists(parentGroupName) || groupName.Equals(parentGroupName))\r\n\t{\r\n\t\treturn false;\r\n\t}\r\n\r\n\tif (!string.IsNullOrEmpty(groupData.ParentGroup) && groupData.ParentGroup.Equals(parentGroupName))\r\n\t{\r\n\t\treturn true;\r\n\t}\r\n\r\n\tif (HasCircularParent(groupName, parentGroupName))\r\n\t{\r\n\t\treturn false;\r\n\t}\r\n\r\n\t// Change the parent group\r\n\tgroupData.ParentGroup = parentGroupName;\r\n\r\n\tInterface.CallHook(\"OnGroupParentSet\", groupName, parentGroupName);\r\n\r\n\treturn true;\r\n}" + }, + { + "Type": 0, + "Name": "OnPluginLoaded", + "HookName": "OnPluginLoaded", + "HookParameters": { + "plugin": "Plugin" + }, + "ReturnBehavior": 0, + "TargetType": "OxideMod", + "Category": "Oxide.Core", + "HookDescription": "Called when any plugin has been loaded. Not to be confused with Loaded", + "MethodData": { + "MethodName": "PluginLoaded", + "ReturnType": "bool", + "Arguments": { + "plugin": "Plugin" + } + }, + "CodeAfterInjection": "public bool PluginLoaded(Plugin plugin)\r\n{\r\n\tplugin.OnError += plugin_OnError;\r\n\ttry\r\n\t{\r\n\t\tplugin.Loader?.PluginErrors.Remove(plugin.Name);\r\n\t\tRootPluginManager.AddPlugin(plugin);\r\n\t\tif (plugin.Loader != null)\r\n\t\t{\r\n\t\t\tif (plugin.Loader.PluginErrors.ContainsKey(plugin.Name))\r\n\t\t\t{\r\n\t\t\t\tUnloadPlugin(plugin.Name);\r\n\t\t\t\treturn false;\r\n\t\t\t}\r\n\t\t}\r\n\t\tplugin.IsLoaded = true;\r\n\t\tCallHook(\"OnPluginLoaded\", plugin);\r\n\t\tLogInfo(\"Loaded plugin {0} v{1} by {2}\", plugin.Title, plugin.Version, plugin.Author);\r\n\t\treturn true;\r\n\t}\r\n\tcatch (Exception ex)\r\n\t{\r\n\t\tif (plugin.Loader != null)\r\n\t\t{\r\n\t\t\tplugin.Loader.PluginErrors[plugin.Name] = ex.Message;\r\n\t\t}\r\n\r\n\t\tLogException($\"Could not initialize plugin '{plugin.Name} v{plugin.Version}'\", ex);\r\n\t\treturn false;\r\n\t}\r\n}" + }, + { + "Type": 0, + "Name": "OnPluginUnloaded", + "HookName": "OnPluginUnloaded", + "HookParameters": { + "plugin": "Plugin" + }, + "ReturnBehavior": 0, + "TargetType": "OxideMod", + "Category": "Oxide.Core", + "HookDescription": "Called when any plugin has been unloaded. Not to be confused with Unload", + "MethodData": { + "MethodName": " UnloadPlugin()", + "ReturnType": "bool", + "Arguments": { + "name": "string" + } + }, + "CodeAfterInjection": "public bool UnloadPlugin(string name)\r\n{\r\n\t// Get the plugin\r\n\tPlugin plugin = RootPluginManager.GetPlugin(name);\r\n\tif (plugin == null || (plugin.IsCorePlugin && !IsShuttingDown))\r\n\t{\r\n\t\treturn false;\r\n\t}\r\n\r\n\t// Let the plugin loader know that this plugin is being unloaded\r\n\tPluginLoader loader = extensionManager.GetPluginLoaders().SingleOrDefault(l => l.LoadedPlugins.ContainsKey(name));\r\n\tloader?.Unloading(plugin);\r\n\r\n\t// Unload it\r\n\tRootPluginManager.RemovePlugin(plugin);\r\n\r\n\t// Let other plugins know that this plugin has been unloaded\r\n\tif (plugin.IsLoaded)\r\n\t{\r\n\t\tCallHook(\"OnPluginUnloaded\", plugin);\r\n\t}\r\n\r\n\tplugin.IsLoaded = false;\r\n\r\n\tLogInfo(\"Unloaded plugin {0} v{1} by {2}\", plugin.Title, plugin.Version, plugin.Author);\r\n\treturn true;\r\n}" + }, + { + "Type": 0, + "Name": "Init", + "HookName": " Init", + "HookParameters": {}, + "ReturnBehavior": 0, + "TargetType": "CSPlugin", + "Category": "Oxide.Core", + "HookDescription": "Called when a plugin is being initialized.\r\n Other plugins may or may not be present, dependant on load order", + "MethodData": { + "MethodName": "HandleAddedToManager", + "ReturnType": "void", + "Arguments": { + "manager": "PluginManager" + } + }, + "CodeAfterInjection": "public override void HandleAddedToManager(PluginManager manager)\r\n{\r\n\t// Let base work\r\n\tbase.HandleAddedToManager(manager);\r\n\r\n\t// Subscribe us\r\n\tforeach (string hookname in Hooks.Keys)\r\n\t{\r\n\t\tSubscribe(hookname);\r\n\t}\r\n\r\n\ttry\r\n\t{\r\n\t\t// Let the plugin know that it is loading\r\n\t\tOnCallHook(\"Init\", null);\r\n\t}\r\n\tcatch (Exception ex)\r\n\t{\r\n\t\tInterface.Oxide.LogException($\"Failed to initialize plugin '{Name} v{Version}'\", ex);\r\n\t\tif (Loader != null)\r\n\t\t{\r\n\t\t\tLoader.PluginErrors[Name] = ex.Message;\r\n\t\t}\r\n\t}\r\n\t//---" + }, + { + "Type": 0, + "Name": "LoadDefaultConfig", + "HookName": "LoadDefaultConfig", + "HookParameters": {}, + "ReturnBehavior": 0, + "TargetType": "Plugin", + "Category": "Oxide.Core", + "HookDescription": "Called when the config for a plugin should be initialized.\r\n Only called if the config file does not already exist", + "MethodData": { + "MethodName": "LoadDefaultConfig", + "ReturnType": "void", + "Arguments": {} + }, + "CodeAfterInjection": "protected virtual void LoadDefaultConfig() => CallHook(\"LoadDefaultConfig\", null);" + }, + { + "Type": 0, + "Name": "LoadDefaultMessages", + "HookName": "LoadDefaultMessages", + "HookParameters": {}, + "ReturnBehavior": 0, + "TargetType": "Plugin", + "Category": "Oxide.Core", + "HookDescription": "Called when the localization for a plugin should be registered", + "MethodData": { + "MethodName": "LoadDefaultMessages", + "ReturnType": "void", + "Arguments": {} + }, + "CodeAfterInjection": "protected virtual void LoadDefaultMessages() => CallHook(\"LoadDefaultMessages\", null);" + }, + { + "Type": 0, + "Name": "OnRconCommand", + "HookName": "OnRconCommand", + "HookParameters": { + "UserEndPoint": "IPEndPoint", + "command": "string", + "args": "string[]" + }, + "ReturnBehavior": 1, + "TargetType": "RemoteConsole", + "Category": "Oxide.Core", + "HookDescription": "Called when a rcon command is received", + "MethodData": { + "MethodName": "OnMessage", + "ReturnType": "void", + "Arguments": { + "event": "MessageEventArgs", + "connection": "WebSocketContext" + } + }, + "CodeAfterInjection": "private void OnMessage(MessageEventArgs e, WebSocketContext connection)\r\n{\r\n\tif (covalence == null)\r\n\t{\r\n\t\tInterface.Oxide.LogError(\"[Rcon] Failed to process command, Covalence is null\");\r\n\t\treturn;\r\n\t}\r\n\tRemoteMessage message = RemoteMessage.GetMessage(e.Data);\r\n\tif (message == null)\r\n\t{\r\n\t\tInterface.Oxide.LogError(\"[Rcon] Failed to process command, RemoteMessage is null\");\r\n\t\treturn;\r\n\t}\r\n\tif (string.IsNullOrEmpty(message.Message))\r\n\t{\r\n\t\tInterface.Oxide.LogError(\"[Rcon] Failed to process command, RemoteMessage.Text is not set\");\r\n\t\treturn;\r\n\t}\r\n\tstring[] fullCommand = CommandLine.Split(message.Message);\r\n\tstring command = fullCommand[0].ToLower();\r\n\tstring[] args = fullCommand.Skip(1).ToArray();\r\n\tif (Interface.CallHook(\"OnRconCommand\", connection.UserEndPoint, command, args) != null)\r\n\t{\r\n\t\treturn;\r\n\t}\r\n\tcovalence.Server.Command(command, args);\r\n}" + }, + { + "Type": 0, + "Name": "OnMessagePlayer", + "HookName": "OnMessagePlayer", + "HookParameters": { + "formatted": "string", + "player": "BasePlayer", + "userId": "ulong" + }, + "ReturnBehavior": 1, + "TargetType": "Player", + "Category": "Oxide.Rust", + "HookDescription": "Called when a message is sent to a player", + "MethodData": { + "MethodName": "Message", + "ReturnType": "void", + "Arguments": { + "player": "BasePlayer", + "message": "string", + "prefix": "string", + "userId": "ulong", + "params": "object[] args" + } + }, + "CodeAfterInjection": "public void Message(BasePlayer player, string message, string prefix, ulong userId = 0, params object[] args)\r\n{\r\n\tif (string.IsNullOrEmpty(message))\r\n\t{\r\n\t\treturn;\r\n\t}\r\n\r\n\tmessage = args.Length > 0 ? string.Format(Formatter.ToUnity(message), args) : Formatter.ToUnity(message);\r\n\tstring formatted = prefix != null ? $\"{prefix} {message}\" : message;\r\n\tif (Interface.CallHook(\"OnMessagePlayer\", formatted, player, userId) != null)\r\n\t{\r\n\t\treturn;\r\n\t}\r\n\tplayer.SendConsoleCommand(\"chat.add\", 2, userId, formatted);\r\n}" + }, + { + "Type": 0, + "Name": "OnServerInitialized", + "HookName": "OnServerInitialized", + "HookParameters": { + "serverInitialized": "bool" + }, + "ReturnBehavior": 0, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "Let plugins know server startup is complete", + "MethodData": { + "MethodName": "OnPluginLoaded", + "ReturnType": "void", + "Arguments": { + "plugin": "Plugin" + } + }, + "CodeAfterInjection": "private void OnPluginLoaded(Plugin plugin)\r\n{\r\n\tif (serverInitialized)\r\n\t{\r\n\t\t// Call OnServerInitialized for hotloaded plugins\r\n\t\tplugin.CallHook(\"OnServerInitialized\", false);\r\n\t}\r\n}" + }, + { + "Type": 0, + "Name": "OnServerInitialized", + "HookName": "OnServerInitialized", + "HookParameters": { + "serverInitialized": "bool" + }, + "ReturnBehavior": 0, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "Let plugins know server startup is complete", + "MethodData": { + "MethodName": "IOnServerInitialized", + "ReturnType": "void", + "Arguments": {} + }, + "CodeAfterInjection": "private void IOnServerInitialized()\r\n{\r\n\tif (!serverInitialized)\r\n\t{\r\n\t\tAnalytics.Collect();\r\n\r\n\t\tif (!Interface.Oxide.Config.Options.Modded)\r\n\t\t{\r\n\t\t\tInterface.Oxide.LogWarning(\"The server is currently listed under Community. Please be aware that Facepunch only allows admin tools\" +\r\n\t\t\t\t\" (that do not affect gameplay) under the Community section\");\r\n\t\t}\r\n\r\n\t\tserverInitialized = true;\r\n\r\n\t\t// Let plugins know server startup is complete\r\n\t\tInterface.CallHook(\"OnServerInitialized\", serverInitialized);\r\n\t}\r\n}" + }, + { + "Type": 0, + "Name": "OnServerShutdown", + "HookName": "OnServerShutdown", + "HookParameters": {}, + "ReturnBehavior": 0, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "Server is shutting down", + "MethodData": { + "MethodName": "IOnServerShutdown", + "ReturnType": "void", + "Arguments": {} + }, + "CodeAfterInjection": "private void IOnServerShutdown()\r\n{\r\n\tInterface.Oxide.CallHook(\"OnServerShutdown\");\r\n\tInterface.Oxide.OnShutdown();\r\n\tCovalence.PlayerManager.SavePlayerData();\r\n}" + }, + { + "Type": 0, + "Name": "CanUseUI", + "HookName": "CanUseUI", + "HookParameters": { + "player": "BasePlayer", + "json": "string" + }, + "ReturnBehavior": 0, + "TargetType": "CUIHelper", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "AddUi", + "ReturnType": "bool", + "Arguments": { + "player": "BasePlayer", + "json": "string" + } + }, + "CodeAfterInjection": "public static bool AddUi(BasePlayer player, string json)\r\n{\r\n\tif (player?.net != null && Interface.CallHook(\"CanUseUI\", player, json) == null)\r\n\t{\r\n\t\tCommunityEntity.ServerInstance.ClientRPC(RpcTarget.Player(\"AddUI\", player.net.connection ), json);\r\n\t\treturn true;\r\n\t}\r\n\treturn false;\r\n}" + }, + { + "Type": 0, + "Name": "OnDestroyUI", + "HookName": "OnDestroyUI", + "HookParameters": { + "player": "BasePlayer", + "elem": "string" + }, + "ReturnBehavior": 0, + "TargetType": "CUIHelper", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "DestroyUi", + "ReturnType": "bool", + "Arguments": { + "player": "BasePlayer", + "elem": "string" + } + }, + "CodeAfterInjection": "public static bool DestroyUi(BasePlayer player, string elem)\r\n{\r\n\tif (player?.net != null)\r\n\t{\r\n\t\tInterface.CallHook(\"OnDestroyUI\", player, elem);\r\n\t\tCommunityEntity.ServerInstance.ClientRPC(RpcTarget.Player(\"DestroyUI\", player.net.connection ), elem);\r\n\t\treturn true;\r\n\t}\r\n\r\n\treturn false;\r\n}" + }, + { + "Type": 0, + "Name": "OnEntityTakeDamage", + "HookName": "OnEntityTakeDamage", + "HookParameters": { + "entity": "BaseCombatEntity", + "hitInfo": "HitInfo" + }, + "ReturnBehavior": 1, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "IOnBaseCombatEntityHurt", + "ReturnType": "object", + "Arguments": { + "entity": "BaseCombatEntity", + "hitInfo": "HitInfo" + } + }, + "CodeAfterInjection": "private object IOnBaseCombatEntityHurt(BaseCombatEntity entity, HitInfo hitInfo)\r\n{\r\n\treturn entity is BasePlayer ? null : Interface.CallHook(\"OnEntityTakeDamage\", entity, hitInfo);\r\n}" + }, + { + "Type": 0, + "Name": "OnNpcTarget", + "HookName": "OnNpcTarget", + "HookParameters": { + "npc": "BaseNpc", + "target": "BaseEntity" + }, + "ReturnBehavior": 1, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "IOnNpcTarget", + "ReturnType": "object", + "Arguments": { + "npc": "BaseNpc", + "target": "BaseEntity" + } + }, + "CodeAfterInjection": "private object IOnNpcTarget(BaseNpc npc, BaseEntity target)\r\n{\r\n\tif (Interface.CallHook(\"OnNpcTarget\", npc, target) != null)\r\n\t{\r\n\t\tnpc.SetFact(BaseNpc.Facts.HasEnemy, 0);\r\n\t\tnpc.SetFact(BaseNpc.Facts.EnemyRange, 3);\r\n\t\tnpc.SetFact(BaseNpc.Facts.AfraidRange, 1);\r\n\r\n\t\t//TODO: Find replacements of those:\r\n\t\t// npc.AiContext.EnemyPlayer = null;\r\n\t\t// npc.AiContext.LastEnemyPlayerScore = 0f;\r\n\r\n\t\tnpc.playerTargetDecisionStartTime = 0f;\r\n\t\treturn 0f;\r\n\t}\r\n\r\n\treturn null;\r\n}" + }, + { + "Type": 0, + "Name": "OnEntitySaved", + "HookName": "OnEntitySaved", + "HookParameters": { + "baseNetworkable": "BaseNetworkable", + "saveInfo": "BaseNetworkable.SaveInfo" + }, + "ReturnBehavior": 0, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "IOnEntitySaved", + "ReturnType": "void", + "Arguments": { + "baseNetworkable": "BaseNetworkable", + "saveInfo": "BaseNetworkable.SaveInfo" + } + }, + "CodeAfterInjection": "private void IOnEntitySaved(BaseNetworkable baseNetworkable, BaseNetworkable.SaveInfo saveInfo)\r\n{\r\n\t// Only call when saving for the network since we don't expect plugins to want to intercept saving to disk\r\n\tif (!serverInitialized || saveInfo.forConnection == null)\r\n\t{\r\n\t\treturn;\r\n\t}\r\n\r\n\tInterface.CallHook(\"OnEntitySaved\", baseNetworkable, saveInfo);\r\n}" + }, + { + "Type": 0, + "Name": "OnLoseCondition", + "HookName": "OnLoseCondition", + "HookParameters": { + "item": "Item", + "amount": "ref float" + }, + "ReturnBehavior": 0, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "Called right before the condition of the item is modified", + "MethodData": { + "MethodName": "IOnLoseCondition", + "ReturnType": "object", + "Arguments": { + "item": "Item", + "amount": "float" + } + }, + "CodeAfterInjection": "private object IOnLoseCondition(Item item, float amount)\r\n{\r\n\tobject[] arguments = { item, amount };\r\n\tInterface.CallHook(\"OnLoseCondition\", arguments);\r\n\tamount = (float)arguments[1];\r\n\tfloat condition = item.condition;\r\n\titem.condition -= amount;\r\n\tif (item.condition <= 0f && item.condition < condition)\r\n\t{\r\n\t\titem.OnBroken();\r\n\t}\r\n\r\n\treturn true;\r\n}" + }, + { + "Type": 0, + "Name": "CanPickupEntity", + "HookName": "CanPickupEntity", + "HookParameters": { + "basePlayer": "BasePlayer", + "entity": "DoorCloser" + }, + "ReturnBehavior": 3, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "Called when a player attempts to pickup a deployed entity (AutoTurret, BaseMountable, BearTrap, DecorDeployable, Door, DoorCloser, ReactiveTarget, SamSite, SleepingBag, SpinnerWheel, StorageContainer, etc.)", + "MethodData": { + "MethodName": " ICanPickupEntity", + "ReturnType": "object", + "Arguments": { + "basePlayer": "BasePlayer", + "entity": "DoorCloser" + } + }, + "CodeAfterInjection": "private object ICanPickupEntity(BasePlayer basePlayer, DoorCloser entity)\r\n{\r\n\tobject callHook = Interface.CallHook(\"CanPickupEntity\", basePlayer, entity);\r\n\treturn callHook is bool result && !result ? (object)true : null;\r\n}" + }, + { + "Type": 0, + "Name": "OnEntityTakeDamage", + "HookName": "OnEntityTakeDamage", + "HookParameters": { + "basePlayer": "BasePlayer", + "hitInfo": "HitInfo" + }, + "ReturnBehavior": 1, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "Alternatively, modify the HitInfo object to change the damage.\r\n It should be okay to set the damage to 0, but if you don't return non-null, the player's client will receive a damage indicator (if entity is a BasePlayer).\r\n HitInfo has all kinds of useful things in it, such as Weapon, damageProperties or damageTypes", + "MethodData": { + "MethodName": "IOnBasePlayerAttacked", + "ReturnType": "object", + "Arguments": { + "basePlayer": "BasePlayer", + "hitInfo": "HitInfo" + } + }, + "CodeAfterInjection": "private object IOnBasePlayerAttacked(BasePlayer basePlayer, HitInfo hitInfo)\r\n{\r\n\tif (!serverInitialized || basePlayer == null || hitInfo == null || basePlayer.IsDead() || isPlayerTakingDamage || basePlayer is NPCPlayer)\r\n\t{\r\n\t\treturn null;\r\n\t}\r\n\r\n\tif (Interface.CallHook(\"OnEntityTakeDamage\", basePlayer, hitInfo) != null)\r\n\t{\r\n\t\treturn true;\r\n\t}\r\n\r\n\tisPlayerTakingDamage = true;\r\n\ttry\r\n\t{\r\n\t\tbasePlayer.OnAttacked(hitInfo);\r\n\t}\r\n\tfinally\r\n\t{\r\n\t\tisPlayerTakingDamage = false;\r\n\t}\r\n\treturn true;\r\n}" + }, + { + "Type": 0, + "Name": "OnEntityTakeDamage", + "HookName": "OnEntityTakeDamage", + "HookParameters": { + "basePlayer": "BasePlayer", + "hitInfo": "HitInfo" + }, + "ReturnBehavior": 1, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "IOnBasePlayerHurt", + "ReturnType": "object", + "Arguments": { + "basePlayer": "BasePlayer", + "hitInfo": "HitInfo" + } + }, + "CodeAfterInjection": "private object IOnBasePlayerHurt(BasePlayer basePlayer, HitInfo hitInfo)\r\n{\r\n\treturn isPlayerTakingDamage ? null : Interface.CallHook(\"OnEntityTakeDamage\", basePlayer, hitInfo);\r\n}" + }, + { + "Type": 0, + "Name": "OnPlayerBanned", + "HookName": "OnPlayerBanned", + "HookParameters": { + "playerName": "string", + "steamId": "ulong", + "Address": "string", + "reason": "string", + "expiry": "long" + }, + "ReturnBehavior": 0, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "OnServerUserSet", + "ReturnType": "void", + "Arguments": { + "steamId": "ulong", + "group": "ServerUsers.UserGroup", + "playerName": "string", + "reason": "string", + "expiry": "long" + } + }, + "CodeAfterInjection": "private void OnServerUserSet(ulong steamId, ServerUsers.UserGroup group, string playerName, string reason, long expiry)\r\n{\r\n\tif (serverInitialized && group == ServerUsers.UserGroup.Banned)\r\n\t{\r\n\t\tstring playerId = steamId.ToString();\r\n\t\tIPlayer player = Covalence.PlayerManager.FindPlayerById(playerId);\r\n\t\tInterface.CallHook(\"OnPlayerBanned\", playerName, steamId, player?.Address ?? \"0\", reason, expiry);\r\n\t\tInterface.CallHook(\"OnUserBanned\", playerName, playerId, player?.Address ?? \"0\", reason, expiry);\r\n\t}\r\n}" + }, + { + "Type": 0, + "Name": "OnUserBanned", + "HookName": "OnUserBanned", + "HookParameters": { + "playerName": "string", + "steamId": "string", + "Address": "string", + "reason": "string", + "expiry": "long" + }, + "ReturnBehavior": 0, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "OnServerUserSet", + "ReturnType": "void", + "Arguments": { + "steamId": "ulong", + "group": "ServerUsers.UserGroup", + "playerName": "string", + "reason": "string", + "expiry": "long" + } + }, + "CodeAfterInjection": "private void OnServerUserSet(ulong steamId, ServerUsers.UserGroup group, string playerName, string reason, long expiry)\r\n{\r\n\tif (serverInitialized && group == ServerUsers.UserGroup.Banned)\r\n\t{\r\n\t\tstring playerId = steamId.ToString();\r\n\t\tIPlayer player = Covalence.PlayerManager.FindPlayerById(playerId);\r\n\t\tInterface.CallHook(\"OnPlayerBanned\", playerName, steamId, player?.Address ?? \"0\", reason, expiry);\r\n\t\tInterface.CallHook(\"OnUserBanned\", playerName, playerId, player?.Address ?? \"0\", reason, expiry);\r\n\t}\r\n}" + }, + { + "Type": 0, + "Name": "OnPlayerUnbanned", + "HookName": "OnPlayerUnbanned", + "HookParameters": { + "playerName": "string", + "steamId": "ulong", + "address": "string" + }, + "ReturnBehavior": 0, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "OnServerUserRemove", + "ReturnType": "void", + "Arguments": { + "steamId": "ulong" + } + }, + "CodeAfterInjection": "private void OnServerUserRemove(ulong steamId)\r\n{\r\n\tif (serverInitialized && ServerUsers.users.ContainsKey(steamId) && ServerUsers.users[steamId].group == ServerUsers.UserGroup.Banned)\r\n\t{\r\n\t\tstring playerId = steamId.ToString();\r\n\t\tIPlayer player = Covalence.PlayerManager.FindPlayerById(playerId);\r\n\t\tInterface.CallHook(\"OnPlayerUnbanned\", player?.Name ?? \"Unnamed\", steamId, player?.Address ?? \"0\");\r\n\t\tInterface.CallHook(\"OnUserUnbanned\", player?.Name ?? \"Unnamed\", playerId, player?.Address ?? \"0\");\r\n\t}\r\n}" + }, + { + "Type": 0, + "Name": "OnUserUnbanned", + "HookName": "OnUserUnbanned", + "HookParameters": { + "playerName": "string", + "steamId": "string", + "address": "string" + }, + "ReturnBehavior": 0, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "OnServerUserRemove", + "ReturnType": "void", + "Arguments": { + "steamId": "ulong" + } + }, + "CodeAfterInjection": "private void OnServerUserRemove(ulong steamId)\r\n{\r\n\tif (serverInitialized && ServerUsers.users.ContainsKey(steamId) && ServerUsers.users[steamId].group == ServerUsers.UserGroup.Banned)\r\n\t{\r\n\t\tstring playerId = steamId.ToString();\r\n\t\tIPlayer player = Covalence.PlayerManager.FindPlayerById(playerId);\r\n\t\tInterface.CallHook(\"OnPlayerUnbanned\", player?.Name ?? \"Unnamed\", steamId, player?.Address ?? \"0\");\r\n\t\tInterface.CallHook(\"OnUserUnbanned\", player?.Name ?? \"Unnamed\", playerId, player?.Address ?? \"0\");\r\n\t}\r\n}" + }, + { + "Type": 0, + "Name": "CanClientLogin", + "HookName": "CanClientLogin", + "HookParameters": { + "connection": "Connection" + }, + "ReturnBehavior": 3, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "Called when a player attempt to login the server", + "MethodData": { + "MethodName": " IOnUserApprove", + "ReturnType": "object", + "Arguments": { + "connection": "Connection" + } + }, + "CodeAfterInjection": "private object IOnUserApprove(Connection connection)\r\n{\r\n\tstring playerName = connection.username;\r\n\tstring connectionId = connection.userid.ToString();\r\n\tstring connectionIp = Regex.Replace(connection.ipaddress, ipPattern, \"\");\r\n\tuint authLevel = connection.authLevel;\r\n\r\n\t// Update name and groups with permissions\r\n\tif (permission.IsLoaded)\r\n\t{\r\n\t\tpermission.UpdateNickname(connectionId, playerName);\r\n\t\tOxideConfig.DefaultGroups defaultGroups = Interface.Oxide.Config.Options.DefaultGroups;\r\n\t\tif (!permission.UserHasGroup(connectionId, defaultGroups.Players))\r\n\t\t{\r\n\t\t\tpermission.AddUserGroup(connectionId, defaultGroups.Players);\r\n\t\t}\r\n\t\tif (authLevel >= 2 && !permission.UserHasGroup(connectionId, defaultGroups.Administrators))\r\n\t\t{\r\n\t\t\tpermission.AddUserGroup(connectionId, defaultGroups.Administrators);\r\n\t\t}\r\n\t}\r\n\r\n\t// Let covalence know\r\n\tCovalence.PlayerManager.PlayerJoin(connection.userid, playerName);\r\n\r\n\t// Call hooks for plugins\r\n\tobject loginSpecific = Interface.CallHook(\"CanClientLogin\", connection);\r\n\tobject loginCovalence = Interface.CallHook(\"CanUserLogin\", playerName, connectionId, connectionIp);\r\n\tobject canLogin = loginSpecific is null ? loginCovalence : loginSpecific;\r\n\tif (canLogin is string || canLogin is bool loginBlocked && !loginBlocked)\r\n\t{\r\n\t\tConnectionAuth.Reject(connection, canLogin is string ? canLogin.ToString() : lang.GetMessage(\"ConnectionRejected\", this, connectionId));\r\n\t\treturn true;\r\n\t}\r\n\r\n\t// Call hooks for plugins\r\n\tobject approvedSpecific = Interface.CallHook(\"OnUserApprove\", connection);\r\n\tobject approvedCovalence = Interface.CallHook(\"OnUserApproved\", playerName, connectionId, connectionIp);\r\n\treturn approvedSpecific is null ? approvedCovalence : approvedSpecific;\r\n}" + }, + { + "Type": 0, + "Name": "CanUserLogin", + "HookName": "CanUserLogin", + "HookParameters": { + "playerName": "string", + "connectionId": "string", + "connectionIp": "string" + }, + "ReturnBehavior": 3, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "Called when a player attempt to login the server", + "MethodData": { + "MethodName": " IOnUserApprove", + "ReturnType": "object", + "Arguments": { + "connection": "Connection" + } + }, + "CodeAfterInjection": "private object IOnUserApprove(Connection connection)\r\n{\r\n\tstring playerName = connection.username;\r\n\tstring connectionId = connection.userid.ToString();\r\n\tstring connectionIp = Regex.Replace(connection.ipaddress, ipPattern, \"\");\r\n\tuint authLevel = connection.authLevel;\r\n\r\n\t// Update name and groups with permissions\r\n\tif (permission.IsLoaded)\r\n\t{\r\n\t\tpermission.UpdateNickname(connectionId, playerName);\r\n\t\tOxideConfig.DefaultGroups defaultGroups = Interface.Oxide.Config.Options.DefaultGroups;\r\n\t\tif (!permission.UserHasGroup(connectionId, defaultGroups.Players))\r\n\t\t{\r\n\t\t\tpermission.AddUserGroup(connectionId, defaultGroups.Players);\r\n\t\t}\r\n\t\tif (authLevel >= 2 && !permission.UserHasGroup(connectionId, defaultGroups.Administrators))\r\n\t\t{\r\n\t\t\tpermission.AddUserGroup(connectionId, defaultGroups.Administrators);\r\n\t\t}\r\n\t}\r\n\r\n\t// Let covalence know\r\n\tCovalence.PlayerManager.PlayerJoin(connection.userid, playerName);\r\n\r\n\t// Call hooks for plugins\r\n\tobject loginSpecific = Interface.CallHook(\"CanClientLogin\", connection);\r\n\tobject loginCovalence = Interface.CallHook(\"CanUserLogin\", playerName, connectionId, connectionIp);\r\n\tobject canLogin = loginSpecific is null ? loginCovalence : loginSpecific;\r\n\tif (canLogin is string || canLogin is bool loginBlocked && !loginBlocked)\r\n\t{\r\n\t\tConnectionAuth.Reject(connection, canLogin is string ? canLogin.ToString() : lang.GetMessage(\"ConnectionRejected\", this, connectionId));\r\n\t\treturn true;\r\n\t}\r\n\r\n\t// Call hooks for plugins\r\n\tobject approvedSpecific = Interface.CallHook(\"OnUserApprove\", connection);\r\n\tobject approvedCovalence = Interface.CallHook(\"OnUserApproved\", playerName, connectionId, connectionIp);\r\n\treturn approvedSpecific is null ? approvedCovalence : approvedSpecific;\r\n}" + }, + { + "Type": 0, + "Name": "OnPlayerBanned", + "HookName": "OnPlayerBanned", + "HookParameters": { + "connection": "Connection", + "status": "string" + }, + "ReturnBehavior": 0, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "IOnPlayerBanned", + "ReturnType": "void", + "Arguments": { + "connection": "Connection", + "status": "AuthResponse" + } + }, + "CodeAfterInjection": "private void IOnPlayerBanned(Connection connection, AuthResponse status)\r\n{\r\n\t// TODO: Get BasePlayer and pass instead of Connection\r\n\tInterface.CallHook(\"OnPlayerBanned\", connection, status.ToString());\r\n}" + }, + { + "Type": 0, + "Name": "OnPlayerOfflineChat", + "HookName": "OnPlayerOfflineChat", + "HookParameters": { + "playerid": "ulong", + "playerName": "string", + "message": "string", + "channel": "Chat.ChatChannel" + }, + "ReturnBehavior": 0, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "Hook is called when a chat message is sent to an offline player ", + "MethodData": { + "MethodName": "IOnPlayerChat(ulong playerId, string playerName, string message, Chat.ChatChannel channel, BasePlayer basePlayer)", + "ReturnType": "object", + "Arguments": { + "playerId": "ulong", + "playerName": "string", + "message": "string", + "channel": "Chat.ChatChannel", + "player": "BasePlayer" + } + }, + "CodeAfterInjection": "private object IOnPlayerChat(ulong playerId, string playerName, string message, Chat.ChatChannel channel, BasePlayer basePlayer)\r\n{\r\n\t// Ignore empty and \"default\" text\r\n\tif (string.IsNullOrEmpty(message) || message.Equals(\"text\"))\r\n\t{\r\n\t\treturn true;\r\n\t}\r\n\r\n\t// Check if chat command\r\n\tstring chatCommandPrefix = CommandHandler.GetChatCommandPrefix(message);\r\n\tif ( chatCommandPrefix != null )\r\n\t{\r\n\t\tTryRunPlayerCommand( basePlayer, message, chatCommandPrefix );\r\n\t\treturn false;\r\n\t}\r\n\r\n\tmessage = message.EscapeRichText();\r\n\r\n\t// Check if using Rust+ app\r\n\tif (basePlayer == null || !basePlayer.IsConnected)\r\n\t{\r\n\t\t// Call offline chat hook\r\n\t\treturn Interface.CallHook(\"OnPlayerOfflineChat\", playerId, playerName, message, channel);\r\n\t}\r\n\r\n\t// Call hooks for plugins\r\n\tobject chatSpecific = Interface.CallHook(\"OnPlayerChat\", basePlayer, message, channel);\r\n\tobject chatCovalence = Interface.CallHook(\"OnUserChat\", basePlayer.IPlayer, message);\r\n\treturn chatSpecific is null ? chatCovalence : chatSpecific;\r\n}" + }, + { + "Type": 0, + "Name": "OnPlayerChat", + "HookName": "OnPlayerChat", + "HookParameters": { + "player": "BasePlayer", + "message": "string", + "channel": "Chat.ChatChannel" + }, + "ReturnBehavior": 0, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "IOnPlayerChat(ulong playerId, string playerName, string message, Chat.ChatChannel channel, BasePlayer basePlayer)", + "ReturnType": "object", + "Arguments": { + "playerId": "ulong", + "playerName": "string", + "message": "string", + "channel": "Chat.ChatChannel", + "player": "BasePlayer" + } + }, + "CodeAfterInjection": "private object IOnPlayerChat(ulong playerId, string playerName, string message, Chat.ChatChannel channel, BasePlayer basePlayer)\r\n{\r\n\t// Ignore empty and \"default\" text\r\n\tif (string.IsNullOrEmpty(message) || message.Equals(\"text\"))\r\n\t{\r\n\t\treturn true;\r\n\t}\r\n\r\n\t// Check if chat command\r\n\tstring chatCommandPrefix = CommandHandler.GetChatCommandPrefix(message);\r\n\tif ( chatCommandPrefix != null )\r\n\t{\r\n\t\tTryRunPlayerCommand( basePlayer, message, chatCommandPrefix );\r\n\t\treturn false;\r\n\t}\r\n\r\n\tmessage = message.EscapeRichText();\r\n\r\n\t// Check if using Rust+ app\r\n\tif (basePlayer == null || !basePlayer.IsConnected)\r\n\t{\r\n\t\t// Call offline chat hook\r\n\t\treturn Interface.CallHook(\"OnPlayerOfflineChat\", playerId, playerName, message, channel);\r\n\t}\r\n\r\n\t// Call hooks for plugins\r\n\tobject chatSpecific = Interface.CallHook(\"OnPlayerChat\", basePlayer, message, channel);\r\n\tobject chatCovalence = Interface.CallHook(\"OnUserChat\", basePlayer.IPlayer, message);\r\n\treturn chatSpecific is null ? chatCovalence : chatSpecific;\r\n}" + }, + { + "Type": 0, + "Name": "OnUserChat", + "HookName": "OnUserChat", + "HookParameters": { + "iplayer": "IPlayer", + "message": "string" + }, + "ReturnBehavior": 0, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "IOnPlayerChat(ulong playerId, string playerName, string message, Chat.ChatChannel channel, BasePlayer basePlayer)", + "ReturnType": "object", + "Arguments": { + "playerId": "ulong", + "playerName": "string", + "message": "string", + "channel": "Chat.ChatChannel", + "player": "BasePlayer" + } + }, + "CodeAfterInjection": "private object IOnPlayerChat(ulong playerId, string playerName, string message, Chat.ChatChannel channel, BasePlayer basePlayer)\r\n{\r\n\t// Ignore empty and \"default\" text\r\n\tif (string.IsNullOrEmpty(message) || message.Equals(\"text\"))\r\n\t{\r\n\t\treturn true;\r\n\t}\r\n\r\n\t// Check if chat command\r\n\tstring chatCommandPrefix = CommandHandler.GetChatCommandPrefix(message);\r\n\tif ( chatCommandPrefix != null )\r\n\t{\r\n\t\tTryRunPlayerCommand( basePlayer, message, chatCommandPrefix );\r\n\t\treturn false;\r\n\t}\r\n\r\n\tmessage = message.EscapeRichText();\r\n\r\n\t// Check if using Rust+ app\r\n\tif (basePlayer == null || !basePlayer.IsConnected)\r\n\t{\r\n\t\t// Call offline chat hook\r\n\t\treturn Interface.CallHook(\"OnPlayerOfflineChat\", playerId, playerName, message, channel);\r\n\t}\r\n\r\n\t// Call hooks for plugins\r\n\tobject chatSpecific = Interface.CallHook(\"OnPlayerChat\", basePlayer, message, channel);\r\n\tobject chatCovalence = Interface.CallHook(\"OnUserChat\", basePlayer.IPlayer, message);\r\n\treturn chatSpecific is null ? chatCovalence : chatSpecific;\r\n}" + }, + { + "Type": 0, + "Name": "OnApplicationCommand", + "HookName": "OnApplicationCommand", + "HookParameters": { + "player": "BasePlayer", + "cmd": "string", + "args": "string[]" + }, + "ReturnBehavior": 0, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "TryRunPlayerCommand", + "ReturnType": "void", + "Arguments": { + "player": "BasePlayer", + "message": "string", + "commandPrefix": "string" + } + }, + "CodeAfterInjection": "private void TryRunPlayerCommand(BasePlayer basePlayer, string message, string commandPrefix)\r\n{\r\n\tif (basePlayer == null)\r\n\t{\r\n\t\treturn;\r\n\t}\r\n\r\n\tstring str = message.Replace(\"\\n\", \"\").Replace(\"\\r\", \"\").Trim();\r\n\r\n\t// Check if it is a chat command\r\n\tif (string.IsNullOrEmpty(str))\r\n\t{\r\n\t\treturn;\r\n\t}\r\n\r\n\t// Parse the command\r\n\tParseCommand(str.Substring(commandPrefix.Length), out string cmd, out string[] args);\r\n\tif (cmd == null)\r\n\t{\r\n\t\treturn;\r\n\t}\r\n\r\n\t// Check if using Rust+ app\r\n\tif (!basePlayer.IsConnected)\r\n\t{\r\n\t\tInterface.CallHook(\"OnApplicationCommand\", basePlayer, cmd, args);\r\n\t\tInterface.CallHook(\"OnApplicationCommand\", basePlayer.IPlayer, cmd, args);\r\n\t\treturn;\r\n\t}\r\n\r\n\t// Is the command blocked?\r\n\tobject commandSpecific = Interface.CallHook(\"OnPlayerCommand\", basePlayer, cmd, args);\r\n\tobject commandCovalence = Interface.CallHook(\"OnUserCommand\", basePlayer.IPlayer, cmd, args);\r\n\tobject canBlock = commandSpecific is null ? commandCovalence : commandSpecific;\r\n\tif (canBlock != null)\r\n\t{\r\n\t\treturn;\r\n\t}\r\n\r\n\t//---\r\n}" + }, + { + "Type": 0, + "Name": "OnApplicationCommand", + "HookName": "OnApplicationCommand", + "HookParameters": { + "iplayer": "IPlayer", + "cmd": "string", + "args": "string[]" + }, + "ReturnBehavior": 0, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "TryRunPlayerCommand", + "ReturnType": "void", + "Arguments": { + "player": "BasePlayer", + "message": "string", + "commandPrefix": "string" + } + }, + "CodeAfterInjection": "private void TryRunPlayerCommand(BasePlayer basePlayer, string message, string commandPrefix)\r\n{\r\n\tif (basePlayer == null)\r\n\t{\r\n\t\treturn;\r\n\t}\r\n\r\n\tstring str = message.Replace(\"\\n\", \"\").Replace(\"\\r\", \"\").Trim();\r\n\r\n\t// Check if it is a chat command\r\n\tif (string.IsNullOrEmpty(str))\r\n\t{\r\n\t\treturn;\r\n\t}\r\n\r\n\t// Parse the command\r\n\tParseCommand(str.Substring(commandPrefix.Length), out string cmd, out string[] args);\r\n\tif (cmd == null)\r\n\t{\r\n\t\treturn;\r\n\t}\r\n\r\n\t// Check if using Rust+ app\r\n\tif (!basePlayer.IsConnected)\r\n\t{\r\n\t\tInterface.CallHook(\"OnApplicationCommand\", basePlayer, cmd, args);\r\n\t\tInterface.CallHook(\"OnApplicationCommand\", basePlayer.IPlayer, cmd, args);\r\n\t\treturn;\r\n\t}\r\n\r\n\t// Is the command blocked?\r\n\tobject commandSpecific = Interface.CallHook(\"OnPlayerCommand\", basePlayer, cmd, args);\r\n\tobject commandCovalence = Interface.CallHook(\"OnUserCommand\", basePlayer.IPlayer, cmd, args);\r\n\tobject canBlock = commandSpecific is null ? commandCovalence : commandSpecific;\r\n\tif (canBlock != null)\r\n\t{\r\n\t\treturn;\r\n\t}\r\n\r\n\t//---\r\n}" + }, + { + "Type": 0, + "Name": "OnPlayerCommand", + "HookName": "OnPlayerCommand", + "HookParameters": { + "player": "BasePlayer", + "cmd": "string", + "args": "string[]" + }, + "ReturnBehavior": 0, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "TryRunPlayerCommand", + "ReturnType": "void", + "Arguments": { + "player": "BasePlayer", + "message": "string", + "commandPrefix": "string" + } + }, + "CodeAfterInjection": "private void TryRunPlayerCommand(BasePlayer basePlayer, string message, string commandPrefix)\r\n{\r\n\tif (basePlayer == null)\r\n\t{\r\n\t\treturn;\r\n\t}\r\n\r\n\tstring str = message.Replace(\"\\n\", \"\").Replace(\"\\r\", \"\").Trim();\r\n\r\n\t// Check if it is a chat command\r\n\tif (string.IsNullOrEmpty(str))\r\n\t{\r\n\t\treturn;\r\n\t}\r\n\r\n\t// Parse the command\r\n\tParseCommand(str.Substring(commandPrefix.Length), out string cmd, out string[] args);\r\n\tif (cmd == null)\r\n\t{\r\n\t\treturn;\r\n\t}\r\n\r\n\t// Check if using Rust+ app\r\n\tif (!basePlayer.IsConnected)\r\n\t{\r\n\t\tInterface.CallHook(\"OnApplicationCommand\", basePlayer, cmd, args);\r\n\t\tInterface.CallHook(\"OnApplicationCommand\", basePlayer.IPlayer, cmd, args);\r\n\t\treturn;\r\n\t}\r\n\r\n\t// Is the command blocked?\r\n\tobject commandSpecific = Interface.CallHook(\"OnPlayerCommand\", basePlayer, cmd, args);\r\n\tobject commandCovalence = Interface.CallHook(\"OnUserCommand\", basePlayer.IPlayer, cmd, args);\r\n\tobject canBlock = commandSpecific is null ? commandCovalence : commandSpecific;\r\n\tif (canBlock != null)\r\n\t{\r\n\t\treturn;\r\n\t}\r\n\r\n\t//---\r\n}" + }, + { + "Type": 0, + "Name": "OnUserCommand", + "HookName": "OnUserCommand", + "HookParameters": { + "iplayer": "IPlayer", + "cmd": "string", + "args": "string[]" + }, + "ReturnBehavior": 0, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "TryRunPlayerCommand", + "ReturnType": "void", + "Arguments": { + "player": "BasePlayer", + "message": "string", + "commandPrefix": "string" + } + }, + "CodeAfterInjection": "private void TryRunPlayerCommand(BasePlayer basePlayer, string message, string commandPrefix)\r\n{\r\n\tif (basePlayer == null)\r\n\t{\r\n\t\treturn;\r\n\t}\r\n\r\n\tstring str = message.Replace(\"\\n\", \"\").Replace(\"\\r\", \"\").Trim();\r\n\r\n\t// Check if it is a chat command\r\n\tif (string.IsNullOrEmpty(str))\r\n\t{\r\n\t\treturn;\r\n\t}\r\n\r\n\t// Parse the command\r\n\tParseCommand(str.Substring(commandPrefix.Length), out string cmd, out string[] args);\r\n\tif (cmd == null)\r\n\t{\r\n\t\treturn;\r\n\t}\r\n\r\n\t// Check if using Rust+ app\r\n\tif (!basePlayer.IsConnected)\r\n\t{\r\n\t\tInterface.CallHook(\"OnApplicationCommand\", basePlayer, cmd, args);\r\n\t\tInterface.CallHook(\"OnApplicationCommand\", basePlayer.IPlayer, cmd, args);\r\n\t\treturn;\r\n\t}\r\n\r\n\t// Is the command blocked?\r\n\tobject commandSpecific = Interface.CallHook(\"OnPlayerCommand\", basePlayer, cmd, args);\r\n\tobject commandCovalence = Interface.CallHook(\"OnUserCommand\", basePlayer.IPlayer, cmd, args);\r\n\tobject canBlock = commandSpecific is null ? commandCovalence : commandSpecific;\r\n\tif (canBlock != null)\r\n\t{\r\n\t\treturn;\r\n\t}\r\n\r\n\t//---\r\n}" + }, + { + "Type": 0, + "Name": "OnUserConnected", + "HookName": "OnUserConnected", + "HookParameters": { + "iplayer": "IPlayer" + }, + "ReturnBehavior": 0, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "IOnPlayerConnected", + "ReturnType": "void", + "Arguments": { + "player": "BasePlayer" + } + }, + "CodeAfterInjection": "private void IOnPlayerConnected(BasePlayer basePlayer)\r\n{\r\n\t// Set language for player\r\n\tlang.SetLanguage(basePlayer.net.connection.info.GetString(\"global.language\", \"en\"), basePlayer.UserIDString);\r\n\r\n\t// Send CUI to player manually\r\n\tbasePlayer.SendEntitySnapshot(CommunityEntity.ServerInstance);\r\n\r\n\t// Let covalence know\r\n\tCovalence.PlayerManager.PlayerConnected(basePlayer);\r\n\tIPlayer player = Covalence.PlayerManager.FindPlayerById(basePlayer.UserIDString);\r\n\tif (player != null)\r\n\t{\r\n\t\tbasePlayer.IPlayer = player;\r\n\t\tInterface.CallHook(\"OnUserConnected\", player);\r\n\t}\r\n\r\n\tInterface.Oxide.CallHook(\"OnPlayerConnected\", basePlayer);\r\n}" + }, + { + "Type": 0, + "Name": "OnPlayerConnected", + "HookName": "OnPlayerConnected", + "HookParameters": { + "player": "BasePlayer" + }, + "ReturnBehavior": 0, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "IOnPlayerConnected", + "ReturnType": "void", + "Arguments": { + "player": "BasePlayer" + } + }, + "CodeAfterInjection": "private void IOnPlayerConnected(BasePlayer basePlayer)\r\n{\r\n\t// Set language for player\r\n\tlang.SetLanguage(basePlayer.net.connection.info.GetString(\"global.language\", \"en\"), basePlayer.UserIDString);\r\n\r\n\t// Send CUI to player manually\r\n\tbasePlayer.SendEntitySnapshot(CommunityEntity.ServerInstance);\r\n\r\n\t// Let covalence know\r\n\tCovalence.PlayerManager.PlayerConnected(basePlayer);\r\n\tIPlayer player = Covalence.PlayerManager.FindPlayerById(basePlayer.UserIDString);\r\n\tif (player != null)\r\n\t{\r\n\t\tbasePlayer.IPlayer = player;\r\n\t\tInterface.CallHook(\"OnUserConnected\", player);\r\n\t}\r\n\r\n\tInterface.Oxide.CallHook(\"OnPlayerConnected\", basePlayer);\r\n}" + }, + { + "Type": 0, + "Name": "OnUserDisconnected", + "HookName": "OnUserDisconnected", + "HookParameters": { + "iplayer": "IPlayer", + "reason": "string" + }, + "ReturnBehavior": 0, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "OnPlayerDisconnected", + "ReturnType": "void", + "Arguments": { + "player": "BasePlayer", + "reason": "string" + } + }, + "CodeAfterInjection": "private void OnPlayerDisconnected(BasePlayer basePlayer, string reason)\r\n{\r\n\tIPlayer player = basePlayer.IPlayer;\r\n\tif (player != null)\r\n\t{\r\n\t\tInterface.CallHook(\"OnUserDisconnected\", player, reason);\r\n\t}\r\n\r\n\tCovalence.PlayerManager.PlayerDisconnected(basePlayer);\r\n}" + }, + { + "Type": 0, + "Name": "OnPlayerLanguageChanged", + "HookName": "OnPlayerLanguageChanged", + "HookParameters": { + "player": "BasePlayer", + "val": "string" + }, + "ReturnBehavior": 0, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "OnPlayerSetInfo", + "ReturnType": "void", + "Arguments": { + "connection": "Connection", + "key": "string", + "val": "string" + } + }, + "CodeAfterInjection": "private void OnPlayerSetInfo(Connection connection, string key, string val)\r\n{\r\n\t// Change language for player\r\n\tif (key == \"global.language\")\r\n\t{\r\n\t\tlang.SetLanguage(val, connection.userid.ToString());\r\n\r\n\t\tBasePlayer basePlayer = connection.player as BasePlayer;\r\n\t\tif (basePlayer != null)\r\n\t\t{\r\n\t\t\tInterface.CallHook(\"OnPlayerLanguageChanged\", basePlayer, val);\r\n\t\t\tif (basePlayer.IPlayer != null)\r\n\t\t\t{\r\n\t\t\t\tInterface.CallHook(\"OnPlayerLanguageChanged\", basePlayer.IPlayer, val);\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}" + }, + { + "Type": 0, + "Name": "OnPlayerLanguageChanged", + "HookName": "OnPlayerLanguageChanged", + "HookParameters": { + "iplayer": "IPlayer", + "val": "string" + }, + "ReturnBehavior": 0, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "OnPlayerSetInfo", + "ReturnType": "void", + "Arguments": { + "connection": "Connection", + "key": "string", + "val": "string" + } + }, + "CodeAfterInjection": "private void OnPlayerSetInfo(Connection connection, string key, string val)\r\n{\r\n\t// Change language for player\r\n\tif (key == \"global.language\")\r\n\t{\r\n\t\tlang.SetLanguage(val, connection.userid.ToString());\r\n\r\n\t\tBasePlayer basePlayer = connection.player as BasePlayer;\r\n\t\tif (basePlayer != null)\r\n\t\t{\r\n\t\t\tInterface.CallHook(\"OnPlayerLanguageChanged\", basePlayer, val);\r\n\t\t\tif (basePlayer.IPlayer != null)\r\n\t\t\t{\r\n\t\t\t\tInterface.CallHook(\"OnPlayerLanguageChanged\", basePlayer.IPlayer, val);\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}" + }, + { + "Type": 0, + "Name": "OnUserKicked", + "HookName": "OnUserKicked", + "HookParameters": { + "iplayer": "IPlayer", + "reason": "string" + }, + "ReturnBehavior": 0, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "OnPlayerKicked", + "ReturnType": "void", + "Arguments": { + "player": "BasePlayer", + "reason": "string" + } + }, + "CodeAfterInjection": "private void OnPlayerKicked(BasePlayer basePlayer, string reason)\r\n{\r\n\tIPlayer player = basePlayer.IPlayer;\r\n\tif (player != null)\r\n\t{\r\n\t\tInterface.CallHook(\"OnUserKicked\", basePlayer.IPlayer, reason);\r\n\t}\r\n}" + }, + { + "Type": 0, + "Name": "OnUserRespawn", + "HookName": "OnUserRespawn", + "HookParameters": { + "iplayer": "IPlayer" + }, + "ReturnBehavior": 1, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "object OnPlayerRespawn", + "ReturnType": "object", + "Arguments": { + "player": "BasePlayer" + } + }, + "CodeAfterInjection": "private object OnPlayerRespawn(BasePlayer basePlayer)\r\n{\r\n\tIPlayer player = basePlayer.IPlayer;\r\n\treturn player != null ? Interface.CallHook(\"OnUserRespawn\", player) : null;\r\n}" + }, + { + "Type": 0, + "Name": "OnUserRespawned", + "HookName": "OnUserRespawned", + "HookParameters": { + "iplayer": "IPlayer" + }, + "ReturnBehavior": 0, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "OnPlayerRespawned", + "ReturnType": "void", + "Arguments": { + "player": "BasePlayer" + } + }, + "CodeAfterInjection": "private void OnPlayerRespawned(BasePlayer basePlayer)\r\n{\r\n\tIPlayer player = basePlayer.IPlayer;\r\n\tif (player != null)\r\n\t{\r\n\t\tInterface.CallHook(\"OnUserRespawned\", player);\r\n\t}\r\n}" + }, + { + "Type": 0, + "Name": "OnRconMessage", + "HookName": "OnRconMessage", + "HookParameters": { + "ipAddress": "IPAddress", + "message": "RemoteMessage" + }, + "ReturnBehavior": 1, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "IOnRconMessage", + "ReturnType": "object", + "Arguments": { + "ipAddress": "IPAddress", + "command": "string" + } + }, + "CodeAfterInjection": "private object IOnRconMessage(IPAddress ipAddress, string command)\r\n{\r\n\tif (ipAddress != null && !string.IsNullOrEmpty(command))\r\n\t{\r\n\t\tRemoteMessage message = RemoteMessage.GetMessage(command);\r\n\r\n\t\tif (string.IsNullOrEmpty(message?.Message))\r\n\t\t{\r\n\t\t\treturn null;\r\n\t\t}\r\n\r\n\t\tif (Interface.CallHook(\"OnRconMessage\", ipAddress, message) != null)\r\n\t\t{\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\tstring[] fullCommand = CommandLine.Split(message.Message);\r\n\r\n\t\tif (fullCommand.Length >= 1)\r\n\t\t{\r\n\t\t\tstring cmd = fullCommand[0].ToLower();\r\n\t\t\tstring[] args = fullCommand.Skip(1).ToArray();\r\n\r\n\t\t\tif (Interface.CallHook(\"OnRconCommand\", ipAddress, cmd, args) != null)\r\n\t\t\t{\r\n\t\t\t\treturn true;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\treturn null;\r\n}" + }, + { + "Type": 0, + "Name": "OnRconCommand", + "HookName": "OnRconCommand", + "HookParameters": { + "ipAddress": "IPAddress", + "message": "string", + "arg": "string[]" + }, + "ReturnBehavior": 1, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "IOnRconMessage", + "ReturnType": "object", + "Arguments": { + "ipAddress": "IPAddress", + "command": "string" + } + }, + "CodeAfterInjection": "private object IOnRconMessage(IPAddress ipAddress, string command)\r\n{\r\n\tif (ipAddress != null && !string.IsNullOrEmpty(command))\r\n\t{\r\n\t\tRemoteMessage message = RemoteMessage.GetMessage(command);\r\n\r\n\t\tif (string.IsNullOrEmpty(message?.Message))\r\n\t\t{\r\n\t\t\treturn null;\r\n\t\t}\r\n\r\n\t\tif (Interface.CallHook(\"OnRconMessage\", ipAddress, message) != null)\r\n\t\t{\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\tstring[] fullCommand = CommandLine.Split(message.Message);\r\n\r\n\t\tif (fullCommand.Length >= 1)\r\n\t\t{\r\n\t\t\tstring cmd = fullCommand[0].ToLower();\r\n\t\t\tstring[] args = fullCommand.Skip(1).ToArray();\r\n\r\n\t\t\tif (Interface.CallHook(\"OnRconCommand\", ipAddress, cmd, args) != null)\r\n\t\t\t{\r\n\t\t\t\treturn true;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\treturn null;\r\n}" + }, + { + "Type": 0, + "Name": "OnServerCommand", + "HookName": "OnServerCommand", + "HookParameters": { + "arg": "ConsoleSystem.Arg" + }, + "ReturnBehavior": 1, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "object IOnServerCommand(ConsoleSystem.Arg arg)", + "ReturnType": "object", + "Arguments": { + "arg": "ConsoleSystem.Arg" + } + }, + "CodeAfterInjection": "private object IOnServerCommand(ConsoleSystem.Arg arg)\r\n{\r\n\t// Ignore invalid connections\r\n\tif (arg == null || arg.Connection != null && arg.Player() == null)\r\n\t{\r\n\t\treturn true;\r\n\t}\r\n\r\n\t// Ignore all chat messages\r\n\tif (arg.cmd.FullName == \"chat.say\" || arg.cmd.FullName == \"chat.teamsay\" || arg.cmd.FullName == \"chat.localsay\")\r\n\t{\r\n\t\treturn null;\r\n\t}\r\n\r\n\t// Is the command blocked?\r\n\tobject commandSpecific = Interface.CallHook(\"OnServerCommand\", arg);\r\n\tobject commandCovalence = Interface.CallHook(\"OnServerCommand\", arg.cmd.FullName, RustCommandSystem.ExtractArgs(arg));\r\n\tobject canBlock = commandSpecific is null ? commandCovalence : commandSpecific;\r\n\tif (canBlock != null)\r\n\t{\r\n\t\treturn true;\r\n\t}\r\n\r\n\treturn null;\r\n}" + }, + { + "Type": 0, + "Name": "OnServerCommand", + "HookName": "OnServerCommand", + "HookParameters": { + "cmd": "string", + "arg": "string[]" + }, + "ReturnBehavior": 1, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "object IOnServerCommand(ConsoleSystem.Arg arg)", + "ReturnType": "object", + "Arguments": { + "arg": "ConsoleSystem.Arg" + } + }, + "CodeAfterInjection": "private object IOnServerCommand(ConsoleSystem.Arg arg)\r\n{\r\n\t// Ignore invalid connections\r\n\tif (arg == null || arg.Connection != null && arg.Player() == null)\r\n\t{\r\n\t\treturn true;\r\n\t}\r\n\r\n\t// Ignore all chat messages\r\n\tif (arg.cmd.FullName == \"chat.say\" || arg.cmd.FullName == \"chat.teamsay\" || arg.cmd.FullName == \"chat.localsay\")\r\n\t{\r\n\t\treturn null;\r\n\t}\r\n\r\n\t// Is the command blocked?\r\n\tobject commandSpecific = Interface.CallHook(\"OnServerCommand\", arg);\r\n\tobject commandCovalence = Interface.CallHook(\"OnServerCommand\", arg.cmd.FullName, RustCommandSystem.ExtractArgs(arg));\r\n\tobject canBlock = commandSpecific is null ? commandCovalence : commandSpecific;\r\n\tif (canBlock != null)\r\n\t{\r\n\t\treturn true;\r\n\t}\r\n\r\n\treturn null;\r\n}" + }, + { + "Type": 0, + "Name": "OnCupboardAuthorize", + "HookName": "OnCupboardAuthorize", + "HookParameters": { + "privlidge": "BuildingPrivlidge", + "player": "BasePlayer" + }, + "ReturnBehavior": 1, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "IOnCupboardAuthorize", + "ReturnType": "object", + "Arguments": { + "userID": "ulong", + "player": "BasePlayer", + "privlidge": "BuildingPrivlidge" + } + }, + "CodeAfterInjection": "private object IOnCupboardAuthorize(ulong userID, BasePlayer player, BuildingPrivlidge privlidge)\r\n{\r\n\tif (userID == player.userID)\r\n\t{\r\n\t\tif (Interface.CallHook(\"OnCupboardAuthorize\", privlidge, player) != null)\r\n\t\t{\r\n\t\t\treturn true;\r\n\t\t}\r\n\t}\r\n\telse if (Interface.CallHook(\"OnCupboardAssign\", privlidge, userID, player) != null)\r\n\t{\r\n\t\treturn true;\r\n\t}\r\n\r\n\treturn null;\r\n}" + }, + { + "Type": 0, + "Name": "OnCupboardAssign", + "HookName": "OnCupboardAssign", + "HookParameters": { + "privlidge": "BuildingPrivlidge", + "userID": "ulong", + "player": "BasePlayer" + }, + "ReturnBehavior": 1, + "TargetType": "RustCore", + "Category": "Oxide.Rust", + "HookDescription": "", + "MethodData": { + "MethodName": "IOnCupboardAuthorize", + "ReturnType": "object", + "Arguments": { + "userID": "ulong", + "player": "BasePlayer", + "privlidge": "BuildingPrivlidge" + } + }, + "CodeAfterInjection": "private object IOnCupboardAuthorize(ulong userID, BasePlayer player, BuildingPrivlidge privlidge)\r\n{\r\n\tif (userID == player.userID)\r\n\t{\r\n\t\tif (Interface.CallHook(\"OnCupboardAuthorize\", privlidge, player) != null)\r\n\t\t{\r\n\t\t\treturn true;\r\n\t\t}\r\n\t}\r\n\telse if (Interface.CallHook(\"OnCupboardAssign\", privlidge, userID, player) != null)\r\n\t{\r\n\t\treturn true;\r\n\t}\r\n\r\n\treturn null;\r\n}" + } + ] +} \ No newline at end of file From f5ee857ac3604fe5b5b16f986b53c58ea9e8862f Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Tue, 8 Oct 2024 22:59:16 -0400 Subject: [PATCH 03/27] More flexible Docs json file to support multiple .JSON files --- util/hooks.ts | 36 +++++++++++++----------------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/util/hooks.ts b/util/hooks.ts index 5d3bf2c..197d551 100644 --- a/util/hooks.ts +++ b/util/hooks.ts @@ -12,34 +12,24 @@ export function getHookJson(filename: string) { } export function getGroupedHooks() { - const hooksJson = getHookJson("docs.json"); + const docsNames: string[] = ["docs.json", "docs_core.json"]; var out = {} as { [key: string]: { [key: string]: IHook[] } }; - hooksJson.forEach((hook) => { - if (!out[hook.Category]) { - out[hook.Category] = {}; - } + for (let filename of docsNames) { + const hooksJson = getHookJson(filename); + hooksJson.forEach((hook) => { + if (!out[hook.Category]) { + out[hook.Category] = {}; + } - if (!out[hook.Category][hook.HookName]) { - out[hook.Category][hook.HookName] = []; - } + if (!out[hook.Category][hook.HookName]) { + out[hook.Category][hook.HookName] = []; + } - out[hook.Category][hook.HookName].push(hook); - }); - - const hooksCoreJson = getHookJson("docs_core.json"); - hooksCoreJson.forEach((hook) => { - if (!out[hook.Category]) { - out[hook.Category] = {}; - } - - if (!out[hook.Category][hook.HookName]) { - out[hook.Category][hook.HookName] = []; - } - - out[hook.Category][hook.HookName].push(hook); - }); + out[hook.Category][hook.HookName].push(hook); + }); + } // Sort categories, hooks and hooks by TargetType and MethodData.MethodName using tolocaleCompare Object.keys(out).forEach((category) => { From 0a854f9f26b6b53f553941f513f9a0db7fb17a23 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Tue, 8 Oct 2024 23:23:01 -0400 Subject: [PATCH 04/27] - --- util/hooks.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/util/hooks.ts b/util/hooks.ts index 197d551..abcfa79 100644 --- a/util/hooks.ts +++ b/util/hooks.ts @@ -17,18 +17,18 @@ export function getGroupedHooks() { var out = {} as { [key: string]: { [key: string]: IHook[] } }; for (let filename of docsNames) { - const hooksJson = getHookJson(filename); - hooksJson.forEach((hook) => { - if (!out[hook.Category]) { - out[hook.Category] = {}; - } + const hooksJson = getHookJson(filename); + hooksJson.forEach((hook) => { + if (!out[hook.Category]) { + out[hook.Category] = {}; + } - if (!out[hook.Category][hook.HookName]) { - out[hook.Category][hook.HookName] = []; - } + if (!out[hook.Category][hook.HookName]) { + out[hook.Category][hook.HookName] = []; + } - out[hook.Category][hook.HookName].push(hook); - }); + out[hook.Category][hook.HookName].push(hook); + }); } // Sort categories, hooks and hooks by TargetType and MethodData.MethodName using tolocaleCompare From 482abeb4a25b66d4dd0a3a49246f1e7b418eda8e Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Tue, 8 Oct 2024 23:27:34 -0400 Subject: [PATCH 05/27] - --- util/hooks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/hooks.ts b/util/hooks.ts index abcfa79..6bfc7ac 100644 --- a/util/hooks.ts +++ b/util/hooks.ts @@ -22,7 +22,7 @@ export function getGroupedHooks() { if (!out[hook.Category]) { out[hook.Category] = {}; } - + if (!out[hook.Category][hook.HookName]) { out[hook.Category][hook.HookName] = []; } From 388d4d9547ef4a52b656f11dd85cf5b156d17e13 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Tue, 15 Oct 2024 20:30:35 -0400 Subject: [PATCH 06/27] Add hook ReturnType Add hook ReturnType info to Docs_core.json file (see oxide.patcher PR and oxide.docs PR related to ReturnType) --- docs_core.json | 191 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 124 insertions(+), 67 deletions(-) diff --git a/docs_core.json b/docs_core.json index a0b0f9e..53fd3e3 100644 --- a/docs_core.json +++ b/docs_core.json @@ -7,9 +7,10 @@ "HookParameters": { "args": "object[]" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "CSharpExtension", - "Category": "Oxide.CSharp", + "Category": "Server", "HookDescription": "Called each frame", "MethodData": { "MethodName": "OnFrame", @@ -25,9 +26,10 @@ "Name": "Loaded", "HookName": "Loaded", "HookParameters": {}, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "CSharpPlugin", - "Category": "Oxide.CSharp", + "Category": "Server", "HookDescription": "Called when a plugin has finished loading\r\nOther plugins may or may not be present, dependant on load order", "MethodData": { "MethodName": "HandleAddedToManager", @@ -41,9 +43,10 @@ "Name": "Unload", "HookName": "Unload", "HookParameters": {}, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "CSharpPlugin", - "Category": "Oxide.CSharp", + "Category": "Server", "HookDescription": "Called when a plugin is being unloaded", "MethodData": { "MethodName": "HandleRemovedFromManager", @@ -62,9 +65,10 @@ "name": "string", "owner": "Plugin" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "Permission", - "Category": "Oxide.Core", + "Category": "Permission", "HookDescription": "Called when a permission has been registered", "MethodData": { "MethodName": "RegisterPermission", @@ -85,9 +89,10 @@ "oldName": "string", "newName": "string" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "Permission", - "Category": "Oxide.Core", + "Category": "Permission", "HookDescription": "Called when a player's stored nickname has been changed", "MethodData": { "MethodName": "UpdateNickname", @@ -106,9 +111,10 @@ "HookParameters": { "name": "string" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "Permission", - "Category": "Oxide.Core", + "Category": "Permission", "HookDescription": "Called when a group has been created successfully", "MethodData": { "MethodName": "CreateGroup", @@ -128,9 +134,10 @@ "HookParameters": { "name": "string" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "Permission", - "Category": "Oxide.Core", + "Category": "Permission", "HookDescription": "Called when a group has been deleted successfully", "MethodData": { "MethodName": "RemoveGroup", @@ -149,9 +156,10 @@ "name": "string", "title": "string" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "Permission", - "Category": "Oxide.Core", + "Category": "Permission", "HookDescription": "Called when a group title has been updated", "MethodData": { "MethodName": "SetGroupTitle", @@ -171,9 +179,10 @@ "name": "string", "title": "string" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "Permission", - "Category": "Oxide.Core", + "Category": "Permission", "HookDescription": "Called when a group title has been updated", "MethodData": { "MethodName": "SetGroupRank", @@ -193,9 +202,10 @@ "name": "string", "parent": "string" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "Permission", - "Category": "Oxide.Core", + "Category": "Permission", "HookDescription": "Called when a group parent has been updated", "MethodData": { "MethodName": "SetGroupParent", @@ -214,9 +224,10 @@ "HookParameters": { "plugin": "Plugin" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "OxideMod", - "Category": "Oxide.Core", + "Category": "Server", "HookDescription": "Called when any plugin has been loaded. Not to be confused with Loaded", "MethodData": { "MethodName": "PluginLoaded", @@ -234,9 +245,10 @@ "HookParameters": { "plugin": "Plugin" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "OxideMod", - "Category": "Oxide.Core", + "Category": "Server", "HookDescription": "Called when any plugin has been unloaded. Not to be confused with Unload", "MethodData": { "MethodName": " UnloadPlugin()", @@ -250,11 +262,12 @@ { "Type": 0, "Name": "Init", - "HookName": " Init", + "HookName": "Init", "HookParameters": {}, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "CSPlugin", - "Category": "Oxide.Core", + "Category": "Server", "HookDescription": "Called when a plugin is being initialized.\r\n Other plugins may or may not be present, dependant on load order", "MethodData": { "MethodName": "HandleAddedToManager", @@ -270,9 +283,10 @@ "Name": "LoadDefaultConfig", "HookName": "LoadDefaultConfig", "HookParameters": {}, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "Plugin", - "Category": "Oxide.Core", + "Category": "Server", "HookDescription": "Called when the config for a plugin should be initialized.\r\n Only called if the config file does not already exist", "MethodData": { "MethodName": "LoadDefaultConfig", @@ -286,9 +300,10 @@ "Name": "LoadDefaultMessages", "HookName": "LoadDefaultMessages", "HookParameters": {}, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "Plugin", - "Category": "Oxide.Core", + "Category": "Server", "HookDescription": "Called when the localization for a plugin should be registered", "MethodData": { "MethodName": "LoadDefaultMessages", @@ -306,9 +321,10 @@ "command": "string", "args": "string[]" }, + "ReturnType": "object", "ReturnBehavior": 1, "TargetType": "RemoteConsole", - "Category": "Oxide.Core", + "Category": "Server", "HookDescription": "Called when a rcon command is received", "MethodData": { "MethodName": "OnMessage", @@ -329,9 +345,10 @@ "player": "BasePlayer", "userId": "ulong" }, + "ReturnType": "object", "ReturnBehavior": 1, "TargetType": "Player", - "Category": "Oxide.Rust", + "Category": "Server", "HookDescription": "Called when a message is sent to a player", "MethodData": { "MethodName": "Message", @@ -353,9 +370,10 @@ "HookParameters": { "serverInitialized": "bool" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Server", "HookDescription": "Let plugins know server startup is complete", "MethodData": { "MethodName": "OnPluginLoaded", @@ -373,9 +391,10 @@ "HookParameters": { "serverInitialized": "bool" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Server", "HookDescription": "Let plugins know server startup is complete", "MethodData": { "MethodName": "IOnServerInitialized", @@ -389,9 +408,10 @@ "Name": "OnServerShutdown", "HookName": "OnServerShutdown", "HookParameters": {}, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Server", "HookDescription": "Server is shutting down", "MethodData": { "MethodName": "IOnServerShutdown", @@ -408,9 +428,10 @@ "player": "BasePlayer", "json": "string" }, - "ReturnBehavior": 0, + "ReturnType": "object", + "ReturnBehavior": 1, "TargetType": "CUIHelper", - "Category": "Oxide.Rust", + "Category": "Server", "HookDescription": "", "MethodData": { "MethodName": "AddUi", @@ -430,9 +451,10 @@ "player": "BasePlayer", "elem": "string" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "CUIHelper", - "Category": "Oxide.Rust", + "Category": "Server", "HookDescription": "", "MethodData": { "MethodName": "DestroyUi", @@ -452,9 +474,10 @@ "entity": "BaseCombatEntity", "hitInfo": "HitInfo" }, + "ReturnType": "object", "ReturnBehavior": 1, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Entity", "HookDescription": "", "MethodData": { "MethodName": "IOnBaseCombatEntityHurt", @@ -474,9 +497,10 @@ "npc": "BaseNpc", "target": "BaseEntity" }, + "ReturnType": "object", "ReturnBehavior": 1, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Entity", "HookDescription": "", "MethodData": { "MethodName": "IOnNpcTarget", @@ -496,9 +520,10 @@ "baseNetworkable": "BaseNetworkable", "saveInfo": "BaseNetworkable.SaveInfo" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Entity", "HookDescription": "", "MethodData": { "MethodName": "IOnEntitySaved", @@ -518,9 +543,10 @@ "item": "Item", "amount": "ref float" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Item", "HookDescription": "Called right before the condition of the item is modified", "MethodData": { "MethodName": "IOnLoseCondition", @@ -540,12 +566,13 @@ "basePlayer": "BasePlayer", "entity": "DoorCloser" }, + "ReturnType": "bool", "ReturnBehavior": 3, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Entity", "HookDescription": "Called when a player attempts to pickup a deployed entity (AutoTurret, BaseMountable, BearTrap, DecorDeployable, Door, DoorCloser, ReactiveTarget, SamSite, SleepingBag, SpinnerWheel, StorageContainer, etc.)", "MethodData": { - "MethodName": " ICanPickupEntity", + "MethodName": "ICanPickupEntity", "ReturnType": "object", "Arguments": { "basePlayer": "BasePlayer", @@ -562,9 +589,10 @@ "basePlayer": "BasePlayer", "hitInfo": "HitInfo" }, + "ReturnType": "object", "ReturnBehavior": 1, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Entity", "HookDescription": "Alternatively, modify the HitInfo object to change the damage.\r\n It should be okay to set the damage to 0, but if you don't return non-null, the player's client will receive a damage indicator (if entity is a BasePlayer).\r\n HitInfo has all kinds of useful things in it, such as Weapon, damageProperties or damageTypes", "MethodData": { "MethodName": "IOnBasePlayerAttacked", @@ -584,9 +612,10 @@ "basePlayer": "BasePlayer", "hitInfo": "HitInfo" }, + "ReturnType": "object", "ReturnBehavior": 1, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Entity", "HookDescription": "", "MethodData": { "MethodName": "IOnBasePlayerHurt", @@ -609,9 +638,10 @@ "reason": "string", "expiry": "long" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Server", "HookDescription": "", "MethodData": { "MethodName": "OnServerUserSet", @@ -637,9 +667,10 @@ "reason": "string", "expiry": "long" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Server", "HookDescription": "", "MethodData": { "MethodName": "OnServerUserSet", @@ -663,9 +694,10 @@ "steamId": "ulong", "address": "string" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Server", "HookDescription": "", "MethodData": { "MethodName": "OnServerUserRemove", @@ -685,9 +717,10 @@ "steamId": "string", "address": "string" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Server", "HookDescription": "", "MethodData": { "MethodName": "OnServerUserRemove", @@ -705,12 +738,13 @@ "HookParameters": { "connection": "Connection" }, + "ReturnType": "bool", "ReturnBehavior": 3, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Server", "HookDescription": "Called when a player attempt to login the server", "MethodData": { - "MethodName": " IOnUserApprove", + "MethodName": "IOnUserApprove", "ReturnType": "object", "Arguments": { "connection": "Connection" @@ -727,12 +761,13 @@ "connectionId": "string", "connectionIp": "string" }, + "ReturnType": "bool", "ReturnBehavior": 3, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Server", "HookDescription": "Called when a player attempt to login the server", "MethodData": { - "MethodName": " IOnUserApprove", + "MethodName": "IOnUserApprove", "ReturnType": "object", "Arguments": { "connection": "Connection" @@ -748,9 +783,10 @@ "connection": "Connection", "status": "string" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Server", "HookDescription": "", "MethodData": { "MethodName": "IOnPlayerBanned", @@ -772,9 +808,10 @@ "message": "string", "channel": "Chat.ChatChannel" }, - "ReturnBehavior": 0, + "ReturnType": "object", + "ReturnBehavior": 1, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Player", "HookDescription": "Hook is called when a chat message is sent to an offline player ", "MethodData": { "MethodName": "IOnPlayerChat(ulong playerId, string playerName, string message, Chat.ChatChannel channel, BasePlayer basePlayer)", @@ -798,9 +835,10 @@ "message": "string", "channel": "Chat.ChatChannel" }, - "ReturnBehavior": 0, + "ReturnType": "object", + "ReturnBehavior": 1, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Player", "HookDescription": "", "MethodData": { "MethodName": "IOnPlayerChat(ulong playerId, string playerName, string message, Chat.ChatChannel channel, BasePlayer basePlayer)", @@ -823,9 +861,10 @@ "iplayer": "IPlayer", "message": "string" }, - "ReturnBehavior": 0, + "ReturnType": "object", + "ReturnBehavior": 1, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Player", "HookDescription": "", "MethodData": { "MethodName": "IOnPlayerChat(ulong playerId, string playerName, string message, Chat.ChatChannel channel, BasePlayer basePlayer)", @@ -849,9 +888,10 @@ "cmd": "string", "args": "string[]" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Server", "HookDescription": "", "MethodData": { "MethodName": "TryRunPlayerCommand", @@ -873,9 +913,10 @@ "cmd": "string", "args": "string[]" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Server", "HookDescription": "", "MethodData": { "MethodName": "TryRunPlayerCommand", @@ -897,9 +938,10 @@ "cmd": "string", "args": "string[]" }, - "ReturnBehavior": 0, + "ReturnType": "object", + "ReturnBehavior": 1, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Player", "HookDescription": "", "MethodData": { "MethodName": "TryRunPlayerCommand", @@ -921,9 +963,10 @@ "cmd": "string", "args": "string[]" }, - "ReturnBehavior": 0, + "ReturnType": "object", + "ReturnBehavior": 1, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Player", "HookDescription": "", "MethodData": { "MethodName": "TryRunPlayerCommand", @@ -943,9 +986,10 @@ "HookParameters": { "iplayer": "IPlayer" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Player", "HookDescription": "", "MethodData": { "MethodName": "IOnPlayerConnected", @@ -963,9 +1007,10 @@ "HookParameters": { "player": "BasePlayer" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Player", "HookDescription": "", "MethodData": { "MethodName": "IOnPlayerConnected", @@ -984,9 +1029,10 @@ "iplayer": "IPlayer", "reason": "string" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Player", "HookDescription": "", "MethodData": { "MethodName": "OnPlayerDisconnected", @@ -1006,9 +1052,10 @@ "player": "BasePlayer", "val": "string" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Player", "HookDescription": "", "MethodData": { "MethodName": "OnPlayerSetInfo", @@ -1029,9 +1076,10 @@ "iplayer": "IPlayer", "val": "string" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Player", "HookDescription": "", "MethodData": { "MethodName": "OnPlayerSetInfo", @@ -1052,9 +1100,10 @@ "iplayer": "IPlayer", "reason": "string" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Server", "HookDescription": "", "MethodData": { "MethodName": "OnPlayerKicked", @@ -1073,9 +1122,10 @@ "HookParameters": { "iplayer": "IPlayer" }, + "ReturnType": "object", "ReturnBehavior": 1, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Player", "HookDescription": "", "MethodData": { "MethodName": "object OnPlayerRespawn", @@ -1093,9 +1143,10 @@ "HookParameters": { "iplayer": "IPlayer" }, + "ReturnType": "void", "ReturnBehavior": 0, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Player", "HookDescription": "", "MethodData": { "MethodName": "OnPlayerRespawned", @@ -1114,9 +1165,10 @@ "ipAddress": "IPAddress", "message": "RemoteMessage" }, + "ReturnType": "object", "ReturnBehavior": 1, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Server", "HookDescription": "", "MethodData": { "MethodName": "IOnRconMessage", @@ -1137,9 +1189,10 @@ "message": "string", "arg": "string[]" }, + "ReturnType": "object", "ReturnBehavior": 1, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Server", "HookDescription": "", "MethodData": { "MethodName": "IOnRconMessage", @@ -1158,9 +1211,10 @@ "HookParameters": { "arg": "ConsoleSystem.Arg" }, + "ReturnType": "object", "ReturnBehavior": 1, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Server", "HookDescription": "", "MethodData": { "MethodName": "object IOnServerCommand(ConsoleSystem.Arg arg)", @@ -1179,9 +1233,10 @@ "cmd": "string", "arg": "string[]" }, + "ReturnType": "object", "ReturnBehavior": 1, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Server", "HookDescription": "", "MethodData": { "MethodName": "object IOnServerCommand(ConsoleSystem.Arg arg)", @@ -1200,9 +1255,10 @@ "privlidge": "BuildingPrivlidge", "player": "BasePlayer" }, + "ReturnType": "object", "ReturnBehavior": 1, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Entity", "HookDescription": "", "MethodData": { "MethodName": "IOnCupboardAuthorize", @@ -1224,9 +1280,10 @@ "userID": "ulong", "player": "BasePlayer" }, + "ReturnType": "object", "ReturnBehavior": 1, "TargetType": "RustCore", - "Category": "Oxide.Rust", + "Category": "Entity", "HookDescription": "", "MethodData": { "MethodName": "IOnCupboardAuthorize", From 308413a0a63f6eae5c3256f309c41cb4758de14c Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Tue, 15 Oct 2024 20:49:56 -0400 Subject: [PATCH 07/27] Add Hook ReturnType Note: Was unable to create a second Fork for this one, so adding to previous PR --- entities/hooks/hook.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/entities/hooks/hook.d.ts b/entities/hooks/hook.d.ts index 0753e47..cc3ee54 100644 --- a/entities/hooks/hook.d.ts +++ b/entities/hooks/hook.d.ts @@ -6,6 +6,7 @@ export default interface IHook { HookName: string; HookDescription?: string; HookParameters?: {[key: string]: string}; + ReturnType?: string; ReturnBehavior: ReturnBehaviour; TargetType: string; Category: string; From ce580482816c3ab58e193ee1651dda7fe7441ac7 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Tue, 15 Oct 2024 20:50:43 -0400 Subject: [PATCH 08/27] Add Hook ReturnType Note: Was unable to create a second Fork for this one, so adding to previous PR --- docs/hooks/[category]/[hook].paths.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/hooks/[category]/[hook].paths.ts b/docs/hooks/[category]/[hook].paths.ts index c8389ca..25fca5e 100644 --- a/docs/hooks/[category]/[hook].paths.ts +++ b/docs/hooks/[category]/[hook].paths.ts @@ -96,7 +96,7 @@ function getHookDescription(hooks: IHook[]) { function getHookAlerts(hooks: IHook[]) { const output = []; - if (hooks.some((hook) => hook.HookName.startsWith("I"))) + if (hooks.some((hook) => hook.HookName.startsWith("IOn"))) { output.push(` ::: warning @@ -148,9 +148,10 @@ function getExamplesMarkdown(hooks: IHook[]) { }, [] as IHook[]); for (const hook of hooks) { + let returnType = (hook.ReturnType!=null) ? hook.ReturnType : "void"; output += `\`\`\`csharp`; //TODO: Use proper return type instead of void - output += `\nprivate void ${hook.HookName}( ${getArgumentString(hook.HookParameters)} )`; + output += `\nprivate ${returnType} ${hook.HookName}( ${getArgumentString(hook.HookParameters)} )`; output += `\n{`; output += `\n Puts( "${hook.HookName} works!" );`; output += `\n}`; From be3e7dab224864e71f456e325c642dbe5ea4408e Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Sun, 29 Dec 2024 11:11:44 -0500 Subject: [PATCH 09/27] missing return line in sample code, when return value is not void --- docs/hooks/[category]/[hook].paths.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/hooks/[category]/[hook].paths.ts b/docs/hooks/[category]/[hook].paths.ts index 25fca5e..aad8f48 100644 --- a/docs/hooks/[category]/[hook].paths.ts +++ b/docs/hooks/[category]/[hook].paths.ts @@ -154,6 +154,7 @@ function getExamplesMarkdown(hooks: IHook[]) { output += `\nprivate ${returnType} ${hook.HookName}( ${getArgumentString(hook.HookParameters)} )`; output += `\n{`; output += `\n Puts( "${hook.HookName} works!" );`; + if (returnType!="void") output += `\n return null;`; output += `\n}`; output += `\n\`\`\`\n`; } From 54cde9e21c5ac6fa78eed6cddb89868e90e03ffb Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Thu, 9 Jan 2025 21:32:12 -0500 Subject: [PATCH 10/27] Add documentation for Attributes and Timers --- docs/guides/developers/Attributes.md | 108 ++++++++++++++++++++ docs/guides/developers/Timers.md | 79 ++++++++++++++ docs/guides/developers/plugin-guidelines.md | 2 +- 3 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 docs/guides/developers/Attributes.md create mode 100644 docs/guides/developers/Timers.md diff --git a/docs/guides/developers/Attributes.md b/docs/guides/developers/Attributes.md new file mode 100644 index 0000000..44210fb --- /dev/null +++ b/docs/guides/developers/Attributes.md @@ -0,0 +1,108 @@ +--- +title: Attributes +after: my-first-plugin +--- +# Attributes +## Commands + +Custom commands are easily implemented with minimal boilerplate for both in-game chat interfaces and conventional command-line interfaces. +## Chat commands +Chat commands are in-game commands entered via the game client's chat, prefixed by a forward slash (/). + +when using covalence +```csharp +[Command("test")] +private void TestCommand(IPlayer player, string command, string[] args) +{ + player.Reply("Test successful!"); +} +``` +when using Rustplugin +```csharp +[ChatCommand("test")] +void cmdTest (BasePlayer player, string command, string [] args) +{ + Puts("Test successful!"); +} +``` +## Console commands +Console commands may be executed from the server console and in-game interfaces F1 (where applicable). + +when using covalence +```csharp +[Command("test")] +private void TestCommand(IPlayer player, string command, string[] args) +{ + player.Reply("Test successful!"); +} +``` +when using Rustplugin +```csharp +[ConsoleCommand("test")] +private void cmdTest((ConsoleSystem.Arg arg)) +{ + Puts("Test successful!"); +} +``` +## Command permissions +Easily restrict command usage to players who have a permission assigned to them. +```csharp +[Command("test"), Permission("epicstuff.use")] +private void TestCommand(IPlayer player, string command, string[] args) +{ + player.Reply("Test successful!"); +} +``` +## Info +Information about the plugin. plugin name (with spaces between words), Developper or maintainer name, and a 3 digit version number. +```csharp +[Info("Plugin name", "Developper/Maintainer", "1.0.0")] +``` +## Description +A short description of what the plugin does +```csharp +[Description("A short description of the plugin")] +``` +## PluginReference +Reference to other plugin, when this plugin need to use functions from other plugins. + +```csharp +[PluginReference] private Plugin Vanish, Backpacks; +``` +Note: when a plugin is required by this plugin, this line should appear at the top. +```csharp +//Requires: Backpacks +``` +If required plugin is absent from plugin folder, this plugin will not start + +## OnlinePlayers +Auto manage an Hashtable of online players. + +see Bank and Trade plugin for usage example +```csharp +class OnlinePlayer +{ + public BasePlayer Player; + public OnlinePlayer (BasePlayer player) + { + } +} + +[OnlinePlayers] +Hash onlinePlayers = new Hash (); +``` +# AutoPatch +Used with HarmonyPatch to automatically install harmony patch when plugin start, and uninstall when plugin terminate +```csharp +[AutoPatch] +[HarmonyPatch(typeof(Planner), "DoPlace")] +static class DoPlace_Process +{ + [HarmonyPrefix] + private static bool Prefix() + { + UnityEngine.Debug.Log($"[Harmony] Planner DoPlace "); + return true; + } +} +``` diff --git a/docs/guides/developers/Timers.md b/docs/guides/developers/Timers.md new file mode 100644 index 0000000..4db7616 --- /dev/null +++ b/docs/guides/developers/Timers.md @@ -0,0 +1,79 @@ +--- +title: Timers +after: Attributes +--- +# Timers + +Timers generally execute functions after a set interval. Optionally continuous, repeating, and immediate timers are also available. +```csharp +PluginTimers pluginTimer; +pluginTimer = new PluginTimers(this); +``` +## Single timer +Executes a function once after the specified delay interval. +```csharp +Timer myTimer = pluginTimer.Once(1f, () => +{ + Puts("Hello world!"); +}); +``` +## Continuous timer + +Executes a function at the specified delay interval (until the timer is manually destroyed or plugin is unloaded). +```csharp +Timer myTimer = pluginTimer.Every(3f, () => +{ + Puts("Hello world!"); +}); +``` +## Repeating timer + +Executes a function a specific number of times at the specified delay interval. If the number of recurrences is not specified (0), then a repeating timer behaves identically to a continuous timer. +```csharp +Timer myTimer = pluginTimer.Repeat(5f, 0, () => +{ + Puts("Hello world!"); +}); +``` +## Immediate timer + +Executes a function immediately (in the next frame). +```csharp +NextFrame(() => +{ + Puts("Hello world!"); +}); +``` +or +```csharp +NextTick(() => +{ + Puts("Hello world!"); +}); +``` +Note: both method call Interface.Oxide.NextTick(callback); +## Destroying timer + +When a timer is no longer operating, it is marked as destroyed. Additionally timers may be destroyed manually if stored in a variable. +```csharp +Timer myTimer = pluginTimer.Every(3f, () => +{ + Puts("Hello world!"); +}); +``` +```csharp +myTimer.DestroyToPool(); +if (myTimer?.Destroyed ?? true) +{ + Puts("Timer destroyed!"); +} +``` +or +```csharp +pluginTimer.Destroy(ref myTimer); +if (myTimer?.Destroyed ?? true) +{ + Puts("Timer destroyed!"); +} +``` +Note: can also use myTimer.Destroy(); but Destroy() does not put timer back in the pool vs the other two methods use pooling \ No newline at end of file diff --git a/docs/guides/developers/plugin-guidelines.md b/docs/guides/developers/plugin-guidelines.md index b904975..7b4af71 100644 --- a/docs/guides/developers/plugin-guidelines.md +++ b/docs/guides/developers/plugin-guidelines.md @@ -1,6 +1,6 @@ --- title: Plugin Guidelines -after: my-first-plugin +after: Timers --- # Plugin Guidelines From 83950b40ada41bdee36bc9f7a117a8024d0ebb98 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Thu, 9 Jan 2025 22:30:56 -0500 Subject: [PATCH 11/27] Add Permissions.md, Database.md, WebRequest.md --- docs/guides/developers/Attributes.md | 3 +- docs/guides/developers/Database.md | 103 ++++++++++++++++ docs/guides/developers/Permissions.md | 126 ++++++++++++++++++++ docs/guides/developers/Timers.md | 2 +- docs/guides/developers/WebRequests.md | 122 +++++++++++++++++++ docs/guides/developers/plugin-guidelines.md | 2 +- 6 files changed, 355 insertions(+), 3 deletions(-) create mode 100644 docs/guides/developers/Database.md create mode 100644 docs/guides/developers/Permissions.md create mode 100644 docs/guides/developers/WebRequests.md diff --git a/docs/guides/developers/Attributes.md b/docs/guides/developers/Attributes.md index 44210fb..835d49b 100644 --- a/docs/guides/developers/Attributes.md +++ b/docs/guides/developers/Attributes.md @@ -1,6 +1,6 @@ --- title: Attributes -after: my-first-plugin +after: WebRequests --- # Attributes ## Commands @@ -106,3 +106,4 @@ static class DoPlace_Process } } ``` +Note: see harmony documentation for info about harmony patches \ No newline at end of file diff --git a/docs/guides/developers/Database.md b/docs/guides/developers/Database.md new file mode 100644 index 0000000..8320c5c --- /dev/null +++ b/docs/guides/developers/Database.md @@ -0,0 +1,103 @@ +--- +title: Database +after: Permissions +--- +# Database + +The Oxide database extensions implement a generalized database abstraction layer for both MySQL and SQLite. +## Open a connection + +Create a new connection to a database by providing the database file location or an address (URI and port), a database name, and authentication credentials. +```csharp +Core.MySql.Libraries.MySql sqlLibrary = Interface.Oxide.GetLibrary(); +Connection sqlConnection = sqlLibrary.OpenDb("localhost", 3306, "umod", "username", "password", this); +``` +## Close the connection + +Close an existing connection to the database. +```csharp +sqlLibrary.CloseDb(sqlConnection); +``` +## Query the database + +Retrieve data from the database, typically using a SELECT statement. +```csharp +string sqlQuery = "SELECT `id`, `field1`, `field2` FROM example_table"; +Sql selectCommand = Oxide.Core.Database.Sql.Builder.Append(sqlQuery); + +sqlLibrary.Query(selectCommand, sqlConnection, list => +{ + if (list == null) + { + return; // Empty result or no records found + } + + StringBuilder newString = new StringBuilder(); + newString.AppendLine(" id\tfield1\tfield2"); + + // Iterate through resulting records + foreach (Dictionary entry in list) + { + newString.AppendFormat(" {0}\t{1}\t{2}\n", entry["id"], entry["field1"], entry["field2"]); + } + + Puts(newString.ToString()); +}); +``` +## Insert query + +Insert records into the database using an INSERT statement. +```csharp +string sqlQuery = "INSERT INTO example_table (`field1`, `field2`) VALUES (@0, @1);"; +Sql insertCommand = Oxide.Core.Database.Sql.Builder.Append(sqlQuery, "field1 value", "field2 value"); + +sqlLibrary.Insert(insertCommand, sqlConnection, rowsAffected => +{ + if (rowsAffected > 0) + { + Puts("New record inserted with ID: {0}", sqlConnection.LastInsertRowId); + } +}); +``` +## Update query + +Update existing records in the database using an UPDATE statement. +```csharp +int exampleId = 2; +string sqlQuery = "UPDATE example_table SET `field1` = @0, `field2` = @1 WHERE `id` = @2;"; +Sql updateCommand = Oxide.Core.Database.Sql.Builder.Append(sqlQuery, "field1 value", "field2 value", exampleId); + +sqlLibrary.Update(updateCommand, sqlConnection, rowsAffected => +{ + if (rowsAffected > 0) + { + Puts("Record successfully updated!"); + } +}); +``` +## Delete query + +Delete existing records from a database using a DELETE statement. +```csharp +int exampleId = 2; +string sqlQuery = "DELETE FROM example_table WHERE `id` = @0;"; +Sql deleteCommand = Oxide.Core.Database.Sql.Builder.Append(sqlQuery, exampleId); + +sqlLibrary.Delete(deleteCommand, sqlConnection, rowsAffected => +{ + if (rowsAffected > 0) + { + Puts("Record successfully deleted!"); + } +}); +``` +## Non-query + +By definition a non-query is a query which modifies data and does not retrieve data. Insert, Update, and Delete queries are all considered non-queries. +```csharp +int exampleId = 2; +string sqlQuery = "UPDATE example_table SET `field1` = @0, `field2` = @1 WHERE `id` = @3;"; +Sql sqlCommand = Oxide.Core.Database.Sql.Builder.Append(sqlQuery, "field1 value", "field2 value", exampleId); + +sqlLibrary.ExecuteNonQuery(sqlCommand, sqlConnection); +``` \ No newline at end of file diff --git a/docs/guides/developers/Permissions.md b/docs/guides/developers/Permissions.md new file mode 100644 index 0000000..df6b782 --- /dev/null +++ b/docs/guides/developers/Permissions.md @@ -0,0 +1,126 @@ +--- +title: Permissions +after: Attributes +--- +# Permissions + +Oxide offers a substantial API to control user access with permissions and groups +Basic usage + +For a primer on how to use permissions as a server owner, please consult the Using the Oxide permissions system tutorial. + +Most plugins can benefit from some permissions. Below is a basic example of how to register a permission and check if a player has that permission assigned to them. +```csharp +namespace Oxide.Plugins +{ + [Info("Epic Stuff", "Unknown Author", "0.1.0")] + [Description("Makes epic stuff happen")] + class EpicStuff : CovalencePlugin + { + private void Init() + { + permission.RegisterPermission("epicstuff.use", this); + } + + private void OnUserConnected(IPlayer player) + { + if (player.HasPermission("epicstuff.use")) + { + // Player has permission, do special stuff for them + } + } + } +} +``` +# API +## Groups +Get all groups +```csharp +string[] groups = permission.GetGroups(); +``` +Check if group exists +```csharp +bool GroupExists = permission.GroupExists("GroupName"); +``` +Create a group +```csharp +bool GroupCreated = permission.CreateGroup("GroupName", "Group Title", 0); +``` +Remove a group +```csharp +bool GroupRemoved = permission.RemoveGroup("GroupName"); +``` +Check if group has a permission +```csharp +bool GroupHasPermission = permission.GroupHasPermission("GroupName", "epicstuff.use"); +``` +Grant permission to a group +```csharp +permission.GrantGroupPermission("GroupName", "epicstuff.use", this); +``` +Revoke permission from a group +```csharp +permission.RevokeGroupPermission("GroupName", "epicstuff.use"); +``` +Get the rank for a group +```csharp +int GroupRank = permission.GetGroupRank("GroupName"); +``` +Get the title for a group +```csharp +string GroupTitle = permission.GetGroupTitle("GroupName"); +``` +Get parent group for a group +```csharp +string GroupParent = permission.GetGroupParent("GroupName"); +``` +Get permissions for a group +```csharp +string[] permissions = permission.GetGroupPermissions("GroupName", false); +``` +Migrate group +```csharp +permission.MigrateGroup("OldGroupName", "NewGroupName"); +``` +## Users +Get permissions granted to player +```csharp +string[] UserPermissions = permission.GetUserPermissions("playerID"); +``` +Check if player has a permission +```csharp +bool UserHasPermission = permission.UserHasPermission("playerID", "epicstuff.use"); +``` +Add player to a group +```csharp +permission.AddUserGroup("playerID", "GroupName"); +``` +Remove player from a group +```csharp +permission.RemoveUserGroup("playerID", "GroupName"); +``` +Check if player is in a group +```csharp +bool UserHasGroup = permission.UserHasGroup("playerID", "GroupName"); +``` +Grant permission to a player +```csharp +permission.GrantUserPermission("playerID", "epicstuff.use", this); +``` +Revoke permission from a player +```csharp +permission.RevokeUserPermission("playerID", "epicstuff.use"); +``` +## Server +Get all registered permissions +```csharp +string[] permissions = permission.GetPermissions(); +``` +Check if a permission exists +```csharp +bool PermissionExists = permission.PermissionExists("epicstuff.use", this); +``` +Register a permission +```csharp +permission.RegisterPermission("epicstuff.use", this); +``` \ No newline at end of file diff --git a/docs/guides/developers/Timers.md b/docs/guides/developers/Timers.md index 4db7616..efca0f9 100644 --- a/docs/guides/developers/Timers.md +++ b/docs/guides/developers/Timers.md @@ -1,6 +1,6 @@ --- title: Timers -after: Attributes +after: my-first-plugin --- # Timers diff --git a/docs/guides/developers/WebRequests.md b/docs/guides/developers/WebRequests.md new file mode 100644 index 0000000..9851421 --- /dev/null +++ b/docs/guides/developers/WebRequests.md @@ -0,0 +1,122 @@ +--- +title: Web Requests +after: Timers +--- +# Web Requests + +Make a web request to a URI (Uniform Resource Identifier) using the HTTP GET, POST, or PUT methods. + +Web requests create a raw connection to a web page as done in a web browser. The request will return true if the web request was sent, false if not. + +The callback is called with 2 parameters - an integer HTTP response code and a string response. +## GET web request + +The HTTP GET method is used to retrieve a resource, usually represented as XML or JSON. HTTP status code 200 (OK) is expected in response to a successful GET request. +```csharp +webrequest.Enqueue("http://www.google.com/search?q=umod", null, (code, response) => +{ + if (code != 200 || response == null) + { + Puts($"Couldn't get an answer from Google!"); + return; + } + Puts($"Google answered: {response}"); +}, this, RequestMethod.GET); +``` +## Advanced GET request + +The following example demonstrates how to specify custom request timeout and/or additional headers. +```csharp +[Command("get")] +private void GetRequest(IPlayer player, string command, string[] args) +{ + // Set a custom timeout (in milliseconds) + float timeout = 200f; + + // Set some custom request headers (eg. for HTTP Basic Auth) + Dictionary headers = new Dictionary { { "header", "value" } }; + + webrequest.Enqueue("http://www.google.com/search?q=umod", null, (code, response) => + GetCallback(code, response, player), this, RequestMethod.GET, headers, timeout); +} + +private void GetCallback(int code, string response, IPlayer player) +{ + if (response == null || code != 200) + { + Puts($"Error: {code} - Couldn't get an answer from Google for {player.Name}"); + return; + } + + Puts($"Google answered for {player.Name}: {response}"); +} +``` +## POST web request + +The HTTP POST method is generally used to create new resources. HTTP status code 200 (OK) OR HTTP status code 201 (Created), and an accompanying redirect (to the newly created resource) are expected in response to a successful POST request. +```csharp +webrequest.Enqueue("http://www.google.com/search?q=umod", "param1=value1", (code, response) => +{ + if (code != 200 || response == null) + { + Puts($"Couldn't get an answer from Google!"); + return; + } + Puts($"Google answered: {response}"); +}, this, RequestMethod.POST); +``` +## PUT web request + +The HTTP PUT is generally used to update existing resources. The request body of a PUT request generally contains an updated representation of the original resource. HTTP status code 200 (OK) OR HTTP status code 204 (No Content) are expected in response to a successful PUT request. + +webrequest.Enqueue("http://www.google.com/search?q=umod", null, (code, response) => +{ + if (code != 200 || response == null) + { + Puts($"Couldn't get an answer from Google!"); + return; + } + Puts($"Google answered: {response}"); +}, this, RequestMethod.PUT); + +## POST and PUT body + +Typically an updated resource is represented in a POST/PUT request body as a query string. +```csharp +Dictionary parameters = new Dictionary(); + +parameters.Add("param1", "value1"); +parameters.Add("param2", "value2"); + +string[] body = string.Join("&", parameters.Cast().Select(key => string.Format("{0}={1}", key, source[key])); +webrequest.Enqueue("http://www.google.com/search?q=umod", body, (code, response) => +{ + if (code != 200 || response == null) + { + Puts($"Couldn't get an answer from Google!"); + return; + } + Puts($"Google answered: {response}"); +}, this, RequestMethod.POST); +``` +## Using a method callback + +The following example demonstrates how to refactor delegate behavior by encapsulating it in a separate method, rather than solely using an anonymous function. +```csharp +[Command("get")] +private void GetRequest(IPlayer player, string command, string[] args) +{ + webrequest.EnqueueGet("http://www.google.com/search?q=umod", (code, response) => GetCallback(code, response, player), this); +} + +private void GetCallback(int code, string response, IPlayer player) +{ + if (response == null || code != 200) + { + Puts($"Error: {code} - Couldn't get an answer from Google for {player.Name}"); + return; + } + + Puts($"Google answered for {player.Name}: {response}"); +} +``` \ No newline at end of file diff --git a/docs/guides/developers/plugin-guidelines.md b/docs/guides/developers/plugin-guidelines.md index 7b4af71..c5600b8 100644 --- a/docs/guides/developers/plugin-guidelines.md +++ b/docs/guides/developers/plugin-guidelines.md @@ -1,6 +1,6 @@ --- title: Plugin Guidelines -after: Timers +after: Database --- # Plugin Guidelines From 9042ec12db675395abed6b0e037bdc77c7b4ff9c Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Sun, 12 Jan 2025 16:14:42 -0500 Subject: [PATCH 12/27] fix typo --- docs/guides/developers/Timers.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/docs/guides/developers/Timers.md b/docs/guides/developers/Timers.md index efca0f9..4db6898 100644 --- a/docs/guides/developers/Timers.md +++ b/docs/guides/developers/Timers.md @@ -5,14 +5,11 @@ after: my-first-plugin # Timers Timers generally execute functions after a set interval. Optionally continuous, repeating, and immediate timers are also available. -```csharp -PluginTimers pluginTimer; -pluginTimer = new PluginTimers(this); -``` + ## Single timer Executes a function once after the specified delay interval. ```csharp -Timer myTimer = pluginTimer.Once(1f, () => +Timer myTimer = timer.Once(1f, () => { Puts("Hello world!"); }); @@ -21,7 +18,7 @@ Timer myTimer = pluginTimer.Once(1f, () => Executes a function at the specified delay interval (until the timer is manually destroyed or plugin is unloaded). ```csharp -Timer myTimer = pluginTimer.Every(3f, () => +Timer myTimer = timer.Every(3f, () => { Puts("Hello world!"); }); @@ -30,7 +27,7 @@ Timer myTimer = pluginTimer.Every(3f, () => Executes a function a specific number of times at the specified delay interval. If the number of recurrences is not specified (0), then a repeating timer behaves identically to a continuous timer. ```csharp -Timer myTimer = pluginTimer.Repeat(5f, 0, () => +Timer myTimer = timer.Repeat(5f, 0, () => { Puts("Hello world!"); }); @@ -56,7 +53,7 @@ Note: both method call Interface.Oxide.NextTick(callback); When a timer is no longer operating, it is marked as destroyed. Additionally timers may be destroyed manually if stored in a variable. ```csharp -Timer myTimer = pluginTimer.Every(3f, () => +Timer myTimer = timer.Every(3f, () => { Puts("Hello world!"); }); @@ -70,7 +67,7 @@ if (myTimer?.Destroyed ?? true) ``` or ```csharp -pluginTimer.Destroy(ref myTimer); +timer.Destroy(ref myTimer); if (myTimer?.Destroyed ?? true) { Puts("Timer destroyed!"); From 989b8b6057d94afb578cf51edf020d446abfd137 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Wed, 15 Jan 2025 21:02:09 -0500 Subject: [PATCH 13/27] missing HookMethod (was not sure if available to plugins) --- docs/guides/developers/Attributes.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/guides/developers/Attributes.md b/docs/guides/developers/Attributes.md index 835d49b..7b6f21e 100644 --- a/docs/guides/developers/Attributes.md +++ b/docs/guides/developers/Attributes.md @@ -91,7 +91,14 @@ class OnlinePlayer [OnlinePlayers] Hash onlinePlayers = new Hash (); ``` -# AutoPatch +## HookMethod +Indicates that the specified method should be a handler for a hook +```csharp +[HookMethod("OnPlayerConnected")] +private void base_OnPlayerConnected(BasePlayer player) => AddOnlinePlayer(player); +``` + +## AutoPatch Used with HarmonyPatch to automatically install harmony patch when plugin start, and uninstall when plugin terminate ```csharp [AutoPatch] From cf3cd78aa64b5b633c64f73d5794345cf91eb364 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Mon, 31 Mar 2025 21:58:48 -0400 Subject: [PATCH 14/27] minor adjustment for returnType in Usage --- docs/hooks/[category]/[hook].paths.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/hooks/[category]/[hook].paths.ts b/docs/hooks/[category]/[hook].paths.ts index aad8f48..a7dd166 100644 --- a/docs/hooks/[category]/[hook].paths.ts +++ b/docs/hooks/[category]/[hook].paths.ts @@ -110,24 +110,26 @@ This is an internal hook and will not be called in plugins. See [Internal Hooks] // Get the return behavior of the hook by return behavior type function getUsageReturn(hookData: IHook) { + let returnType = "non-null"; + if (hookData.ReturnType!=null && hookData.ReturnType!="object") returnType = hookData.ReturnType.replace(/`1/g, '>').replace(/\//g, '.'); + if (hookData.ReturnBehavior === ReturnBehaviour.Continue) { - return "* No return behavior"; + return `* No return behavior`; } if (hookData.ReturnBehavior === ReturnBehaviour.ExitWhenValidType) { - return "* Return a non-null value to override default behavior"; + return `* Return a ${returnType} value to override default behavior`; } if (hookData.ReturnBehavior === ReturnBehaviour.ExitWhenNonNull) { - return "* Return a non-null value or bool to override default behavior"; + return `* Return a ${returnType} value to override default behavior`; } - //TODO: Get the return type of the hook if (hookData.ReturnBehavior === ReturnBehaviour.UseArgumentString) { - return "* Return TYPE to prevent default behavior"; + return `* Return type ${returnType} to prevent default behavior`; } - return "* No return behavior"; + return `* No return behavior`; } // Generate example code for the hook @@ -148,7 +150,7 @@ function getExamplesMarkdown(hooks: IHook[]) { }, [] as IHook[]); for (const hook of hooks) { - let returnType = (hook.ReturnType!=null) ? hook.ReturnType : "void"; + let returnType = (hook.ReturnType!=null && hook.ReturnType!="void") ? "object" : "void"; output += `\`\`\`csharp`; //TODO: Use proper return type instead of void output += `\nprivate ${returnType} ${hook.HookName}( ${getArgumentString(hook.HookParameters)} )`; From 871fa2352284139b3fa09d627e6fa22dc31557b2 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Mon, 21 Apr 2025 21:09:40 -0400 Subject: [PATCH 15/27] //reference attribute --- docs/guides/developers/Attributes.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/guides/developers/Attributes.md b/docs/guides/developers/Attributes.md index 7b6f21e..c8b3304 100644 --- a/docs/guides/developers/Attributes.md +++ b/docs/guides/developers/Attributes.md @@ -113,4 +113,11 @@ static class DoPlace_Process } } ``` -Note: see harmony documentation for info about harmony patches \ No newline at end of file +Note: see harmony documentation for info about harmony patches + +## Reference +Add the ablility to reference additionnal DLL files to be used by the plugin +```csharp +//Reference: System.Drawing +``` + From cbad52dcb46cf88387a12b6ed9670963dc29bc11 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Tue, 22 Apr 2025 21:06:02 -0400 Subject: [PATCH 16/27] For test webpage on https://[username].github.io/Oxide.Docs --- .github/workflows/deploy.yml | 66 ++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..7a685e3 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,66 @@ +# Sample workflow for building and deploying a VitePress site to GitHub Pages +# +name: Deploy VitePress site to Pages + +on: + # Runs on pushes targeting the `main` branch. Change this to `master` if you're + # using the `master` branch as the default branch. + push: + branches: [main] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + # Build job + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Not needed if lastUpdated is not enabled + # - uses: pnpm/action-setup@v3 # Uncomment this block if you're using pnpm + # with: + # version: 9 # Not needed if you've set "packageManager" in package.json + # - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm # or pnpm / yarn + - name: Setup Pages + uses: actions/configure-pages@v4 + - name: Install dependencies + run: npm ci # or pnpm install / yarn install / bun install + - name: Build with VitePress + run: npm run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/.vitepress/dist + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + runs-on: ubuntu-latest + name: Deploy + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file From ae189c456b3b522eebe8a99f77a996ba70a725a9 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Tue, 22 Apr 2025 21:32:31 -0400 Subject: [PATCH 17/27] . --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7a685e3..a6953e4 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -50,7 +50,7 @@ jobs: - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: - path: docs/.vitepress/dist + path: .vitepress/dist # Deployment job deploy: From 5712d0457273727b0f724f16431c99dc450ee1d4 Mon Sep 17 00:00:00 2001 From: Lorenzo <114453992+Lorenzo-oo@users.noreply.github.com> Date: Tue, 22 Apr 2025 21:41:24 -0400 Subject: [PATCH 18/27] Delete .github/workflows directory --- .github/workflows/deploy.yml | 66 ------------------------------------ 1 file changed, 66 deletions(-) delete mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index a6953e4..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,66 +0,0 @@ -# Sample workflow for building and deploying a VitePress site to GitHub Pages -# -name: Deploy VitePress site to Pages - -on: - # Runs on pushes targeting the `main` branch. Change this to `master` if you're - # using the `master` branch as the default branch. - push: - branches: [main] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. -concurrency: - group: pages - cancel-in-progress: false - -jobs: - # Build job - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Not needed if lastUpdated is not enabled - # - uses: pnpm/action-setup@v3 # Uncomment this block if you're using pnpm - # with: - # version: 9 # Not needed if you've set "packageManager" in package.json - # - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: npm # or pnpm / yarn - - name: Setup Pages - uses: actions/configure-pages@v4 - - name: Install dependencies - run: npm ci # or pnpm install / yarn install / bun install - - name: Build with VitePress - run: npm run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: .vitepress/dist - - # Deployment job - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - needs: build - runs-on: ubuntu-latest - name: Deploy - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file From 1dd84437d2f6a7e82574c0f2801be375447c6f35 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Tue, 22 Apr 2025 22:53:38 -0400 Subject: [PATCH 19/27] update apr 22 --- docs/guides/developers/Attributes.md | 2 +- docs/guides/developers/Database.md | 2 +- docs/guides/developers/Permissions.md | 2 +- docs/guides/developers/WebRequests.md | 2 +- docs/guides/developers/data-storage.md | 136 ++++++++++++++++++++ docs/guides/developers/plugin-guidelines.md | 2 +- docs/guides/developers/using-decompiler.md | 40 ++++++ 7 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 docs/guides/developers/data-storage.md create mode 100644 docs/guides/developers/using-decompiler.md diff --git a/docs/guides/developers/Attributes.md b/docs/guides/developers/Attributes.md index c8b3304..2571ca4 100644 --- a/docs/guides/developers/Attributes.md +++ b/docs/guides/developers/Attributes.md @@ -1,6 +1,6 @@ --- title: Attributes -after: WebRequests +after: webrequests --- # Attributes ## Commands diff --git a/docs/guides/developers/Database.md b/docs/guides/developers/Database.md index 8320c5c..4f7d92b 100644 --- a/docs/guides/developers/Database.md +++ b/docs/guides/developers/Database.md @@ -1,6 +1,6 @@ --- title: Database -after: Permissions +after: permissions --- # Database diff --git a/docs/guides/developers/Permissions.md b/docs/guides/developers/Permissions.md index df6b782..4985841 100644 --- a/docs/guides/developers/Permissions.md +++ b/docs/guides/developers/Permissions.md @@ -1,6 +1,6 @@ --- title: Permissions -after: Attributes +after: attributes --- # Permissions diff --git a/docs/guides/developers/WebRequests.md b/docs/guides/developers/WebRequests.md index 9851421..864b453 100644 --- a/docs/guides/developers/WebRequests.md +++ b/docs/guides/developers/WebRequests.md @@ -1,6 +1,6 @@ --- title: Web Requests -after: Timers +after: timers --- # Web Requests diff --git a/docs/guides/developers/data-storage.md b/docs/guides/developers/data-storage.md new file mode 100644 index 0000000..5b923ac --- /dev/null +++ b/docs/guides/developers/data-storage.md @@ -0,0 +1,136 @@ +--- +title: Data Storage +after: using-decompiler +--- + +# Data Storage + +## Configuration/user data file + +As previously mentioned in section [My first plugin](./my-first-plugin), configuration and user data file use `Newtonsoft.Json` to serialize data structure. + +the basic data structure +```csharp +private class Configuration +{ + public string ReplyMessage; +} +``` +will be serialised to +```json +{ + "ReplyMessage" : "a simple reply message" +} +``` +Best practice is to use [NewtonSoft serialization attributes](https://www.newtonsoft.com/json/help/html/SerializeObject.htm) for clarity and easy readability. +Most commonly use attributes : +```csharp +private class PluginData +{ + [JsonProperty(PropertyName = "A simple message when player spawn")] + public string ReplyMessage; + + // Add some info about default value + // or possible range of the data + [JsonProperty(PropertyName = "Maximum health value (default=100)")] + public int MaxHealth = 100; + + // because sometime you need data that does not need to be saved + [JsonIgnore] + public Vector3 Position; + + // Here, ObjectCreationHandling is required, + // to avoid initialisation data to be added over and over, each time plugin restart. + [JsonProperty(PropertyName = "Zones to prevent something", ObjectCreationHandling = ObjectCreationHandling.Replace)] + public List Zones = new List { "KeepOut" }; +} +``` +## Language data file + +The language file is initialized in the LoadDefaultMessages hook. All messages definition will be stored in a file in the ./oxide/lang/[Language code]/[Plugin name].json. +Only the missing messages are added but it does not overwrite the one already existing. This allows server owners to customize messages to their preference. +Server owners can also translate messages to other languages. for example, messages could be translated into Italian and saved in the ./oxide/lang/it/[Plugin name].json file +Note: To revert the language file to the default for a plugin, just delete the file ./oxide/lang/{Language code]/[Plugin name].json +``` csharp +private new void LoadDefaultMessages() +{ + lang.RegisterMessages(new Dictionary + { + ["MSG1"] = "English string 1", + ["MSG2"] = "English string 2", + ... + }, this, "en"); + + lang.RegisterMessages(new Dictionary + { + ["MSG1"] = "Localised string for message 1", + ["MSG2"] = "Localised string for message 2", + }, this, "fr"); + + // ... other languages +} +``` + +ex: The previous test plugin would initialise a file ./oxide/lang/en/test.json that can be edited by server owners. +```json +{ + "MSG1": "English string 1", + "MSG2": "English string 1", +} +``` + +Sample code to retrieving translated message from the language file using the lang.GetMessage function. +``` csharp +private string Lang(string key, string id = null, params object[] args) => string.Format(lang.GetMessage(key, this, id), args); +``` + +### Language code per country/language +| Country | Language code | Country | Language code | Country | Language code | +| :---------------- | :------: | :---------------- | :------: | :---------------- | :------: | +| Afrikaans | af | Arabic | ar | Catalan | ca | +| Czech | cs | Denmark | da | Deutsch | de | +| Greek | el | English | en | Pirate english | en-pt | +| Spanish | es-ES | Finland | fi | French | fr | +| Hebrew | he | Hungarian | hu | Italian | it | +| Japan | ja | Korea | ko | Netherland | nl | +| Norwegian | no | Polish | pl | Portuguese Brazil | pt-BR | +| Portuguese Portugal | pt-PT | Romania | ro | Russia | ru | +| Serbian | sr | Swedish | sv | Turkish | tr | +| Ukrainian | uk | Vietnamese | vi | | | +| Simplified Chinese | zh-CN | Traditional Chinese | zh-TW | | | + + +## Protobuf storage + +see ProtoStorage class + +``` csharp +[ProtoContract] +public class sample { + [ProtoMember(1)] + public string data1; + [ProtoMember(2)] + public List data2; + [ProtoMember(3)] + public DateTime data3; + [ProtoMember(4)] + public int data4; +} +``` + +``` csharp +if ProtoStorage.Exists(filename) + sample mySample = ProtoStorage.Load(filename) ?? new sample(); +else + sample mySample = new sample(); +``` + +``` csharp +ProtoStorage.Save(mySample, filename); +``` + +## Database storage + + + + diff --git a/docs/guides/developers/plugin-guidelines.md b/docs/guides/developers/plugin-guidelines.md index c5600b8..fc29447 100644 --- a/docs/guides/developers/plugin-guidelines.md +++ b/docs/guides/developers/plugin-guidelines.md @@ -1,6 +1,6 @@ --- title: Plugin Guidelines -after: Database +after: database --- # Plugin Guidelines diff --git a/docs/guides/developers/using-decompiler.md b/docs/guides/developers/using-decompiler.md new file mode 100644 index 0000000..ac2d97d --- /dev/null +++ b/docs/guides/developers/using-decompiler.md @@ -0,0 +1,40 @@ +--- +title: Using Decompiler +after: publicizer +--- + +# Using Decompiler + +## Why use a Decompiler + +The Oxide hooks are a part of the information needed to program plugins. You also need to have some knowledge of the Rust game API, and there is no documentation for that. Also there are some changes every wipe. The next best thing is to inspect a decompiled version of the game DLL files. The main Rust class can be found in the Assembly-CSharp.dll file. + +## Decompiler software + +Many software exist to help decompile C# code. here a are a few examples + +**[DNSpy](https://dnspy.org/)** +**[ILSpy](https://github.com/icsharpcode/ILSpy/releases)** +**[dotPeek](https://www.jetbrains.com/decompiler/)** +**JustDecompile** discontinued + +## Modules to load + +These are the most common DLL files to load in the decompiler. Files are located in the `.\server\RustDedicated_Data\Managed\` folder. The files beginning with Oxide are part of the Oxide development platform. Files starting with Oxide.Ext. are extension modules. + +`Assembly-CSharp.dll` (main dll files) +`Oxide.Core.dll` +`Oxide.Rust.dll` +`Oxide.Common.dll` +`Oxide.CSharp.dll` +`Oxide.MySql.dll` +`Oxide.References.dll` +`Oxide.Unity.dll` +`UnityEngine.dll` + +## How to use decompilers + +Use Google to find a good tutorial or go on Youtube to learn about your prefered decompile tool. + + + From 770bfa2d47c85d33d75d4999f6288ca3243ddbf4 Mon Sep 17 00:00:00 2001 From: Lorenzo <114453992+Lorenzo-oo@users.noreply.github.com> Date: Tue, 22 Apr 2025 22:57:30 -0400 Subject: [PATCH 20/27] Rename Attributes.md to attributes.md --- docs/guides/developers/{Attributes.md => attributes.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/guides/developers/{Attributes.md => attributes.md} (100%) diff --git a/docs/guides/developers/Attributes.md b/docs/guides/developers/attributes.md similarity index 100% rename from docs/guides/developers/Attributes.md rename to docs/guides/developers/attributes.md From 934f15b53c9479853ad1bd6b65e4ed17c9c8487a Mon Sep 17 00:00:00 2001 From: Lorenzo <114453992+Lorenzo-oo@users.noreply.github.com> Date: Tue, 22 Apr 2025 22:57:50 -0400 Subject: [PATCH 21/27] Rename Database.md to database.md --- docs/guides/developers/{Database.md => database.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docs/guides/developers/{Database.md => database.md} (99%) diff --git a/docs/guides/developers/Database.md b/docs/guides/developers/database.md similarity index 99% rename from docs/guides/developers/Database.md rename to docs/guides/developers/database.md index 4f7d92b..120e3e4 100644 --- a/docs/guides/developers/Database.md +++ b/docs/guides/developers/database.md @@ -100,4 +100,4 @@ string sqlQuery = "UPDATE example_table SET `field1` = @0, `field2` = @1 WHERE Sql sqlCommand = Oxide.Core.Database.Sql.Builder.Append(sqlQuery, "field1 value", "field2 value", exampleId); sqlLibrary.ExecuteNonQuery(sqlCommand, sqlConnection); -``` \ No newline at end of file +``` From 8899a8b5293b560c4875699b5a78daa21a3fb46b Mon Sep 17 00:00:00 2001 From: Lorenzo <114453992+Lorenzo-oo@users.noreply.github.com> Date: Tue, 22 Apr 2025 22:58:07 -0400 Subject: [PATCH 22/27] Rename Permissions.md to permissions.md --- docs/guides/developers/{Permissions.md => permissions.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docs/guides/developers/{Permissions.md => permissions.md} (99%) diff --git a/docs/guides/developers/Permissions.md b/docs/guides/developers/permissions.md similarity index 99% rename from docs/guides/developers/Permissions.md rename to docs/guides/developers/permissions.md index 4985841..1fcdf82 100644 --- a/docs/guides/developers/Permissions.md +++ b/docs/guides/developers/permissions.md @@ -123,4 +123,4 @@ bool PermissionExists = permission.PermissionExists("epicstuff.use", this); Register a permission ```csharp permission.RegisterPermission("epicstuff.use", this); -``` \ No newline at end of file +``` From dfd56a10942ae3629548a8dcf14da499212a8835 Mon Sep 17 00:00:00 2001 From: Lorenzo <114453992+Lorenzo-oo@users.noreply.github.com> Date: Tue, 22 Apr 2025 22:58:26 -0400 Subject: [PATCH 23/27] Rename Timers.md to timers.md --- docs/guides/developers/{Timers.md => timers.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docs/guides/developers/{Timers.md => timers.md} (96%) diff --git a/docs/guides/developers/Timers.md b/docs/guides/developers/timers.md similarity index 96% rename from docs/guides/developers/Timers.md rename to docs/guides/developers/timers.md index 4db6898..decba0d 100644 --- a/docs/guides/developers/Timers.md +++ b/docs/guides/developers/timers.md @@ -73,4 +73,4 @@ if (myTimer?.Destroyed ?? true) Puts("Timer destroyed!"); } ``` -Note: can also use myTimer.Destroy(); but Destroy() does not put timer back in the pool vs the other two methods use pooling \ No newline at end of file +Note: can also use myTimer.Destroy(); but Destroy() does not put timer back in the pool vs the other two methods use pooling From c005ea6ddb7fa2ff78c58fd6a93ee3550da1a1d7 Mon Sep 17 00:00:00 2001 From: Lorenzo <114453992+Lorenzo-oo@users.noreply.github.com> Date: Tue, 22 Apr 2025 22:58:44 -0400 Subject: [PATCH 24/27] Rename WebRequests.md to webrequests.md --- docs/guides/developers/{WebRequests.md => webrequests.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docs/guides/developers/{WebRequests.md => webrequests.md} (99%) diff --git a/docs/guides/developers/WebRequests.md b/docs/guides/developers/webrequests.md similarity index 99% rename from docs/guides/developers/WebRequests.md rename to docs/guides/developers/webrequests.md index 864b453..8cedef4 100644 --- a/docs/guides/developers/WebRequests.md +++ b/docs/guides/developers/webrequests.md @@ -119,4 +119,4 @@ private void GetCallback(int code, string response, IPlayer player) Puts($"Google answered for {player.Name}: {response}"); } -``` \ No newline at end of file +``` From 535f65f4a3a0069c812cebc6f1f6fa1162ea930d Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Wed, 23 Apr 2025 15:44:39 -0400 Subject: [PATCH 25/27] Modification of section data-storage and using-plugin --- docs/guides/developers/data-storage.md | 128 +++++++++++++++++++-- docs/guides/developers/using-decompiler.md | 2 +- 2 files changed, 117 insertions(+), 13 deletions(-) diff --git a/docs/guides/developers/data-storage.md b/docs/guides/developers/data-storage.md index 5b923ac..691e1a0 100644 --- a/docs/guides/developers/data-storage.md +++ b/docs/guides/developers/data-storage.md @@ -7,7 +7,10 @@ after: using-decompiler ## Configuration/user data file -As previously mentioned in section [My first plugin](./my-first-plugin), configuration and user data file use `Newtonsoft.Json` to serialize data structure. +As previously mentioned in the section [My first plugin](./my-first-plugin), configuration and user data file use `Newtonsoft.Json` to serialize data structure. +Configuration files are stored in `./oxide/config folder` +User data files are stored in `./oxide/data` +Default path can be modified to save in subfolder. the basic data structure ```csharp @@ -45,12 +48,25 @@ private class PluginData public List Zones = new List { "KeepOut" }; } ``` + +will be serialized to + +```json +{ + "A simple message when player spawn" : "a simple reply message", + "Maximum health value (default=100)" : 100, + "Zones to prevent something" : [ + "KeepOut" + ] +} +``` + ## Language data file -The language file is initialized in the LoadDefaultMessages hook. All messages definition will be stored in a file in the ./oxide/lang/[Language code]/[Plugin name].json. +The language file is initialized in the LoadDefaultMessages hook. All messages definition will be stored in a file in the `./oxide/lang/(Language code)/(Plugin name).json`. Only the missing messages are added but it does not overwrite the one already existing. This allows server owners to customize messages to their preference. -Server owners can also translate messages to other languages. for example, messages could be translated into Italian and saved in the ./oxide/lang/it/[Plugin name].json file -Note: To revert the language file to the default for a plugin, just delete the file ./oxide/lang/{Language code]/[Plugin name].json +Server owners can also translate messages to other languages. for example, messages could be translated into Italian and saved in the `./oxide/lang/it/(Plugin name).json` file +Note: To revert the language file to the default for a plugin, just delete the file `./oxide/lang/(Language code)/(Plugin name).json` ``` csharp private new void LoadDefaultMessages() { @@ -102,7 +118,13 @@ private string Lang(string key, string id = null, params object[] args) => strin ## Protobuf storage -see ProtoStorage class +Protobuf store data in a binary format. Main advantage is a more compact and faster data storage, with the disadvantage to not be human readable + +`[ProtoContract]` : to indicates that this class will serialize. +`[ProtoMember(N)]` : where N represents the number in which order it will serialize +`[ProtoIgnore]` : this field will not be serialized. + +### Protobuf class sample ``` csharp [ProtoContract] @@ -110,27 +132,109 @@ public class sample { [ProtoMember(1)] public string data1; [ProtoMember(2)] - public List data2; + public List data2; [ProtoMember(3)] public DateTime data3; - [ProtoMember(4)] + [ProtoIgnore] public int data4; } ``` - +### Protobuf Loading ``` csharp -if ProtoStorage.Exists(filename) - sample mySample = ProtoStorage.Load(filename) ?? new sample(); +sample mySample +if (ProtoStorage.Exists(filename)) +{ + mySample = ProtoStorage.Load(filename) ?? new sample(); +} else - sample mySample = new sample(); +{ + mySample = new sample(); +} ``` - +### Protobuf Saving ``` csharp ProtoStorage.Save(mySample, filename); ``` ## Database storage +Data can be stored in a database using either `SQLite` or `MySQL`. + +### SQlite +In an Oxide plugin, SQLite can be used as a lightweight database solution for storing and retrieving data. +Unlike traditional file-based storage, SQLite allows structured data management through tables, like tracking player statistics. +Plugins can interact with the database using SQL queries to insert, update, delete, or retrieve information. +Since SQLite is embedded, it eliminates the need for a separate database server, +making it a convenient choice for smaller-scale applications within game servers. +Example of SQLite plugin : +```csharp +using Oxide.Core; +using Oxide.Core.Libraries.Covalence; + +namespace Oxide.Plugins +{ + [Info("Test SQLite", "Oxide", "1.0.0")] + public class TestSQLite : CovalencePlugin + { + private readonly string _databaseName = "TestSQLite"; + static readonly Oxide.Core.SQLite.Libraries.SQLite sqlite = Interface.Oxide.GetLibrary(); + static Oxide.Core.Database.Connection sqlConnection; + + private void Init() + { + sqlConnection = sqlite.OpenDb(_databaseName, this); + var sql = new Oxide.Core.Database.Sql(); + sql.Append(@"CREATE TABLE IF NOT EXISTS Players (id INTEGER PRIMARY KEY, name TEXT, kills INTEGER);"); + sqlite.Insert(sql, sqlConnection); + } + + private void InsertPlayerData(string playerName, int kills) + { + string query = $"INSERT INTO Players (name, kills) VALUES ('{playerName}', {kills});"; + var sql = new Oxide.Core.Database.Sql(); + sql.Append(query); + sqlite.Insert(sql, sqlConnection); + } + + private void ReadPlayerData() + { + string query = "SELECT * FROM Players;"; + var sql = new Oxide.Core.Database.Sql(); + sql.Append(query); + sqlite.Query(sql, sqlConnection, list => + { + foreach (var row in list) + { + Puts($"Player {row["name"]} has {row["kills"]} kills."); + } + }); + } + + [Command("addplayer")] + private void CmdAddPlayer(IPlayer iplayer, string command, string[] args) + { + if (args.Length < 2) + { + Puts("addplayer, Missing arguments"); + return; + } + InsertPlayerData(args[0], int.Parse(args[1])); + Puts($"Added player {args[0]} with {args[1]} kills."); + } + + [Command("listplayers")] + private void CmdListPlayers(IPlayer iplayer, string command, string[] args) + { + Puts("listplayers command"); + ReadPlayerData(); + } + } +} +``` +### MySQL +To use MySQL, you need a separate database server. MySQL and MariaDB are the most commonly used server, but other product also be used. +- [MySQL](https://dev.mysql.com/downloads/installer/), version 5.7.X or earlier. +- [MariaDB](https://mariadb.org/download/?t=mariadb&p=mariadb&r=11.7.2&os=windows&cpu=x86_64&pkg=msi&mirror=osuosl) version 10.9.8 or earlier. diff --git a/docs/guides/developers/using-decompiler.md b/docs/guides/developers/using-decompiler.md index ac2d97d..0aad442 100644 --- a/docs/guides/developers/using-decompiler.md +++ b/docs/guides/developers/using-decompiler.md @@ -20,7 +20,7 @@ Many software exist to help decompile C# code. here a are a few examples ## Modules to load -These are the most common DLL files to load in the decompiler. Files are located in the `.\server\RustDedicated_Data\Managed\` folder. The files beginning with Oxide are part of the Oxide development platform. Files starting with Oxide.Ext. are extension modules. +These are the most common DLL files to load in the decompiler. For game Rust, files are located in the `.\server\RustDedicated_Data\Managed\` folder. The files beginning with Oxide are part of the Oxide development platform. Files starting with Oxide.Ext. are extension modules. `Assembly-CSharp.dll` (main dll files) `Oxide.Core.dll` From 719ed026109a36cb0ce74c40119b7d051593acc3 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Fri, 25 Apr 2025 10:53:48 -0400 Subject: [PATCH 26/27] Fix typo --- docs/guides/developers/data-storage.md | 36 ++++++++++++++++---------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/docs/guides/developers/data-storage.md b/docs/guides/developers/data-storage.md index 691e1a0..750b9eb 100644 --- a/docs/guides/developers/data-storage.md +++ b/docs/guides/developers/data-storage.md @@ -7,10 +7,10 @@ after: using-decompiler ## Configuration/user data file -As previously mentioned in the section [My first plugin](./my-first-plugin), configuration and user data file use `Newtonsoft.Json` to serialize data structure. -Configuration files are stored in `./oxide/config folder` -User data files are stored in `./oxide/data` -Default path can be modified to save in subfolder. +As previously mentioned in the section [My first plugin](./my-first-plugin), configuration and user data file use `Newtonsoft.Json` to serialize data structure. +Configuration files are stored in `./oxide/config folder` and +User data files are stored in `./oxide/data`. +But default path can be modified to save in subfolder. the basic data structure ```csharp @@ -25,8 +25,8 @@ will be serialised to "ReplyMessage" : "a simple reply message" } ``` -Best practice is to use [NewtonSoft serialization attributes](https://www.newtonsoft.com/json/help/html/SerializeObject.htm) for clarity and easy readability. -Most commonly use attributes : +Using the variable name as the key, which is not good for readability and clarity. +Best practice is to use [NewtonSoft serialization attributes](https://www.newtonsoft.com/json/help/html/SerializeObject.htm) for clarity the information given to the user.Most commonly use attributes : ```csharp private class PluginData { @@ -49,7 +49,7 @@ private class PluginData } ``` -will be serialized to +and previous sample code will serialize to: ```json { @@ -61,12 +61,13 @@ will be serialized to } ``` + ## Language data file -The language file is initialized in the LoadDefaultMessages hook. All messages definition will be stored in a file in the `./oxide/lang/(Language code)/(Plugin name).json`. -Only the missing messages are added but it does not overwrite the one already existing. This allows server owners to customize messages to their preference. -Server owners can also translate messages to other languages. for example, messages could be translated into Italian and saved in the `./oxide/lang/it/(Plugin name).json` file -Note: To revert the language file to the default for a plugin, just delete the file `./oxide/lang/(Language code)/(Plugin name).json` +The language file is initialized in the LoadDefaultMessages hook. All new message definition will be stored in a file in the `./oxide/lang/(Language code)/(Plugin name).json`. +Only the missing messages are added but does not overwrite the one already existing. This allows server owners to customize messages to their preference. +Server owners can also translate messages to other languages. for example, messages could be translated into Italian and saved in the `./oxide/lang/it/(Plugin name).json` file. +Note: To revert to the default messages, just delete the file `./oxide/lang/en/(Plugin name).json`. ``` csharp private new void LoadDefaultMessages() { @@ -87,7 +88,7 @@ private new void LoadDefaultMessages() } ``` -ex: The previous test plugin would initialise a file ./oxide/lang/en/test.json that can be edited by server owners. +Ex: The previous test code would initialize a file ./oxide/lang/en/test.json that can be customized by server owners. ```json { "MSG1": "English string 1", @@ -118,7 +119,7 @@ private string Lang(string key, string id = null, params object[] args) => strin ## Protobuf storage -Protobuf store data in a binary format. Main advantage is a more compact and faster data storage, with the disadvantage to not be human readable +Protobuf store data in a binary format. Main advantage is a more compact and faster data storage, with the disadvantage to not be human readable like JSON. `[ProtoContract]` : to indicates that this class will serialize. `[ProtoMember(N)]` : where N represents the number in which order it will serialize @@ -234,7 +235,14 @@ namespace Oxide.Plugins ``` ### MySQL -To use MySQL, you need a separate database server. MySQL and MariaDB are the most commonly used server, but other product also be used. +Using MySQL is very similar to SQLite, with the difference that, you need a separate database server that can be on a different machine then the game server, +and many servers can connect to the same database server. MySQL and MariaDB are probably the most commonly used server, but other products also be used. - [MySQL](https://dev.mysql.com/downloads/installer/), version 5.7.X or earlier. - [MariaDB](https://mariadb.org/download/?t=mariadb&p=mariadb&r=11.7.2&os=windows&cpu=x86_64&pkg=msi&mirror=osuosl) version 10.9.8 or earlier. + +```csharp +Oxide.Core.Database.Connection sqlConnection; +Core.MySql.Libraries.MySql Sql = Interface.Oxide.GetLibrary(); +sqlConnection = Sql.OpenDb(config.MYSQL_host, config.MySQL_port, config.MySQL_db, confif.MySQL_user, config.MySQL_pass + ";Connection Timeout = 10; CharSet=utf8mb4", this); +``` From f5ef17e8a8e618adf7bf152a16019261d0840702 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Sun, 27 Apr 2025 23:26:23 -0400 Subject: [PATCH 27/27] Add guide for coroutines and pooling --- docs/guides/developers/attributes.md | 2 +- docs/guides/developers/coroutines.md | 90 ++++++++++++++ docs/guides/developers/data-storage.md | 156 ++++++++++--------------- docs/guides/developers/database.md | 13 ++- docs/guides/developers/pooling.md | 110 +++++++++++++++++ 5 files changed, 276 insertions(+), 95 deletions(-) create mode 100644 docs/guides/developers/coroutines.md create mode 100644 docs/guides/developers/pooling.md diff --git a/docs/guides/developers/attributes.md b/docs/guides/developers/attributes.md index 2571ca4..2ead8ff 100644 --- a/docs/guides/developers/attributes.md +++ b/docs/guides/developers/attributes.md @@ -99,7 +99,7 @@ private void base_OnPlayerConnected(BasePlayer player) => AddOnlinePlayer(player ``` ## AutoPatch -Used with HarmonyPatch to automatically install harmony patch when plugin start, and uninstall when plugin terminate +Used with HarmonyPatch to automatically install the patch when plugin start, and uninstall it, when plugin terminate ```csharp [AutoPatch] [HarmonyPatch(typeof(Planner), "DoPlace")] diff --git a/docs/guides/developers/coroutines.md b/docs/guides/developers/coroutines.md new file mode 100644 index 0000000..fd132a6 --- /dev/null +++ b/docs/guides/developers/coroutines.md @@ -0,0 +1,90 @@ +--- +title: Coroutines +after: pooling +--- + +# Coroutines ( Unity ) + +Coroutines are usefull for tasks that run across multiple frames. Like waiting to HTTP transfers, +Remember that coroutines are not threads. coroutines operation execute on the main thread of the game. + +see [Unity documentation about coroutine](https://docs.unity3d.com/6000.1/Documentation/Manual/coroutines.html) + +Example of coroutine sending http message to Discord + +```csharp + private class DiscordComponent : MonoBehaviour + { + private readonly Queue _queue = new Queue(); + // URL generated in Discord + private string _url; + // coroutine busy status flag + private bool _busy = false; + + public DiscordComponent Configure(string url) + { + if (url == null) throw new ArgumentNullException(nameof(url)); + _url = url; + return this; + } + + // Add a message to a queue + public DiscordComponent SendTextMessage(string message, params object[] args) + { + message = args.Length > 0 ? string.Format(message, args) : message; + return AddQueue(new MessageRequest(message)); + } + + private DiscordComponent AddQueue(object request) + { + _queue.Enqueue(request); + if (!_busy) + StartCoroutine(ProcessQueue()); + return this; + } + + // Coroutine to process message queue + private IEnumerator ProcessQueue() + { + if (_busy) yield break; + _busy = true; + while (_queue.Count!=0) + { + var request = _queue.Dequeue(); + yield return ProcessRequest(request); + } + _busy = false; + } + + private IEnumerator ProcessRequest(object request) + { + // Code to send message to discord + byte[] data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(request)); + UploadHandlerRaw uh = new UploadHandlerRaw(data) { contentType = "application/json" }; + UnityWebRequest www = UnityWebRequest.PostWwwForm(_url, UnityWebRequest.kHttpVerbPOST); + www.uploadHandler = uh; + yield return www.SendWebRequest(); + + if (www.result == UnityWebRequest.Result.ConnectionError || + www.result == UnityWebRequest.Result.ProtocolError) + print($"ERROR: {www.error} | {www.downloadHandler?.text}"); + + www.Dispose(); + + // Wait 2 second between message to avoid spamming Discord + yield return new WaitForSeconds(2.0f); + } +} + +private class MessageRequest +{ + [JsonProperty("content")] + public string Content { get; set; } + + public MessageRequest(string content) + { + if (content == null) throw new ArgumentNullException(nameof(content)); + Content = content; + } +} +``` \ No newline at end of file diff --git a/docs/guides/developers/data-storage.md b/docs/guides/developers/data-storage.md index 750b9eb..5e698ed 100644 --- a/docs/guides/developers/data-storage.md +++ b/docs/guides/developers/data-storage.md @@ -8,8 +8,8 @@ after: using-decompiler ## Configuration/user data file As previously mentioned in the section [My first plugin](./my-first-plugin), configuration and user data file use `Newtonsoft.Json` to serialize data structure. -Configuration files are stored in `./oxide/config folder` and -User data files are stored in `./oxide/data`. +Configuration files are stored in the `./oxide/config folder` and +user data files are stored in `./oxide/data`. But default path can be modified to save in subfolder. the basic data structure @@ -26,7 +26,7 @@ will be serialised to } ``` Using the variable name as the key, which is not good for readability and clarity. -Best practice is to use [NewtonSoft serialization attributes](https://www.newtonsoft.com/json/help/html/SerializeObject.htm) for clarity the information given to the user.Most commonly use attributes : +Best practice is to use [NewtonSoft serialization attributes](https://www.newtonsoft.com/json/help/html/SerializeObject.htm) attributes, to improve clarity of the information given to the user. Most commonly use attributes are JsonProperty and JsonIgnore: ```csharp private class PluginData { @@ -38,7 +38,7 @@ private class PluginData [JsonProperty(PropertyName = "Maximum health value (default=100)")] public int MaxHealth = 100; - // because sometime you need data that does not need to be saved + // because some data does not need to be saved. only save what's needed [JsonIgnore] public Vector3 Position; @@ -60,7 +60,66 @@ and previous sample code will serialize to: ] } ``` +### JSON converter + +It is possible to add custom converter modules by using JsonConverter, for specific types of data. +JsonConverter derived class with methods WriteJson, ReadJson and CanConvert, need to be defined. +The function will determine what to save and how to format the data. The sample code is to show how to serialize `Vector3` on a single line. +```csharp{5-6} +DynamicConfigFile data; +private void LoadData() +{ + data = Interface.Oxide.DataFileSystem.GetFile(Name); + data.Settings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + data.Settings.Converters = new JsonConverter[] { new UnityVector3Converter() }; + try + { + datafile = data.ReadObject>(); + } + catch + { + datafile = new List(); + } + data.Clear(); +} +``` +```csharp +private void SaveData() +{ + if (datafile != null) data.WriteObject(datafile); +} +``` +```csharp +private class UnityVector3Converter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var vector = (Vector3)value; + writer.WriteValue($"{vector.x} {vector.y} {vector.z}"); + } + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.String) + { + var values = reader.Value.ToString().Trim().Split(' '); + return new Vector3(Convert.ToSingle(values[0]), Convert.ToSingle(values[1]), Convert.ToSingle(values[2])); + } + var o = JObject.Load(reader); + return new Vector3(Convert.ToSingle(o["x"]), Convert.ToSingle(o["y"]), Convert.ToSingle(o["z"])); + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(Vector3); + } +} +``` + +With this sample code, the serialized data will be compact and look like this for Vertor3d data. +```json +"position": "-2310 11.10 574", +``` ## Language data file @@ -157,92 +216,3 @@ else ProtoStorage.Save(mySample, filename); ``` -## Database storage - -Data can be stored in a database using either `SQLite` or `MySQL`. - -### SQlite -In an Oxide plugin, SQLite can be used as a lightweight database solution for storing and retrieving data. -Unlike traditional file-based storage, SQLite allows structured data management through tables, like tracking player statistics. -Plugins can interact with the database using SQL queries to insert, update, delete, or retrieve information. -Since SQLite is embedded, it eliminates the need for a separate database server, -making it a convenient choice for smaller-scale applications within game servers. - -Example of SQLite plugin : -```csharp -using Oxide.Core; -using Oxide.Core.Libraries.Covalence; - -namespace Oxide.Plugins -{ - [Info("Test SQLite", "Oxide", "1.0.0")] - public class TestSQLite : CovalencePlugin - { - private readonly string _databaseName = "TestSQLite"; - static readonly Oxide.Core.SQLite.Libraries.SQLite sqlite = Interface.Oxide.GetLibrary(); - static Oxide.Core.Database.Connection sqlConnection; - - private void Init() - { - sqlConnection = sqlite.OpenDb(_databaseName, this); - var sql = new Oxide.Core.Database.Sql(); - sql.Append(@"CREATE TABLE IF NOT EXISTS Players (id INTEGER PRIMARY KEY, name TEXT, kills INTEGER);"); - sqlite.Insert(sql, sqlConnection); - } - - private void InsertPlayerData(string playerName, int kills) - { - string query = $"INSERT INTO Players (name, kills) VALUES ('{playerName}', {kills});"; - var sql = new Oxide.Core.Database.Sql(); - sql.Append(query); - sqlite.Insert(sql, sqlConnection); - } - - private void ReadPlayerData() - { - string query = "SELECT * FROM Players;"; - var sql = new Oxide.Core.Database.Sql(); - sql.Append(query); - sqlite.Query(sql, sqlConnection, list => - { - foreach (var row in list) - { - Puts($"Player {row["name"]} has {row["kills"]} kills."); - } - }); - } - - [Command("addplayer")] - private void CmdAddPlayer(IPlayer iplayer, string command, string[] args) - { - if (args.Length < 2) - { - Puts("addplayer, Missing arguments"); - return; - } - InsertPlayerData(args[0], int.Parse(args[1])); - Puts($"Added player {args[0]} with {args[1]} kills."); - } - - [Command("listplayers")] - private void CmdListPlayers(IPlayer iplayer, string command, string[] args) - { - Puts("listplayers command"); - ReadPlayerData(); - } - } -} -``` - -### MySQL -Using MySQL is very similar to SQLite, with the difference that, you need a separate database server that can be on a different machine then the game server, -and many servers can connect to the same database server. MySQL and MariaDB are probably the most commonly used server, but other products also be used. - -- [MySQL](https://dev.mysql.com/downloads/installer/), version 5.7.X or earlier. -- [MariaDB](https://mariadb.org/download/?t=mariadb&p=mariadb&r=11.7.2&os=windows&cpu=x86_64&pkg=msi&mirror=osuosl) version 10.9.8 or earlier. - -```csharp -Oxide.Core.Database.Connection sqlConnection; -Core.MySql.Libraries.MySql Sql = Interface.Oxide.GetLibrary(); -sqlConnection = Sql.OpenDb(config.MYSQL_host, config.MySQL_port, config.MySQL_db, confif.MySQL_user, config.MySQL_pass + ";Connection Timeout = 10; CharSet=utf8mb4", this); -``` diff --git a/docs/guides/developers/database.md b/docs/guides/developers/database.md index 120e3e4..32a0f19 100644 --- a/docs/guides/developers/database.md +++ b/docs/guides/developers/database.md @@ -5,13 +5,23 @@ after: permissions # Database The Oxide database extensions implement a generalized database abstraction layer for both MySQL and SQLite. +[MySQL](https://dev.mysql.com/downloads/installer/) and [MariaDB](https://mariadb.org/download/?t=mariadb&p=mariadb&r=11.7.2&os=windows&cpu=x86_64&pkg=msi&mirror=osuosl) are probably the most commonly used server, but other products also be used. + + ## Open a connection -Create a new connection to a database by providing the database file location or an address (URI and port), a database name, and authentication credentials. +Create a new `MYSQL` connection to a database by providing the database file location or an address (URI and port), a database name, and authentication credentials. ```csharp Core.MySql.Libraries.MySql sqlLibrary = Interface.Oxide.GetLibrary(); Connection sqlConnection = sqlLibrary.OpenDb("localhost", 3306, "umod", "username", "password", this); ``` + +In the case of SQLite, the database is local, in `./oxide/data/` folder +```csharp +Oxide.Core.SQLite.Libraries.SQLite sqlLibrary = Interface.Oxide.GetLibrary(); +Connection sqlConnection = sqlLibrary.OpenDb(_databaseName, this); +``` + ## Close the connection Close an existing connection to the database. @@ -75,6 +85,7 @@ sqlLibrary.Update(updateCommand, sqlConnection, rowsAffected => } }); ``` + ## Delete query Delete existing records from a database using a DELETE statement. diff --git a/docs/guides/developers/pooling.md b/docs/guides/developers/pooling.md new file mode 100644 index 0000000..7941fee --- /dev/null +++ b/docs/guides/developers/pooling.md @@ -0,0 +1,110 @@ +--- +title: Pooling +after: data-storage +--- + +# Pooling + +## Reducing allocations and garbage collections with pooling + +A pool provides a repository of active and ready-made objects that may be obtained upon request and freed back to the pool after they are no longer needed. + +The purpose of pooling is to reduce memory footprint and the number of cycles normally associated with constructing and destroying objects. + +Normally when an object is instantiated, memory must be allocated for that object and when the object is no longer needed it is marked for destruction. An object marked for destruction it is not destroyed immediately, instead it sits in memory until the garbage collector destroys the object later. + +```csharp +var objectArray = new object[2]; +var anotherObjectArray = new object[2]; +``` + +The above code will create two object arrays which must be garbage collected. In contrast, the following code will only create one object array and is not garbage collected. +This is just an example as Oxide.Core.ArrayPool is marked obsolete + +```csharp +// +// +``` + +```csharp + // Note that Oxide.Core.ArrayPool is obsolete + object[] objectArray = ArrayPool.Get(2); + ArrayPool.Free(objectArray); + + object[] anotherObjectArray = ArrayPool.Get(4); + ArrayPool.Free(anotherObjectArray); +``` + +In the case where a program must create a lot of objects, the garbage collector may in-fact represent a significant and typically unavoidable performance bottleneck. Pooling provides a relatively easy way around this bottleneck by avoiding memory allocations and garbage collections altogether. + +## Facepunch.Pool (Rust) + +The Facepunch.Pool implementation is specific to the game Rust. +Facepunch pooling is divided on three types of object `` or collections of objects. +- Object or collections of objects derived from IPooled +- Collections of object not derived from IPooled +- Collections of objects not part of the two previous cases. + +## Pooling of IPooled derived object + +`Get()` and `Free(ref T obj)` / `Free(ref ICollection obj)` + +Where ICollection is one of : `List, HashSet, BufferList, Queue, ListHashSet, Dictionnary, ListDictionary` and `` is derived from IPooled + +example with Item, derived from IPooled : + +```csharp +var itemList = Pool.Get>(); +player.inventory.GetAllItems(itemList); +// Do some stuff with itemList +Pool.Free(ref itemList); +``` + +## Pooling of non-IPooled derived object + +`Get()` and `FreeUnmanaged(ref ICollection obj)` + +Where ICollection is one of : `MemoryStream, StringBuilder, Stopwatch, List, HashSet, BufferList, Queue, ListHashSet, Dictionnary, ListDictionary` + +example with DieselEngine : + +```csharp +List engineList = Pool.Get>(); +Vis.Entities(position, 20f, engineList, -1); +// Do some stuff with engine +Pool.FreeUnmanaged(ref engineList); +``` + +## Pooling of other objects + +`Get()` and `FreeUnsafe(ref T obj)` + +```csharp +Stack ovenStack = Pool.Get>(); +// Do some stuff with ovenStack +Pool.FreeUnsafe(ref ovenStack); +``` + +### Using try / finally + +It is important to ensure that when an object or ICollection is obtained from a pool that it is freed back to that (same) pool. This can be difficult when exceptions are thrown by any code in-between. + +If there is any chance that an exception may be thrown, wrap the code with a "try/finally" block to ensure the code within the "finally" is always executed no matter the outcome. +```csharp +// Obtain an StringBuilder +StringBuilder sb = Pool.Get(); + +try +{ + throw new System.Exception("Oh no!"); + return sb.ToString(); +} +finally +{ + Pool.FreeUnmanaged(ref sb); +} +``` + +### Reset + +When using `Pool.Free`, `Pool.FreeUnmanaged` or `Pool.FreeUnsafe`, the object freed back to the pool will reset to `obj = default(T);` and `ICollection` will be cleared. When the object is used again, it will not contain previous data.