Hooks
Details about basic hook usage
uMod supports hundreds of hooks out of the box and more can be implemented by plugins.
Observer pattern
Often referred to as the "pub/sub" or the "Publish-subscribe" pattern, the observer pattern is the key pattern used by hooks.
uMod will search plugins for any subscriptions to a hook when the hook is called. Hook methods are then invoked by uMod and often have additional return behavior which modifies or cancels the default game behavior.
Global vs. local scope
Global
Normally hooks are dispatched from the global scope which is accessed via the primary Interface like so:
Interface.uMod.CallHook("MyTestHook");
Alternatively, global hooks may be dispatched directly from the Interface class:
Interface.CallHook("MyTestHook");
Local
Plugins may dispatch hooks locally in a similar way:
CallHook("MyTestHook");
uMod.Common.IPlugin MyPlugin;
void Loaded()
{
MyPlugin?.CallHook("MyTestHook");
}
Subscription
All private methods in a plugin class become hooks when the plugin loads. When a hook is invoked, all hook methods with a matching name (and matching parameters) are invoked in every plugin that subscribes to the hook.
bool CanPlayerLogin(string playerName, string playerId, string playerIp)
{
Logger.Info("No one can connect");
return false; // By returning false, no players may connect to this server
}
The above method uses the CanPlayerLogin hook and (by returning false) prevents anyone from connecting to the server.
Hook attribute
Hook methods with private access modifiers are automatically registered and subscribed when a plugin is loaded. When implementing hook methods with public access modifiers it is necessary to annotate the methods with the [Hook] attribute.
[Hook]
public bool CanPlayerLogin(string playerName, string playerId, string playerIp)
{
return false;
}
A list of all available hooks categorized by game can be found here.
If the method name does not match the name of the hook, the Hook attribute may specify the name explicitly.
[Hook("CanPlayerLogin")]
public bool MyCanPlayerLogin(string playerName, string playerId, string playerIp)
{
return false;
}
Async attribute
Annotate a hook method with the [Async] attribute to perform computationally expensive operations in a background thread. Asynchronous hooks may not have return behavior or integration with other hooks.
[Async]
void OnServerSave()
{
/* Perform expensive operation */
}
Manual subscription
Typically it is not necessary to subscribe or unsubscribe from hooks, however it is sometimes useful to control which hook methods are subscribed.
Unsubscribe
The example method below unsubscribes OnPlayerChat if chatFeatureEnabled is false. Please consider the Configuration documentation for details on how to properly implement configuration options. For the purpose of this example we will use a simple local variable that is always false.
bool chatFeatureEnabled = false;
void Init()
{
if (!chatFeatureEnabled)
{
Unsubscribe(nameof(OnPlayerChat));
}
}
object OnPlayerChat(IPlayer player, string message)
{
// Do stuff
}
Signature
bool Unsubscribe(string hookName);
Subscribe
After unsubscribing from a hook, as in the previous example, the plugin may re-subscribe again to re-enable the plugin's chat behavior.
[Command("epic_chat_enable")]
void EnableChatBehaviorCommand(IPlayer player)
{
if (player.IsAdmin)
{
Logger.Info("Enabling chat behavior");
chatFeatureEnabled = true;
Subscribe(nameof(OnPlayerChat));
}
}
The above command will allow admins to re-enable the special chat behavior.
Signature
bool Subscribe(string hookName);
IsSubscribed
Check whether or not a hook is currently subscribed using the IsSubscribed method.
Signature
bool IsSubscribed(string hookName);
Overloading
If multiple methods are subscribed to the same hook, the hook methods with parameters that match the supplied arguments will be invoked.
For example, if a plugin were to implement a new hook named EpicHook and dispatch it twice using different parameters like so:
int EpicNumber = 42;
float EpicFloat = 77.7f;
string EpicString = "Whoa";
Interface.CallHook("EpicHook", EpicNumber, EpicFloat);
Interface.CallHook("EpicHook", EpicNumber, EpicString);
Another plugin could subscribe to the new "EpicHook" like so:
void EpicHook(int epicNumber, float epicFloat)
{
Logger.Info($"Received a number '{epicNumber}' and float '{epicFloat}'");
}
void EpicHook(int epicNumber, string epicString)
{
Logger.Info($"Received a number '{epicNumber}' and string '{epicString}'");
}
And finally, the expected output after combining both implementations would be:
Received a number '42' and float '77.7'
Received a number '42' and string 'Whoa'
Parameter substitution
Parameter substitution is a form of "magic" where an argument is converted to another type before a hook method is invoked.
Create a converter by implementing a method and annotating it with the Converter attribute.
- The first parameter of the converter method is the input type (or
TInput) - The return type of the converter method is the output type (or
TOutput)
When a hook within the plugin scope receives an argument of type TInput but it does not match the expected parameter type TOutput , then the corresponding converter method will attempt to convert the argument to the expected type before calling the hook.
private Dictionary<int, MockData> mockData = new Dictionary<int, MockData>();
class MockData
{
public int ID = 1;
}
// TInput: int
// TOutput: MockData
[Converter]
MockData ConvertMockData(int id)
{
mockData.TryGetValue(id, out MockData mockDataImpl);
return mockDataImpl;
}
[Hook("OnAnyHook")]
void OnAnyHook(MockData mockDataImpl)
{
if (mockDataImpl != null)
{
Logger.Info($"Converted {mockDataImpl.ID}");
}
else
{
Logger.Info("No mockData found");
}
}
// Later..
mockData.Add(1, new MockData());
CallHook("OnAnyHook", 1);
In the example above, the hook OnAnyHook is called and passed a integer. Before the hook is called, the converter method converts the integer to the concrete MockData type.
The IPlayer/GamePlayer subtitution is baked-in and does not require additional converters to be specified.
Return behavior
A passive hook is a hook which as no return behavior. Return behavior is used to modify or change the outcome of game functionality. Each individual hook may behave differently and it is generally recommended to consider a hook's documentation to understand it's return behavior. There are 4 ways to categorize return behavior...
Non-null cancellation
Returning any value except null will cancel the default behavior.
True/false override
Returning false will cancel default behavior and returning true will continue default behavior.
Object override
Returning an object of a particular class will override default behavior.
Primitive override
Returning a primitive (e.g. integer, float) will override default behavior.
Deferral
Hook conflicts can be resolved easily by annotating a hook with the Defer attribute.
A deferred hook will be skipped if any plugin named in the Defer annotation returns anything other than null.
umod/plugins/FirstPlugin.cs (snippet)
bool OnJump(IPlayer player)
{
return false;
}
umod/plugins/SecondPlugin.cs (snippet)
[Defer("FirstPlugin")]
bool OnJump(IPlayer player)
{
return true;
}
In the above scenario, if both plugins are loaded, then the OnJump method of SecondPlugin will be skipped because the OnJump method of FirstPlugin returned a non-null value.
If only one plugin is loaded, the hook will be invoked normally (in either case) and no deferral will occur.
For very popular hooks, or in the case where many deferrals are needed, it's recommended to use hook events instead.
Events
The hook event system provides a convenient passthru interface for disparate plugins to share generic state information and wrap the normal hook execution workflow.
When multiple plugins use the same hook to modify return behavior in different ways, return conflicts may be difficult to resolve. Hook priority or load order (e.g. Oxide) is a poor solution to this problem.
To help resolve hook conflicts, the hook workflow is split into 5 events: Before, After, Completed, Failed, and Canceled.
Beforehooks are executed first.Completedhooks are executed if no hook methods fail or cancel the hook.Failedhooks are executed if any hook fails (usually due to an exception)Canceledhooks are executed if any hooks attempts to cancel the hook.Afterhooks are executed last regardless of the final hook state (completed, failed, canceled)
umod/plugins/FirstPlugin.cs (snippet)
bool IsMuted(IPlayer player)
{
return player.BelongsToGroup("firstplugin.muted");
}
// This hook will be called first
[Before]
void OnPlayerChat(IPlayer player, string message, HookEvent e)
{
if (IsMuted(player))
{
// Communicate to other plugins using OnPlayerChat that execution should be canceled (and why)
e.Cancel("Player is muted");
}
}
Any hook at any point in the hook execution workflow may observe or modify the event state and register callbacks to be triggered should the event state update.
umod/plugins/SecondPlugin.cs (snippet)
bool OnPlayerChat(IPlayer player, string message, HookEvent e)
{
// Has a previously executed hook tried cancel the hook event
if (e.Canceled)
{
player.Reply(e.StateReason);
return false;
}
// In case another hook post-execution attempts to cancel the hook event
e.Context.Canceled(delegate(Plugin plugin, HookEvent canceledArgs)
{
player.Reply(canceledArgs.StateReason);
canceledArgs.FinalValue = false;
});
return true;
}
[Completed("OnPlayerChat")]
void OnPlayerChatCompleted(IPlayer player, string message, IServer server)
{
// We know this player is not muted, so broadcast their message
server.Broadcast(message, player.Name);
}
Please consider the Hook Events documentation for more information.
Namespaces
When a hook has large scope it is possible to separate the hook into different methods. Hook namespacing provides an easy way to ensure all concerns are executed when dispatched from the global scope while still giving developers the flexibility to dispatch specific concerns individually from the plugin scope.
For example, if a plugin were to have multiple initialization concerns and in some cases later need to reinitialize one particular concern then namespacing that concern would help avoid the need for any additional abstractions while also leveraging the existing hook functionality as documented above.
void Init()
{
Logger.Info("Main initialization");
// Main initialization
}
[Hook("Init.Concern")]
void InitConcern()
{
Logger.Info("Concern initialization");
// Code here
}
Example executation using the above hook definitions
// Execute both "Init" hooks
CallHook("Init");
// Prints: Main initialization
// Prints: Concern initialization
// Execute a specific concern
CallHook("Init.Concern");
// Prints: Concern initialization
Some keywords are reserved and cannot be used as namespaces: After, Canceled, Completed, Failed, None, and Started.