Plugins

Plugins add behavior to the World: manual move, trajectory missions, race. They turn TCP commands into events (which become ECS writes → physics) and may draw overlays. There is one base class — Plugin (robosim/plugin.py, level 3) — and one orchestrator — PluginsManager (level 4). Concrete plugins live in server/plugins/ (app-level), registered by name and selected with --plugins:

# server/server.py
PluginsManager.REGISTERED_PLUGINS = {
    "manual_move":        ManualMovePlugin,        # default
    "trajectory_mission": TrajectoryMissionPlugin,
    "race":               RacePlugin,
}
# ./server/server.py --plugins manual_move race

A plugin handles a set of commands and answers each with a Response:

class Plugin(Drawable, Restorable, ABC):
    @property
    def commands(self) -> list[str]: ...                       # which TCP commands route here

    # Required: one message → one Response (ok / err / physics).
    def on_message_receive(self, world, entity_id, message) -> Response: ...

    # Required: final reply for a `physics` Response, after physics ran (so it can read statuses).
    def on_message_response(self, world, event) -> Response: ...

    # Optional: internal events with no message (wind, a running mission's motion). These get no reply.
    def on_tick(self, world) -> list[Event]: ...

    # Optional: hooks around the physics system.
    def on_before_physics(self, world): ...
    def on_after_physics(self, world): ...

    # Optional: draw overlays. Called once for the main camera, once per FPV robot (entity_id set).
    def draw(self, world, entity_id: int | None = None): ...

Response has three constructors, one per kind — the kind picks the path through the tick:

Response.ok("move_applied", new_state=...)   # success → wire reply as-is, skips physics
Response.err("control_input not provided")   # error   → wire reply as-is, skips physics
Response.physics(motion_input=ctrl_input)    # → Event → write ECS → physics → on_message_response

Message flow within a tick

The PluginsManager routes every inbound TCP message to the plugin(s) that own its command, gathers the resulting events, runs physics, then sends exactly one reply per message. io_handler and responses_handler bracket the tick: the first turns inbound messages into events and applies them to the World, the last guarantees every message that arrived gets exactly one reply by tick end.

                              ┌──────────────────────────────────────────────────┐
                              │                PLUGINS MANAGER                   │
        TCP channels          │                                                  │
       ┌──────────┐           │   ┌─────────┐   ┌─────────┐   ┌─────────┐        │
       │ Robot 0  │───msg────►│   │ Manual  │   │ Mission │   │  Race   │  ...   │
       │ channel  │           │   │  Move   │   │ Plugin  │   │ Plugin  │        │
       └──────────┘           │   └────┬────┘   └────┬────┘   └────┬────┘        │
       ┌──────────┐           │        │  on_message_receive / on_tick           │
       │ Robot 1  │───msg────►│        └────────────┬──────────────┘  → Events   │
       │ channel  │           │                     ▼                            │
       └──────────┘           │   ┌──────────────────────────────────────────┐   │
              ▲               │   │ io_handler: Events → write ECS data       │   │
              │               │   │ (plugin order = priority: mission beats   │   │
              │               │   │  manual move for the same robot)          │   │
              │               │   └─────────────────┬────────────────────────┘   │
              │               │                     ▼                            │
              │               │   ┌──────────────────────────────────────────┐   │
              │               │   │ PhysicsSystem (× subticks) over the World:│   │
              │               │   │ integrate → detect → resolve → commit     │   │
              │               │   └─────────────────┬────────────────────────┘   │
              │               │                     ▼                            │
              │               │   ┌──────────────────────────────────────────┐   │
              │               │   │ responses_handler: on_message_response →  │   │
              │               │   │ one reply per message (reads statuses)    │   │
              │               │   └─────────────────┬────────────────────────┘   │
              │               └─────────────────────┼────────────────────────────┘
              │                                     │
              └───────────────response──────────────┘

Before writing a plugin, read the tick invariants on the Architecture & Main Loop page — they spell out the guarantees this flow relies on (one reply per message, Response.kind picks the path, address entities by id, plugin order = priority, durable state lives in components).