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).