Event Handling and Communication Between Addons
Updated 21 October, 2026
Nexus has a simple, string-based event system. Any addon can subscribe to an event by name and any addon can raise one, with an optional pointer to data. Nexus itself uses it to announce changes, the built-in ArcDPS integration uses it to pass on combat data, and addons use it to talk to each other without depending on each other's code. This guide covers subscribing, raising, targeting a single addon, designing your own events and the events that are available out of the box.
Subscribing to an event
An event handler takes a single void* argument, the event's payload. What it points to depends on the event:
void OnIdentityUpdated(void* aEventArgs)
{
Mumble::Identity* identity = (Mumble::Identity*)aEventArgs;
if (!identity) { return; }
char msg[64];
snprintf(msg, sizeof(msg), "Now playing %s.", identity->Name);
APIDefs->Log(LOGL_INFO, "MyAddon", msg);
}
void AddonLoad(AddonAPI_t* aApi)
{
APIDefs = aApi;
APIDefs->Events_Subscribe(EV_MUMBLE_IDENTITY_UPDATED, OnIdentityUpdated);
}
void AddonUnload()
{
APIDefs->Events_Unsubscribe(EV_MUMBLE_IDENTITY_UPDATED, OnIdentityUpdated);
}
Mumble::Identity is defined in the RCGG-lib-mumble-api header.
- Events are identified by name only. You can subscribe to an event before anyone raises it, and before the addon that raises it is loaded.
- Unsubscribe with the same name and the same function. Subscribing the same function twice means it is called twice.
- Do not unsubscribe a handler from inside a handler of the same event. Do it in your unload function or from another place.
How events are delivered
Raising an event calls all of its subscribers immediately, one after another, on the thread that raised it. Events_Raise only returns once every handler has returned. This has a few consequences for handlers:
- The payload is only valid during the call. If you need the data later, copy it.
- Handlers may run on any thread. Nexus raises some events from the render thread and some from background threads, and ArcDPS combat events arrive on ArcDPS's thread. Protect shared data, and do not call ImGui from handlers.
- Slow handlers slow down the raiser. A combat event handler that takes a millisecond delays ArcDPS and every addon after you, in every fight. Copy what you need, and process it elsewhere.
Raising events
Raise an event with a name and a pointer to its payload, or use a notification when there is nothing to send:
#define EV_MYADDON_TIMER_EXPIRED "EV_MYADDON_TIMER_EXPIRED"
#define EV_MYADDON_RESET "EV_MYADDON_RESET"
struct TimerExpired_t
{
uint32_t TimerID;
const char* Name;
};
void OnTimerExpired(uint32_t aID, const char* aName)
{
TimerExpired_t payload{ aID, aName };
APIDefs->Events_Raise(EV_MYADDON_TIMER_EXPIRED, &payload);
}
void Reset()
{
APIDefs->Events_RaiseNotification(EV_MYADDON_RESET); /* Payload is nullptr. */
}
Because delivery is synchronous, the payload can live on the stack: it outlives every handler.
Targeted events
Sometimes an event is meant for one addon only, for example a reply to a request. Events_RaiseTargeted and Events_RaiseNotificationTargeted deliver an event only to the addon with the given signature. Nexus knows which addon a handler belongs to from the module its code lives in, so the receiver subscribes as usual.
A common pattern is request and reply. Addon A owns some data. Addon B asks for it, passing its own signature, and A replies only to B:
/* Addon A: answers requests. */
#define EV_MYADDON_REQUEST_STATUS "EV_MYADDON_REQUEST_STATUS"
#define EV_MYADDON_STATUS "EV_MYADDON_STATUS"
struct Status_t
{
uint32_t Version;
bool IsActive;
};
void OnStatusRequested(void* aEventArgs)
{
if (!aEventArgs) { return; }
uint32_t requester = *(uint32_t*)aEventArgs;
Status_t status{ 1, s_IsActive };
APIDefs->Events_RaiseTargeted(requester, EV_MYADDON_STATUS, &status);
}
/* In Load: */
APIDefs->Events_Subscribe(EV_MYADDON_REQUEST_STATUS, OnStatusRequested);
/* Addon B: asks for the status. */
void OnStatus(void* aEventArgs)
{
Status_t* status = (Status_t*)aEventArgs;
/* Copy what you need. */
}
/* In Load: */
APIDefs->Events_Subscribe(EV_MYADDON_STATUS, OnStatus);
uint32_t self = MY_SIGNATURE;
APIDefs->Events_Raise(EV_MYADDON_REQUEST_STATUS, &self);
Other addons subscribed to EV_MYADDON_STATUS do not receive B's reply. Use Events_RaiseNotificationTargeted(signature, name) for targeted events without a payload. If no handler of the target addon is subscribed, nothing happens.
The same pattern lets late subscribers catch up: when an addon loads after the data source, it requests the current state instead of waiting for the next change. The ArcDPS integration offers exactly this, see below.
Designing your own events
Events become part of your addon's public interface as soon as another addon uses them. A little care keeps them stable:
- Document the payload. Publish the struct definitions and say how long the data is valid, ideally in a small header other developers can include.
- Plan for change. Never change the layout of a published payload. Add a new event, or include a version or size field from the start so receivers can check what they got.
- Use plain data. Fixed-size integers, plain structs and C strings work for every addon, including those written in other languages. Avoid C++ classes, STL containers and anything that needs a specific compiler.
Events suit notifications and one-off messages. For state that other addons should be able to read at any time, use a shared resource instead, and raise an event when it changes.
Events raised by Nexus
| Event | Payload | Raised when |
|---|---|---|
EV_MUMBLE_IDENTITY_UPDATED |
Mumble::Identity* |
The player identity in MumbleLink changed: another character, map, commander tag, interface size and so on. Also raised right after any addon loads. |
EV_WINDOW_RESIZED |
None | The game window or its buffers were resized. Read the new size from NexusLinkData_t. |
EV_ADDON_LOADED |
uint32_t* signature |
An addon finished loading. |
EV_ADDON_UNLOADED |
uint32_t* signature |
An addon was unloaded. |
EV_VOLATILE_ADDON_DISABLED |
uint32_t* signature |
A volatile addon was not loaded because the game was updated. |
EV_INPUTBIND_UPDATED |
None | The user changed a keybind. |
The first four are defined in Nexus.h. For the others, use the name as a string.
ArcDPS events
When ArcDPS is loaded, the built-in ArcDPS integration of Nexus forwards ArcDPS's combat API as events. Your addon does not need to be an ArcDPS extension, and it does not need to know how ArcDPS was installed.
Combat events
| Event | Payload | Contains |
|---|---|---|
EV_ARCDPS_COMBATEVENT_SQUAD_RAW |
EvCombatData* |
ArcDPS's squad combat callback (combat). |
EV_ARCDPS_COMBATEVENT_LOCAL_RAW |
EvCombatData* |
ArcDPS's local combat callback (combat_local), covering everything in your area. |
The payload wraps the arguments of ArcDPS's combat callback. The CombatEvent and AgentShort types are ArcDPS's cbtevent and ag, also available in the RCGG-lib-arcdps-api header:
struct EvCombatData
{
ArcDPS::CombatEvent* ev;
ArcDPS::AgentShort* src;
ArcDPS::AgentShort* dst;
char* skillname;
uint64_t id;
uint64_t revision;
};
The meaning of every field is described in ArcDPS's own API documentation. These events arrive on ArcDPS's thread at a high rate in large fights, so copy what you need and return quickly.
Squad, self and target
The integration also tracks ArcDPS's agent updates and raises simpler events for them:
| Event | Payload | Raised when |
|---|---|---|
EV_ACCOUNT_NAME |
const char* |
Your own account name became known. |
EV_ARCDPS_SELF_JOIN, EV_ARCDPS_SELF_LEAVE |
EvAgentUpdate* |
Your own character appeared or left, for example after a map change. |
EV_ARCDPS_SQUAD_JOIN, EV_ARCDPS_SQUAD_LEAVE |
EvAgentUpdate* |
A squad member joined or left. |
EV_ARCDPS_TARGET_CHANGED |
EvAgentUpdate* |
Your target changed. |
struct EvAgentUpdate
{
char account[64]; /* Account name. */
char character[64]; /* Character name. */
uintptr_t id; /* Agent ID. */
uintptr_t instanceId; /* Instance ID, unique per map. */
uint32_t added; /* Agent was added (1) or removed (0). */
uint32_t target; /* Agent is the new target. */
uint32_t Self; /* Agent is you. */
uint32_t prof; /* Profession. */
uint32_t elite; /* Elite specialization. */
uint16_t team; /* Team. */
uint16_t subgroup; /* Squad subgroup. */
};
If your addon loads after these events were raised, ask the integration to send them again. It replies with the regular events:
| Raise this notification | To receive |
|---|---|
EV_REQUEST_ACCOUNT_NAME |
EV_ACCOUNT_NAME |
EV_REPLAY_ARCDPS_SELF_JOIN |
EV_ARCDPS_SELF_JOIN |
EV_REPLAY_ARCDPS_SQUAD_JOIN |
EV_ARCDPS_SQUAD_JOIN for every current squad member |
EV_REPLAY_ARCDPS_TARGET_CHANGED |
EV_ARCDPS_TARGET_CHANGED |
/* In Load, after subscribing: */
APIDefs->Events_Subscribe("EV_ARCDPS_SQUAD_JOIN", OnSquadJoin);
APIDefs->Events_RaiseNotification("EV_REPLAY_ARCDPS_SQUAD_JOIN");
The replies are raised shortly afterwards from a separate thread, not during your own call. They are broadcast to all subscribers, so make your handlers tolerate receiving the same member twice. If the data is not known yet, for example because you are not in a squad, there is no reply.
Unofficial Extras
If the Unofficial Extras ArcDPS extension is installed, its callbacks are forwarded as well. The payload types are defined in its Definitions.h.
| Event | Payload |
|---|---|
EV_UNOFFICIAL_EXTRAS_SQUAD_UPDATE |
Pointer to a struct of UserInfo* and uint64_t count |
EV_UNOFFICIAL_EXTRAS_LANGUAGE_CHANGED |
Language* |
EV_UNOFFICIAL_EXTRAS_KEYBIND_CHANGED |
KeyBinds::KeyBindChanged* |
EV_UNOFFICIAL_EXTRAS_CHAT_MESSAGE |
ChatMessageInfo* |