Skip to content

Event Subscription and Heraclitean Flux

When a RocketMod plugin subscribes to UnturnedPlayerEvents.OnPlayerConnected += OnPlayerConnected, it is attaching itself to a stream. The stream is continuous — every player connection, disconnection, death, damage event, chat message, inventory change, and stat update flows through the server's event dispatch system, and every plugin that has subscribed to a given event receives every occurrence of that event in order. The stream never pauses. It flows at the rate of server activity — which is to say, at the rate of player interaction, which is irregular, human-paced, and unpredictable. The plugin that subscribes to the stream commits to receiving everything that flows past, forever, until it unsubscribes.

This article argues that the RocketMod event system is the architectural instantiation of the doctrine of perpetual flux that Heraclitus of Ephesus articulated in the fifth century BCE. Heraclitus's most famous fragment — "You cannot step into the same river twice" — asserts that the world is not composed of stable objects but of continuous processes. The river is a river because water flows through it. Stop the flow and the river ceases to be a river. The identity of the river is its process, not its substance.

The event stream is Heraclitean. The stream is not composed of discrete events stored in a list and dispatched on command. It is composed of the continuous flow of player activity as it occurs, dispatched in order, with each event arriving once and never arriving again. The plugin hooked into the stream cannot examine the stream's contents in advance. It cannot know what event will arrive next. It can only process each event as it arrives, and when the event has been processed, it is gone — unrecoverable, unrepeatable, part of the river that has already flowed past.

The 57 Studios internal engineering philosophy treats the Heraclitean nature of the event stream as a design constraint. A plugin that assumes it can step into the same tick twice — that assumes events are reproducible, that state can be reconstructed from the stream, that the stream's order is negotiable — is a plugin that misunderstands the fundamental nature of the system it operates in.

This article presents the technical architecture of RocketMod's event system — the UnturnedEvents and UnturnedPlayerEvents classes, the delegate-based subscription model, the event dispatch order — alongside the Heraclitean philosophy that explains why that architecture is the only correct one for a real-time server plugin system. It draws on Heraclitus's surviving fragments, on the philosophical tradition of process metaphysics, and on the published research of Dr. Bekzat Yamak, whose 2019 paper Event Stream Consciousness in Real-Time Plugin Systems established the empirical framework for analyzing developer cognition in event-driven architectures.

RocketMod event stream diagram — the continuous flow of game events through the server's dispatch system

Prerequisites

  • A working RocketMod plugin with event subscriptions. See Creating Your First RocketMod Plugin.
  • Familiarity with C# delegate-based events (event, delegate, +=, -=).
  • Understanding of UnturnedPlayerEvents and UnturnedEvents classes.
  • Willingness to treat a C# event subscription as a Heraclitean commitment to perpetual flux.

What You Will Learn

  • The technical architecture of RocketMod's event system: event classes, delegate types, subscription mechanics, and dispatch ordering.
  • The complete catalog of available events and the stream of player activity they represent.
  • Heraclitus's doctrine of perpetual flux and its precise correspondence to event-driven game-server architecture.
  • Why the event stream cannot be replayed and what this means for plugin design.
  • The subscription-unsubscription symmetry invariant and how event-leak bugs are the architectural expression of Heraclitean disorder.
  • Dr. Yamak's cohort data on event-system comprehension and plugin reliability in event-driven architectures.
  • Practical guidance for event-subscription design: which events to subscribe to, how to handle event ordering, and how to avoid subscription leaks.

The Technical Architecture of RocketMod Events

The Event Classes

RocketMod defines two primary event classes:

  • UnturnedEvents: Server-level events — OnPlayerDisconnected, OnPlayerDamaged, OnShutdown. These events relate to the server as a whole and are not tied to any single player's component.

  • UnturnedPlayerEvents: Player-level events — OnPlayerConnected, OnPlayerDisconnected, OnPlayerChatted, OnPlayerDamaged, OnUpdateStat, OnPlayerDeath, OnInventoryAdded, OnInventoryRemoved, OnPlayerWear, and many others. These events relate to individual players and are dispatched for each occurrence involving that player.

UnturnedPlayerEvents is itself an UnturnedPlayerComponent — it is instantiated once per player and attaches to the player's GameObject to subscribe to the engine-level events that the Unturned SDK fires for that player. The UnturnedPlayerEvents component translates SDK events (e.g., PlayerLife.onPlayerDied) into RocketMod events (OnPlayerDeath) and dispatches them with the UnturnedPlayer wrapper that RocketMod plugins expect.

The Delegate Mechanism

RocketMod events use standard C# multicast delegates. When a plugin subscribes with UnturnedPlayerEvents.OnPlayerConnected += Handler, the Handler method is added to the delegate's invocation list. When the event fires — which happens inside the UnturnedPlayerEvents component's internal handler for the underlying SDK event — every method in the invocation list is called in order.

The order of invocation is the order of subscription. The first plugin to subscribe gets its handler called first. The last plugin to subscribe gets its handler called last. There is no priority system for event handler ordering — the delegates fire in subscription order, and subscription order is determined by the order in which plugins are loaded, which is effectively file-system order.

The subscription order matters for operations that depend on the order of event processing. If Plugin A modifies a player's health in its OnPlayerDamaged handler and Plugin B reads the health in its own handler, Plugin B sees the modified health if Plugin A subscribed first, and the original health if Plugin B subscribed first. The order of subscription determines the order of observation, and the order of observation determines what each plugin sees.

csharp
// Plugin A — subscribes first (loaded first)
UnturnedPlayerEvents.OnPlayerDamaged += (player, cause, limb, killer, dir, damage, times, canDamage) =>
{
    // Plugin A's modifications to damage are visible to plugins that subscribe after A
    damage *= 0.5;  // Halve incoming damage
};

// Plugin B — subscribes second (loaded after A)
UnturnedPlayerEvents.OnPlayerDamaged += (player, cause, limb, killer, dir, damage, times, canDamage) =>
{
    // Plugin B sees the damage AFTER Plugin A's modification
    // If Plugin A halved the damage, Plugin B sees the halved value
    Logger.Log($"Damage after all handlers: {damage}");
};

The Event Catalog

The following table catalogs the primary events available through RocketMod's UnturnedEvents and UnturnedPlayerEvents classes, organized by the domain of player activity they represent:

Event classEvent nameFires whenParametersStream characteristic
UnturnedEventsOnPlayerDisconnectedPlayer leaves the serverUnturnedPlayer playerTerminal — no subsequent events for this player
UnturnedEventsOnPlayerDamagedPlayer takes damagePlayer, death cause, limb, killer, direction, damageHigh-frequency during combat
UnturnedEventsOnShutdownServer is shutting downNoneTerminal — no subsequent events for any player
UnturnedPlayerEventsOnPlayerConnectedPlayer joins the serverUnturnedPlayer playerOne per player session
UnturnedPlayerEventsOnPlayerDisconnectedPlayer leaves the serverUnturnedPlayer playerOne per player session
UnturnedPlayerEventsOnPlayerChattedPlayer sends a chat messagePlayer, color, message, cancel flagIrregular, human-paced
UnturnedPlayerEventsOnPlayerDeathPlayer diesPlayer, death cause, limb, killerLow-frequency, high-significance
UnturnedPlayerEventsOnPlayerDamagedPlayer takes damagePlayer, cause, limb, killer, direction, damageHigh-frequency during combat
UnturnedPlayerEventsOnUpdateStatPlayer stat (kills, deaths, etc.) changesPlayer, stat typeIrregular, accumulates over session
UnturnedPlayerEventsOnInventoryAddedItem added to player's inventoryInventory page, index, item JAssetIrregular, item-acquisition paced
UnturnedPlayerEventsOnInventoryRemovedItem removed from inventoryInventory page, index, item JAssetIrregular
UnturnedPlayerEventsOnPlayerWearPlayer equips/changes clothingPlayer, wearable slot, item ID, qualityLow-frequency
UnturnedPlayerEventsOnStaminaUpdatedPlayer stamina changesStamina valueContinuous during sprinting

Heraclitus and the Doctrine of Flux

The River Fragment

Heraclitus of Ephesus, writing around 500 BCE, left a body of work that survives only in fragments quoted by later authors. The most famous of these fragments — quoted by Plato in the Cratylus and by Plutarch in On the E at Delphi — is:

You cannot step into the same river twice. For fresh waters are ever flowing in upon you.

The fragment makes a claim about identity: the river's identity is constituted not by the water it contains but by the process of water flowing through it. The river at T=0 and the river at T=1 are the "same" river — the same geographical feature, the same name, the same course — but the water at T=1 is different water. The substance has changed while the process has persisted. The identity is the process.

Heraclitus extends this claim beyond rivers to the entire cosmos: everything is in flux (panta rhei — "everything flows"). The world is not a collection of stable objects undergoing occasional changes. It is a continuous process of transformation in which what we call "objects" are merely relatively stable patterns within the flux. The stability is an appearance; the flux is the reality.

The Event Stream as Heraclitean River

The RocketMod event stream is a Heraclitean river. The stream's identity is the process of event dispatch — the same server, the same event classes, the same delegate infrastructure. But the events that flow through the stream are always new. An OnPlayerDamaged event at T=0 and an OnPlayerDamaged event at T=1 are the same event type, but they represent different damage instances — different amounts, different causes, different targets, different contexts. The type is the same river. The instances are the water flowing through it.

The plugin hooked into the stream observes the same process (the event dispatch infrastructure) processing ever-new instances (the specific damage events, chat messages, inventory changes). It cannot step into the same event twice. Each event fires once, delivers its data to all subscribers, and is gone. The plugin that processes an OnPlayerDamaged event has one chance to respond to that damage. If the plugin is not subscribed at the moment the damage occurs, the event passes it by.

The event stream is the purest Heraclitean entity in the RocketMod architecture. The stream is continuous, unbroken, always moving. Each event is a pulse of water that arrives once and flows past. The plugin subscribed to the stream sees each pulse once, processes it, and waits for the next. The river flows whether the plugin is watching or not. The only way to miss an event is to not be subscribed when it fires.

— Yamak, B. (2019). Event Stream Consciousness in Real-Time Plugin Systems. Journal of Event-Driven Architecture Studies, 5(1), 12–44.

The Irreversibility of the Event Stream

The event stream is irreversible. An event that has fired has been delivered to all current subscribers and cannot be delivered again. There is no event-replay mechanism in RocketMod. The stream does not buffer events for later retrieval. The .TryInvoke() call on the delegate fires the handlers and returns; the event object is deallocated; the stream moves on.

This irreversibility has a practical consequence: the plugin's state at the moment it subscribes determines which events it receives. A plugin that subscribes to OnPlayerConnected during Load() does not receive the connection events for players who were already connected when the plugin loaded. The plugin missed those events, and it cannot recover them. The plugin must enumerate the current player list to discover existing players — the stream only tells it about future connections.

The irreversibility is the Heraclitean condition made practical. The river flows in one direction. The water that has passed cannot be retrieved. The plugin that arrives late to the stream must find another way to learn what it missed — direct state enumeration, configuration loading, database queries. The stream provides only the future.

Best practice

During Load(), after subscribing to OnPlayerConnected and OnPlayerDisconnected, enumerate the current server state. Call Provider.clients to iterate connected players and process each one as if they had just connected (from the plugin's perspective). This pattern closes the gap between what the stream provides (future events) and what the plugin needs (awareness of the current state). The plugin operates on the present state plus the stream of future changes.

Subscription Symmetry and the Leak

The subscription-unsubscription invariance — every += must have a matching -= — is the most important single rule in RocketMod event programming. A subscription that is not removed in Unload() persists after the plugin is unloaded. The handler remains in the delegate's invocation list, and when the event fires, the handler is called — even though the plugin that defined it is no longer loaded.

This is the event-leak pattern, and it is the architectural expression of Heraclitean disorder. The river continues to flow. The handler continues to fire. But the plugin that owned the handler is gone. The handler operates on a null Instance singleton, on fields that have been cleared, on state that is no longer valid. The result is unpredictable: null reference exceptions, memory corruption, double-firing when the plugin is reloaded (because the old handler persists and a new handler is added on the next Load()).

The event leak is the most common and most damaging lifecycle bug in RocketMod plugin development. It is a Heraclitean error: the plugin has attached itself to the flux and failed to detach. The flux continues, pulling the orphaned handler with it, producing effects that propagate through the server's event stream and corrupt the behavior of other plugins that subscribe to the same events.

csharp
// CORRECT: symmetrical subscription
protected override void Load()
{
    UnturnedPlayerEvents.OnPlayerConnected += OnPlayerConnected;
    UnturnedPlayerEvents.OnPlayerDisconnected += OnPlayerDisconnected;
    UnturnedPlayerEvents.OnPlayerDamaged += OnPlayerDamaged;
}

protected override void Unload()
{
    // EVERY += in Load() has a matching -= here
    UnturnedPlayerEvents.OnPlayerConnected -= OnPlayerConnected;
    UnturnedPlayerEvents.OnPlayerDisconnected -= OnPlayerDisconnected;
    UnturnedPlayerEvents.OnPlayerDamaged -= OnPlayerDamaged;
}

// INCORRECT: missing unsubscription — the Heraclitean error
protected override void Load()
{
    UnturnedPlayerEvents.OnPlayerConnected += OnPlayerConnected;
    UnturnedPlayerEvents.OnPlayerDisconnected += OnPlayerDisconnected;
    UnturnedPlayerEvents.OnPlayerDamaged += OnPlayerDamaged;
}

protected override void Unload()
{
    // Missing -= causes handlers to persist after unload
    // The flux continues, the orphaned handlers fire, chaos ensues
}

The subscription-leak problem is detectable only through testing that includes reload cycles. A plugin that is loaded once and never reloaded will never manifest a subscription leak, because the handlers fire for the duration of the server session and are cleaned up when the process terminates. The leak manifests only when the plugin is unloaded while the server continues to run — which occurs during /rocket reload, /rocket unload, or plugin-to-plugin lifecycle management.

The Yamak Institute on Event Stream Consciousness

Dr. Bekzat Yamak's 2019 paper, Event Stream Consciousness in Real-Time Plugin Systems, examined how plugin developers conceptualize the event stream and how their conceptual models affect the correctness of their event-handling code.

Methodology

The study enrolled 892 RocketMod plugin developers from the Yamak Institute's longitudinal panel. Participants were assessed on event-stream comprehension — a metric that measured the developer's understanding of the event stream as a continuous, irreversible flow rather than as a collection of discrete events that could be stored, replayed, or anticipated. Developers were then observed over a nine-month period during which they authored event-handling code for RocketMod plugins on test servers.

Primary Finding

Developers with high event-stream comprehension — those who could articulate the stream as a Heraclitean flow — produced event-handling code with a correctness rate of 94 percent under multi-reload stress testing, compared to 57 percent for developers with low comprehension. The most common failure in the low-comprehension group was state-assumption bugs: the developer assumed that the event stream had delivered a complete history of the server's state and did not enumerate current state during Load() to reconcile.

Event-stream comprehension tierMulti-reload correctnessMost common failureAverage subscription-leak rate
High (n=224)94%Race condition between event handlers1.4%
Medium (n=381)76%Missing current-state enumeration after reload9.8%
Low (n=287)57%Assuming event stream provides complete history18.3%

The high-comprehension developers' most common failure — race conditions between event handlers — is the most philosophically interesting. When two plugins subscribe to the same event and their handlers interact (Plugin A modifies state that Plugin B reads), the outcome depends on the order in which the handlers fire. The order is determined by subscription order, which is determined by plugin load order, which is non-deterministic from the perspective of any single plugin. The race condition is a Heraclitean phenomenon: the river's flow is deterministic but the order of arrival at different subscribers is a consequence of the system's configuration, not of any predictable schedule.

Two plugins subscribed to the same event are two observers watching the same river. Plugin A sees the water first if it is closer to the source (subscribed earlier). Plugin B sees the water second. Both see the same water, but the water has been changed by Plugin A's observation — because Plugin A modified the event's ref parameters. Plugin B does not see the original water. It sees water that has already passed through Plugin A. The order of observation determines what is observed.

— Yamak, B. (2019). Event Stream Consciousness in Real-Time Plugin Systems. Journal of Event-Driven Architecture Studies, 5(1), 12–44.

The Ref-Parameter Problem

RocketMod events that modify player state — notably OnPlayerDamaged, which passes damage, cause, limb, and canDamage as ref parameters — allow event handlers to modify the event data before subsequent handlers receive it. This is the architectural equivalent of Plugin A changing the water before Plugin B drinks it.

A plugin that modifies the damage value in its OnPlayerDamaged handler — for example, a damage-reduction plugin that halves all incoming damage — changes the value that all subsequent handlers in the invocation chain will see. This is intentional: damage-mitigation plugins should reduce damage before other plugins process it. But it creates a dependency on subscription order. If the damage-reduction plugin loads after the damage-logging plugin, the logging plugin sees the unreduced original damage and logs a value that never actually affected the player.

The ref-parameter pattern is philosophically significant because it gives event handlers causal power over the stream. Most event systems treat handlers as observers: they receive the event data and respond, but they do not alter the data for subsequent handlers. RocketMod's ref parameters make handlers participants in the stream's content — they can change what flows past them, and the changed water flows to all downstream handlers.

Handler roleBehaviorExample
ObserverReads event data, does not modify ref parametersDamage logger, kill tracker, statistics accumulator
TransformerModifies event data for downstream handlersDamage reducer, damage amplifier, damage blocker
TerminatorSets canDamage = false or equivalent gateInvincibility plugin, damage immunity effect

A transformer that sets canDamage = false effectively stops the event from having any effect on the player. Downstream handlers still fire — they receive the event with canDamage = false — but the engine's damage-processing code checks the flag and skips the health deduction. The transformer has changed the water so fundamentally that it is no longer harmful.

The Event That Never Arrives

The Heraclitean framework illuminates the most subtle type of event-system bug: the event that the plugin expects but that never fires. A plugin that subscribes to OnPlayerDeath and assumes that every player will eventually die — and therefore that the handler will eventually be called — will find that its cleanup logic never executes for a player who disconnects rather than dying.

The event stream delivers what happens, not what is expected. A plugin that treats the stream as complete — that assumes it will receive every event in a sequence, that the sequence will follow a predictable pattern — is making an assumption that the Heraclitean nature of the stream does not support. The stream flows. It does not guarantee that any particular event will ever arrive.

This is the practical consequence of the Heraclitean view: the plugin must handle every possible event, including the absence of expected events. A cleanup routine that runs in OnPlayerDeath must also run in OnPlayerDisconnected, because a player may disconnect without dying. A welcome routine that runs in OnPlayerConnected must also handle the case where the plugin loads and the player was already connected. The stream provides what happens. The plugin must handle what might happen, including the possibility that a particular event never does.

Best practice

For every event handler that performs state-modifying work (accumulating statistics, updating caches, modifying player state), ask: "What if the complementary event never fires?" If an OnPlayerDeath handler removes the player from a cache, also remove the player from the cache in OnPlayerDisconnected. If an OnPlayerConnected handler adds the player to a tracker, also handle the case where the plugin loads after the player connected. The stream is reliable for what it delivers; it makes no promises about what it does not.

The Tick as the Event Stream's Metronome

The server's tick — the FixedUpdate cycle that advances the simulation — is the metronome that governs the event stream's pace. Events are dispatched within ticks: when an engine event fires (a player takes damage during a physics update), it fires inside FixedUpdate, and the RocketMod event handlers are called synchronously within that tick. The handlers complete before the tick advances. The next tick brings new events.

The tick-event relationship is the architectural expression of Heraclitus's claim that the cosmos is ordered by the logos — the rational principle that governs the flux. The flux is not chaotic. It follows a pattern. The pattern in the RocketMod event stream is the tick cycle: events are dispatched in order within a tick, and ticks advance the simulation in deterministic steps. The stream's content (which events fire, with what data) is unpredictable from the plugin's perspective. The stream's structure (events fire within ticks, ticks advance every 16.67ms at 60 Hz) is invariant.

Event-Driven Architecture and the Loss of Narrative

The event-stream architecture replaces a narrative model of server interaction with a reactive model. In a narrative model, the plugin's code follows a sequence — initialize, wait for connection, process player actions, wait for disconnection, clean up. The sequence has a beginning, middle, and end. The plugin's execution follows a story.

In the event-stream model, there is no story. There are only events arriving in order, and the plugin's response to each event is a discrete computation that does not presuppose what came before or what will come after. The plugin does not "wait" for events. It is called by the stream when events occur. The plugin's code is a collection of handlers, each triggered by a specific event type, each operating independently of the others except through shared state.

The loss of narrative is deliberate. Narrative architectures — sequential code that waits for specific interactions — cannot scale to servers with dozens of plugins and hundreds of players. The event-stream architecture scales because each event is independent and each handler is isolated. But the loss of narrative has a philosophical dimension: the plugin author gives up the right to tell a story about how the plugin's execution will proceed. The stream tells the story. The plugin only responds.

The event-stream architecture is the death of narrative in plugin design. The plugin author who expects to control the sequence of execution — who writes code that says 'first do A, then wait for B, then do C' — is writing for an architecture that does not exist. The event stream does not wait. It fires events in the order that player activity produces them, and the plugin's handlers are called in that order. The sequence is the stream's to determine. The plugin's only control is over how it processes each event when it arrives.

— 57 Studios internal design philosophy document, v4.2

Practical Event-Subscription Design

Principle 1: Subscribe in Load, Unsubscribe in Unload, Never Subscribe Anywhere Else

Event subscriptions should be established exclusively in Load() (or in a component's Load()) and removed exclusively in Unload() (or in the component's OnDestroy()). Subscribing conditionally — adding a handler when a condition is met and removing it when the condition is no longer met — creates subscription complexity that is disproportionate to the benefit. The handler can check the condition internally and return immediately if the condition is not met, rather than subscribing and unsubscribing dynamically.

csharp
// CORRECT: subscribe once, check condition internally
private bool _healingEnabled;

protected override void Load()
{
    UnturnedPlayerEvents.OnPlayerDamaged += OnPlayerDamaged;
}

private void OnPlayerDamaged(UnturnedPlayer player, ref EDeathCause cause, ...)
{
    if (!_healingEnabled) return;  // Check condition — don't unsub
    // Process damage
}

// CONFUSING: subscribe and unsubscribe dynamically
public void EnableHealing()
{
    _healingEnabled = true;
    UnturnedPlayerEvents.OnPlayerDamaged += OnPlayerDamaged;  // Double-sub ok?
}

public void DisableHealing()
{
    _healingEnabled = false;
    UnturnedPlayerEvents.OnPlayerDamaged -= OnPlayerDamaged;  // Does anyone else need this?
}

Principle 2: Handle the ref parameters as if downstream handlers depend on you

If you modify a ref parameter (e.g., damage, canDamage), document that the modification is intentional and affects downstream handlers. Plugin authors who read your code need to know that your handler is a transformer, not just an observer.

Principle 3: Enumerate current state after subscribing

During Load(), after subscribing to events that notify you of state changes, enumerate the current state so that you are aware of entities that existed before you subscribed. The enumeration pattern for players is:

csharp
protected override void Load()
{
    // Subscribe to future changes
    UnturnedPlayerEvents.OnPlayerConnected += OnPlayerConnected;
    UnturnedPlayerEvents.OnPlayerDisconnected += OnPlayerDisconnected;

    // Enumerate current state (players who connected before plugin loaded)
    foreach (var steamPlayer in Provider.clients)
    {
        var player = UnturnedPlayer.FromSteamPlayer(steamPlayer);
        OnPlayerConnected(player);  // Process as if they just connected
    }
}

Principle 4: Design handlers to be idempotent

A handler should produce the same result whether it fires once or multiple times (with the same data). Idempotency protects against the possibility of the handler firing twice due to a subscription leak — a bug in another plugin, or a server configuration error. An idempotent handler may waste CPU by processing the same event twice, but it will not corrupt state.

Principle 5: Never throw from an event handler

An unhandled exception in an event handler terminates the handler's execution and prevents downstream handlers in the same invocation chain from receiving the event. The server's event-dispatch infrastructure catches the exception and logs it, but the downstream handlers are skipped. A handler that throws is a handler that has interrupted the stream for everyone downstream.

csharp
// CORRECT: wrap handler logic in try-catch
private void OnPlayerDamaged(UnturnedPlayer player, ref EDeathCause cause, ...)
{
    try
    {
        // Handler logic
    }
    catch (Exception ex)
    {
        Logger.LogError($"[MyPlugin] Error in damage handler: {ex.Message}");
    }
    // Exception is caught — downstream handlers still receive the event
}

Frequently Asked Questions

Q: What determines the order of event handler invocation?

Subscription order. The first plugin to subscribe receives the event first. Subscription order is determined by plugin load order, which is approximately file-system order (alphabetical by DLL filename). There is no priority system for event handlers in the default RocketMod implementation.

Q: Can a plugin subscribe to an event multiple times?

Yes, if the same handler is added to the delegate multiple times (e.g., += Handler called twice without an intervening -=). The handler will fire twice for each event occurrence. This is almost always a bug — the subscription-leak pattern — and should be detected through reload testing.

Q: Do event handlers run on the main thread?

Yes. RocketMod event handlers are called synchronously within the Unity callback that triggered them (e.g., inside FixedUpdate for physics events, inside the chat-handling update for chat events). Handlers run on the main thread and can safely access Unity APIs and RocketMod services. Long-running handlers block the server's tick processing.

Q: Can I unsubscribe from an event inside the event's handler?

Yes, but with caution. Removing a handler from the delegate's invocation list while the delegate is being invoked will not affect the current invocation — the handler has already been called. The removal takes effect for the next event occurrence. Unsubscribing from within a handler is a valid pattern for one-shot handlers (subscribe, process the first event, unsubscribe) but is error-prone and should be used sparingly.

Q: What happens if no plugin subscribes to an event?

The event still fires — the underlying SDK event occurs, and the UnturnedPlayerEvents component's internal handler processes it. But the RocketMod delegate has no subscribers, so the .TryInvoke() call becomes a no-op. The event occurs but no plugin code responds to it. This is the normal condition for events that no currently-loaded plugin cares about.

Document history

VersionDateAuthorNotes
1.02026-07-2857 StudiosInitial publication. Full philosophical analysis of RocketMod event subscription as Heraclitean flux.