Command System

Commands represent actions that change game state. They are the standard way to perform mutations in Spark: giving items, dealing damage, accepting quests, modifying reputation, and so on.

Why Commands?

Commands exist for two reasons:

  1. Validation: Every action goes through a handler that can validate inputs before applying changes.

  2. Networking: Commands are routed through the network provider. In single-player, they execute locally. In multiplayer, they can be sent to the server for authoritative execution. The calling code doesn't need to know which mode it's in.

ICommand Interface

ICommand is a marker interface with no methods or properties:

public interface ICommand { }

Commands are simple data containers. They hold the parameters needed to perform an action but contain no logic:

public class GiveItemCommand : ICommand
{
    public string TargetEntityId { get; set; }
    public string ItemEntryId { get; set; }
    public int Quantity { get; set; }
}

ICommandHandler Interface

The handler contains all the logic for processing a command:

A typical handler follows this pattern:

The four-step pattern is consistent across all Spark command handlers: validate, find, apply, publish.

Registering Command Handlers

Register handlers in your plugin's registration method:

Each command type has exactly one handler. Registering a handler for a type that already has one will overwrite the previous handler.

Executing Commands

Execute commands through the network provider:

The ?. null-conditional is important. If no network provider is set up, the command silently does nothing rather than throwing.

How Execution Works

When ExecuteCommand is called:

  1. The network provider receives the command.

  2. It looks up the registered handler for the command's type.

  3. It invokes Handle() on the handler using reflection-based dispatch (in NetworkProviderBase).

  4. The handler runs its validation and logic.

In single-player mode, LocalNetworkProvider executes everything immediately and locally. In multiplayer, a custom network provider could serialize the command and send it to the server.

LocalNetworkProvider

LocalNetworkProvider is the default provider. It:

  • Registers automatically if no other provider is set

  • Executes all commands immediately and locally

  • Reports IsServer => true (since single-player is effectively the server)

  • Registers built-in handlers like DragDropCommandHandler

You don't need to configure it. It just works.

Command Design Guidelines

Keep commands as simple data containers:

Commands should be serializable for networking compatibility. Use entry IDs (strings) instead of direct object references.

Best Practices

  • One command per action. Don't bundle multiple actions into one command.

  • Validate everything in the handler. Don't assume inputs are valid.

  • Always publish an event after applying state changes. This keeps the rest of the framework in sync.

  • Use descriptive names: {Verb}{Noun}Command (e.g., AcceptQuestCommand, CraftItemCommand).

  • Keep commands idempotent where possible. Running the same command twice should produce predictable results.

  • Log warnings for validation failures. This makes debugging much easier.

Last updated

Was this helpful?