Skip to main content

Mod Interoperability

Starting from Warudo 0.14.3, mods can call each other. Mods do not need to use uMod's reference dependency system; instead, they can interoperate using the new PluginRouter mechanism.

Signals and Slots

Mods can send one-way, non-directional messages to other mods via signals. This is typically used to broadcast internal mod events — for example, an environment broadcasting the start and end of an internal animation, collision states, whether a predefined environment event was triggered, or other mod internal states that other mods can automatically synchronize with.

Emitting Signals

To emit a signal you need a valid Plugin/Asset/Node instance and call its EmitSignal instance method with a class that inherits from Warudo.Core.Events.Event:

[AssetType(Id="AMod.Asset")]
public class AModAsset: Asset {
public class MyEventOnA : Warudo.Core.Events.Event
{
public string message;
}

[Trigger]
public void OnSomeEvent()
{
this.EmitSignal(new MyEventOnA {
message = "Hello from MyMod!"
});
}
}

This is equivalent to calling

Context.PluginRouter.EmitSignal(this, new MyEventOnA {
message = "Hello from MyMod!"
});

Connecting Slots

To receive signals, call ConnectSlot on your Plugin/Asset/Node instance, passing the sender type ID and a callback:

[AssetType(Id="BMod.Asset")]
public class BModAsset: Asset {
public class MyEventOnB : Warudo.Core.Events.Event
{
public string message;
}
public override void OnCreate()
{
base.OnCreate();
var link = ConnectSlot<MyEventOnB>("AMod.Asset", (evt, entity) => {
// evt is the Signal instance, in this case MyEventOnB
// entity is the entity that sent the signal, in this case an instance of AModAsset
});
// Disconnect the slot. It will be automatically disconnected when the instance is destroyed
DisconnectSlot(link);
}
}

In the example above, the first argument to ConnectSlot ("AMod.Asset") is the type ID of the signal sender — it can be any Plugin/Asset/Node type ID. MyEventOnA and MyEventOnB are interoperable as long as their internal structure is the same.

This is equivalent to calling

var link = Context.PluginRouter.ConnectSlot<MyEventOnB>(this, "AMod.Asset", (e, s) => {
if (CanReceiveEvents) {
handler(e, s);
}
});

Commands

Mods can also communicate bidirectionally using commands. Unlike signals, commands are directed request-response interactions: the sender waits for a response from the receiver.

Registering a Command

To register a command, call RegisterCommand on a Plugin/Asset/Node instance and provide the command name and a callback:

[AssetType(Id="AMod.Asset")]
public class AModAsset: Asset {
public class MyCommandRequestOnA
{
public string message;
}
public class MyCommandResponseOnA
{
public int time;
public string message;
}

[Trigger]
public void OnSomeEvent()
{
var commandId = this.RegisterCommand<MyCommandRequestOnA, MyCommandResponseOnA>("MyCommand", (req) => {
// req is the command request parameter
Debug.Log($"AMod received command: {req.message}");
// Return value
return new MyCommandResponseOnA {
time = DateTime.Now.Millisecond,
message = "Response from AMod"
};
});
}
}

To unregister, call UnregisterCommand. Registrations are automatically removed when the instance is destroyed:

this.UnregisterCommand(commandId);

Executing a Command

To send a command you need a valid target Plugin/Asset/Node instance. Use the PluginRouter's ExecuteCommand method to send the command:

public class MyCommandRequestOnB
{
public string message;
}
public class MyCommandResponseOnB
{
public int time;
public string message;
}

[DataInput]
[TypeIdFilter("AMod.Asset")]
Asset targetEntity;

// Execute command
CommandResult<MyCommandResponseOnB> commandResult = Context.PluginRouter.ExecuteCommand<MyCommandRequestOnB, MyCommandResponseOnB>(
targetEntity, // Target entity, can be a Plugin/Asset/Node instance
"MyCommand", // Command name
new MyCommandRequestOnB { // Command request parameter
message = "Hello from BMod!"
}
);

The method returns a CommandResult<TResponse> instance, which contains the result of the command:

public class CommandResult<T>
{
public CommandResultStatus Status;
public T Data;
}

CommandResultStatus is an enum that indicates the execution status of the command:

public enum CommandResultStatus
{
SUCCESS, // Successfully executed
ENTITY_NOT_FOUND, // Target entity not found
COMMAND_NOT_FOUND, // Target entity has not registered this command
EXECUTION_ERROR // An error occurred while executing the command
}

You can check the Status field to determine whether the command succeeded, and access the response via the Data field:

if (commandResult.Status == CommandResultStatus.SUCCESS)
{
Debug.Log($"BMod received response: {commandResult.Data.message} at {commandResult.Data.time}");
}
else
{
Debug.LogError($"Command execution failed with status: {commandResult.Status}");
}

Like signals, command request and response types are interoperable as long as their internal structure matches.

Querying whether a Command Exists

Context.PluginRouter.HasCommand<TArgs>(targetEntity, "CommandName"); // returns bool

Contributions

Edit on Github

Authors

Last updated on 2026.01.13