Skip to content

Client and Reflex

A regular module contains one client runtime. A Reflex module adds a separate server runtime and a transport between them:

client input/UI ── intent ──▶ Reflex decision
client display ◀─ state ─── Reflex result

The runtimes do not share variables, live objects or callbacks. A helper from src/shared/ may be imported by both entries, but it is bundled and executed as a separate copy in each runtime. Only serialized messages cross the live client/Reflex boundary.

WorkOwnerWhy
HUD, UXML, cameras, sound, hintsClientThese APIs exist only where the game renders them
Module controls and local key stateClientInput actions are client-only
Visual lines and overlaysClientThey are presentation, not server entities
Decisions the server API can observe and applyReflexThey do not need to trust client-reported state
Server timing and repeated checksReflexThey should not wait for client frames or round trips
Showing a confirmed server resultClientReflex sends data; client chooses how to present it

Ask: which runtime can observe the required state, apply the effect and verify the result? Put the decision there. Reflex runs on the server, but its module API is intentionally limited and visibility-filtered; server location alone does not make every possible decision authoritative. A client can display predicted or pending state, but it should not become the source of truth for state Reflex can enforce.

A client-only module needs:

src/client/main.ts

A Reflex module adds:

src/server/main.ts
src/shared/messages.ts

Client files import @killscript/sdk/client; server files import @killscript/sdk/server. The compiler rejects a client implementation import from server code and the reverse. Plain interfaces and message types belong in src/shared/.

// Client: report what the player requested.
controls.toggle.onPressed(() => {
coreNetwork.send("setEnabled", { enabled: !displayedEnabled });
});
coreNetwork.on("stateChanged", ({ enabled }) => {
displayedEnabled = enabled;
updateHud(enabled);
});
// Reflex: own and confirm the state.
let enabled = false;
coreNetwork.on("setEnabled", ({ enabled: requested }) => {
const local = Agents.GetLocalAgent();
enabled = local?.Stats.IsAlive === true && requested;
coreNetwork.send("stateChanged", {
enabled,
changedAtTick: Time.Tick,
});
});

The client never claims that the request succeeded. It updates from the state Reflex accepted. Conversely, Reflex should not ask the client for information already available from server globals.

See Typed networking for the complete protocol declaration and same-tick batching behavior.

// Client: presentation follows frames.
import { scheduler } from "@killscript/sdk/client";
scheduler.frame(updateCamera);
// Reflex: simulation follows server ticks.
import { scheduler } from "@killscript/sdk/server";
scheduler.tick(updateServerState);

There is no client OnTick or Reflex OnFrame in the exposed API. Use Time.Tick inside a client frame callback when presentation needs to notice a new simulation tick.