import type { FinalRestProvenance } from './final-rest.js'; import type { Vec3, Quat, AttachmentEntry } from './state.js'; import type { RoomVarValue } from './room.js'; /** * ADR-0003 World routes (item distributions with channel `world`). A seated player asks its room for a one-time * acquisition capability for ONE route of the room's world; the room mints it against the backend with its room * token and the SEAT's account, and answers only the asker. The capability then rides `POST /iwp/purchase`. * Bare string names, deliberately outside ClientMessageType: the room owns this lane (helix-colyseus-server * DISTRIBUTION_CAPABILITY_MESSAGE / _RESULT), and an older room simply never answers. */ export declare const DISTRIBUTION_CAPABILITY_MESSAGE = "distributionCapability"; export declare const DISTRIBUTION_CAPABILITY_RESULT = "distributionCapabilityResult"; /** `dkey:` or `distribution:`; the room refuses anything else. */ export type DistributionCapabilityRequestMessage = { requestId: string; ref: string; }; export type DistributionCapabilityResultMessage = { requestId: string; capability: string; expiresAt: string; ref: string; } | { requestId: string; error: string; }; export declare const ClientMessageType: { /** The player's predicted kinematic state for this tick (seq-tagged for reconciliation). */ readonly State: "state"; /** Activate or deactivate an ability on this player (the built-in, engine-level intent). */ readonly Ability: "ability"; /** A world-authored declared action (the generic extensibility channel — see ActionMessage). */ readonly Action: "action"; /** Play/stop a gesture on this player's character (v19 — see GestureMessage; oneshots relay, loops sync as state). */ readonly Gesture: "gesture"; /** Replace this player's full prop-attachment set (v20 — see AttachmentsMessage; pure synced state, no relay). */ readonly Attachments: "attachments"; /** Forward a backend-minted purchase receipt to the room (v22 — see PurchaseReceiptMessage; opaque JWT, room-verified). */ readonly PurchaseReceipt: "purchaseReceipt"; /** Ask the room to re-resolve this player's avatar (v23 — see AvatarRefreshMessage; hint only, empty payload). */ readonly AvatarRefresh: "avatarRefresh"; /** Ask the room to re-resolve this player's emote set (v28 — see EmoteRefreshMessage; hint only, empty payload). */ readonly EmoteRefresh: "emoteRefresh"; /** Join (or leave) another player's paired emote (v28 — see EmoteJoinMessage; a request the room may refuse silently). */ readonly EmoteJoin: "emoteJoin"; /** An owner-authoritative entity's client-simulated state for this tick (Tier 2 Phase 4.6c — see EntityStateMessage). */ readonly EntityState: "entityState"; /** * A BATCH of owner-authoritative entity uploads for this tick — ALL the entities this connection hosts in one * message (Tier 2 Phase 4.10 — see EntityStateBatchMessage). The per-connection rate cap counts MESSAGES, so a * host of N entities must batch (one message/tick) instead of N single EntityState messages, or it starves its * own upload budget. EntityScene uses this; the single EntityState stays for one-off uploads (e.g. a flag). */ readonly EntityStateBatch: "entityStateBatch"; /** Clock-sync probe: the client's local timestamp, which the room echoes in a Pong (RTT + offset). */ readonly Ping: "ping"; }; export type ClientMessageType = (typeof ClientMessageType)[keyof typeof ClientMessageType]; /** Server → client messages (the room sends these; SDK exposes them via room.onMessage(type, cb)). */ export declare const ServerMessageType: { /** Reply to a Ping: the client's echoed timestamp + the server's current time, for offset estimation. */ readonly Pong: "pong"; /** * A2 reconnection ack — the player-seat divergence correction (spec §9). Sent ONLY to the uploading client, in * two cases: the movement gate REJECTED its `state` frame, or a rule teleport/respawn re-anchored the seat (the * proactive ack — the mover learns NOW instead of via its next rejected upload). The plain accepted case is * silent — the accepted position is already in synced state. The client snaps its local body to the anchored * position, prunes its reconciliation buffer through the echoed seq, and (v27) adopts the teleport epoch it * carries. See StateAckMessage. */ readonly StateAck: "stateAck"; /** * A2 reconnection ack — the owner-hosted entity divergence correction (spec §9). Sent ONLY to the hosting client, * BATCHED (one message per tick acks every hosted entity whose upload the gate rejected — mirrors EntityStateBatch). * The host re-anchors each entity's local sim to the anchored position so its uploads stop being fenced. See * EntityStateAckMessage. */ readonly EntityStateAck: "entityStateAck"; /** * Oneshot gesture relay (v19): player X played world-registered clip Y — broadcast to every client EXCEPT the * sender (who already played it locally, client-predicted). Loops never use this channel — they sync via * PlayerState.loopGesture so late joiners see them. See GestureEventMessage. */ readonly Gesture: "gesture"; /** Authoritative transforms emitted when a declared physics group first reaches full rest. */ readonly GroupRestSnapshot: "groupRestSnapshot"; /** A module instance exhausted its per-activation work budget and was isolated. */ readonly ModuleFused: "moduleFused"; /** A sleeping/preloaded module began a fresh evaluation activation. */ readonly ModuleWake: "moduleWake"; /** An active/fused module became cold and left the active processing set. */ readonly ModuleSleep: "moduleSleep"; /** Result/revocation of an exclusive engaged-module authority request. */ readonly ModuleEngagement: "moduleEngagement"; /** Namespaced transient presentation emitted by a mounted module. */ readonly ModuleEffect: "moduleEffect"; /** A trusted root vehicle lifetime ended; recipients may retire its sparse action replay high-water. */ readonly VehicleEntityRetired: "__helix:vehicleEntityRetired"; /** Server-authoritative accepted sparse-action fences, replayed after join/reconnect. */ readonly VehicleActionFences: "__helix:vehicleActionFences"; /** * A ticket to a `ticketed` room instance, sent PRIVATELY to each player an `openRoom` rule effect selected * (room types, SPEC-U1). The client redeems it through the join API (`{ ticket }`) — see RoomTicketMessage. * Like the `__helix:*` channels the name sits outside the declared-name grammar (':' and '-'), so a world * event can never shadow it and it needs no RESERVED_MESSAGE_TYPES entry. Purely additive and server→client: * a bundle frozen on an older SDK registers no handler and simply never hops, and no client→server type was * added, so there is no room-before-republish ordering constraint and no CONTRACT_VERSION bump. */ readonly RoomTicket: "helix:room-ticket"; }; export type ServerMessageType = (typeof ServerMessageType)[keyof typeof ServerMessageType]; /** Per-tick state upload. Units match PlayerReplicaState (position m; all angles degrees, see *Deg). */ export interface StateMessage { /** Monotonic per-client sequence; the room echoes the last applied seq for client reconciliation. */ seq: number; position: Vec3; facingYawDeg: number; speed: number; moveDirectionDeg: number; verticalVelocity: number; grounded: boolean; crouched: boolean; /** Physics owns the body (v25) — see PlayerReplicaState.ragdolled. OPTIONAL on the wire like the v24 gun * fields: pre-v25 bundles omit it and must keep playing (the room leaves the schema default false). */ ragdolled?: boolean; /** The entry shove for THIS ragdoll (v26) — unit horizontal direction × m/s, meaningful only while * `ragdolled`. OPTIONAL like `ragdolled` itself: pre-v26 bundles omit them and replicas crumple unshoved. */ ragdollLaunchX?: number; ragdollLaunchZ?: number; ragdollSpeedMps?: number; aimYawDeg: number; aimPitchDeg: number; /** * Sender send-time — the client's monotonic `performance.now()` ms when it sampled this frame. The server * SANITIZES it into the synced `PlayerState.posT` (room.ts) for source-time interpolation: clients space keyframes * by `posT` deltas, not arrival time, so delivery jitter stops becoming motion jitter. Only deltas matter (the * origin is arbitrary). Optional — an older client omits it and the server falls back to its own apply-clock. */ tMs?: number; /** * The last server teleport epoch this client has ADOPTED from a StateAck (v27). Stamped by the SDK at flush * (like seq/tMs — worlds cannot supply it). The room drops a frame stamped BELOW the seat's current epoch: * that frame was sampled before a teleport/respawn the client hadn't applied yet, and accepting it would * silently undo the server's move (the in-flight-frame race). Strictly-below only — an unadopted HIGHER stamp * (post-reconnect) passes. Optional: a pre-v27 bundle omits it and the fence skips, like `epoch` on entity * uploads. */ teleportSeq?: number; /** * Live IK reach targets (v21), world METERS — present only while the sender's arm/gaze IK is actually * engaged (an inactive hand OMITS its field; the room then clears the seat's paired *Active flag). * IK_LIMITS-bounded relative to this message's `position`: a hand can't plausibly reach past arm's * length, a gaze point is capped at interaction range. Out-of-bounds drops the whole upload. */ reachLeft?: Vec3; reachRight?: Vec3; lookTarget?: Vec3; /** * Gun state (v24) — weapon SIGNALS, not motion: replicas re-run the same local easing/FX off these fields. * Omitted entirely when the sender carries no gun-control ability; an invalid value drops the whole upload. * * `shotSeq` is a monotonic per-session shot counter, +1 per real shot. It is coalesced latest-wins at * 10/20 Hz, so consumers react to the DELTA (never assume +1); it restarts at 0 on reconnect, and a * non-positive delta means re-anchor (never replay). */ shotSeq?: number; /** Weapon drawn (vs holstered). */ gunDrawn?: boolean; /** ADS intent, not an aim fraction — replicas ease locally with the same time constant. */ aiming?: boolean; /** Reload in progress (wire-timed on replicas). */ reloading?: boolean; } /** * An owner-authoritative entity's client-simulated state for this tick (Tier 2 Phase 4.6c, spec §9/§10.4). The * client that the server marked as the entity's `controller` (see EntityState.controller) simulates the entity * and uploads its transform + declared vars; the room sanity-validates (it owns this entity? movement within * its declared `maxSpeed`? vars within their declared bounds?) and relays. Cross-player consequences still go * through server-validated rules/actions — never this channel (the publish-time firewall enforces that). */ export interface EntityStateMessage { transactionId?: string; /** Optional module provenance; when present, `entity` is local to this module instance. */ moduleId?: string; /** The entity id this upload is for (the state.entities map key). Rejected unless the sender is its controller. */ entity: string; /** Monotonic per-entity sequence; the room ignores a stale/out-of-order frame. */ seq: number; /** The entity's world position, METERS — gated against the kind's declared maxSpeed. */ position: Vec3; /** * Physics state — uploaded ONLY for a `physics` (dynamic-body) kind (networked-physics, plan Phase 0). `velocity` * is gated against the kind's maxSpeed (magnitude-clamped on ingest, widened during a collision-grace window); * `orientation` is normalized (zero/non-finite → identity). Synced so remotes extrapolate with the right velocity * + reproduce spin (research §2). Omit for a kinematic kind. */ velocity?: Vec3; angularVelocity?: Vec3; orientation?: Quat; /** Declared per-kind vars to set (same vocabulary as roomVars); each is clamped to its declared type/bounds on ingest. */ vars?: Record; /** * The authority EPOCH the client is simulating under (Tier 2 Phase 4.5.12, spec §9). Mirror the entity's synced * `authorityEpoch`; the room bumps it on every controller handoff (host migration AND voluntary ownership * transfer) and rejects an upload whose epoch ≠ the entity's current one — fencing a revoked/stale authority * era even from a still-connected prior owner whose sessionId would otherwise pass. Omit only for the very * first frame before the field is observed; the room tolerates absence (sessionId ownership still gates). */ epoch?: number; /** * Sender send-time (the owner's monotonic `performance.now()` ms at upload) — the server SANITIZES it into the * entity's synced `posT` for source-time interpolation, exactly like the player's `StateMessage.tMs`. Optional. */ tMs?: number; } /** * One entity's slot in an EntityStateBatchMessage. Compact, short keys (sent verbatim in msgpack), with position * as a 3-tuple instead of a {x,y,z} object — the same authoritative meaning as EntityStateMessage's per-entity * fields. `v`/`ep` are omitted when unchanged/unobserved to keep the batch small. */ export interface EntityStateBatchEntry { transactionId?: string; /** Entity id (the state.entities map key). Rejected unless the sender is its controller. */ e: string; /** World position as [x, y, z], METERS — gated against the kind's declared maxSpeed. */ p: [number, number, number]; /** Linear velocity [x,y,z], m/s — physics kinds only; magnitude-clamped to the gate on ingest. Omit when unchanged/non-physics. */ lv?: [number, number, number]; /** Angular velocity [x,y,z], rad/s — physics kinds only. Omit when unchanged/non-physics. */ av?: [number, number, number]; /** Orientation quaternion [x,y,z,w] — physics kinds only; normalized (zero/non-finite → identity) on ingest. Omit when unchanged/non-physics. */ q?: [number, number, number, number]; /** Declared per-kind vars to set (clamped to declared type/bounds on ingest). Omit when unchanged this tick. */ v?: Record; /** Authority epoch this entity is simulated under (mirror its synced authorityEpoch). Omit before observed. */ ep?: number; } /** * A batch of owner-authoritative entity uploads (Tier 2 Phase 4.10, spec §10.4) — every entity this connection * hosts, in ONE message. The per-connection rate cap (MESSAGE_RATE.entityStateHz) counts messages, so batching * lets a host refresh many entities per tick within one token instead of spending one token per entity. The room * applies each entry exactly like a single EntityStateMessage (ownership + epoch + maxSpeed gate + var clamp). */ export interface EntityStateBatchMessage { finalRest?: { scope?: FinalRestProvenance; version: 1; requestId: number; group: string; transactionId: string; }; /** Optional module provenance shared by every local entity id in this batch. */ moduleId?: string; /** Monotonic per-connection batch sequence; the room ignores a stale/out-of-order batch per entity (gate seq). */ seq: number; /** One entry per hosted entity changed this tick (bounded by the room's per-owner entity cap). */ states: EntityStateBatchEntry[]; /** * Sender send-time (the host's monotonic `performance.now()` ms when it sampled this batch) — ONE per batch (all * entries are sampled the same frame); the server sanitizes it into each entity's synced `posT`. Optional. */ tMs?: number; } /** Discrete ability activation intent. */ export interface AbilityMessage { /** Ability id (must be one the world's build declares). */ ability: string; /** true = activate, false = deactivate. */ active: boolean; } /** * SDK-owned replay/authority fence for one named Universal Vehicle component * action. Authored item code supplies none of these values: the SDK resolves * the live entity epoch and immutable Build identity, then owns the sequence. */ export interface VehicleActionEnvelope { /** Exact authoritative moving-entity identity from the signed placement. */ entityId: string; /** Current room-issued entity authority era. */ authorityEpoch: number; /** Monotonic per-client/entity/authority-era action sequence. */ seq: number; /** Immutable vehicle Build identity read from the entity's protected vBuild state. */ buildDigest: string; /** * Optional desired uint16 bits inside this action's trusted server mask. * Omitted by legacy/toggle requests; when present the room validates and * atomically replaces that masked field instead of toggling arrival state. */ targetBits?: number; } /** Server-authenticated retirement of one root vehicle entity lifetime. Interest filtering never emits this. */ export interface VehicleEntityRetiredMessage { entityId: string; authorityEpoch: number; } /** Exact accepted sparse-action replay fences for this seat at one connection boundary. */ export interface VehicleActionFencesMessage { entries: Array<{ entityId: string; authorityEpoch: number; seq: number; }>; } /** * A world-authored declared action — the generic extensibility channel (e.g. 'captureFlag', * 'scorePoint'). The room validates `name` against the world's declared action set and `args` against * that action's declared arg schema before mutating authoritative state (table-driven dispatch); this * module carries the envelope + arg vocabulary only, never the per-world rules. */ export interface ActionMessage { finalRest?: { scope?: FinalRestProvenance; version: 1; requestId: number; group: string; }; /** Optional module provenance; absent addresses the byte-compatible implicit world program. */ moduleId?: string; /** Full mounted identity used by specialized item actions. */ itemInstanceId?: string; bundleChecksumSha256?: string; moduleRevision?: number; /** * Platform-owned engaged-module envelope. `acquire` requests the exclusive room grant for this * declared action; `release` relinquishes the current grant. Ordinary semantic verbs omit it. */ engagement?: 'acquire' | 'release'; /** SDK-generated correlation id, present only on acquire/release requests. */ engagementRequestId?: number; /** * Room-issued authority epoch. The SDK stamps it on release and every post-grant semantic verb; * authored item data never supplies or persists it. */ engagementEpoch?: number; /** Action name; must be one the world's build declares. */ name: string; /** Primitive/Vec3 args, validated against the declared action's schema by the room. */ args?: Record; /** * Optional strict vehicle-component fence on the existing action channel. * Legacy actions omit it; a trusted vehicle placement that declares named * component actions requires it and validates every field server-side. */ vehicleAction?: VehicleActionEnvelope; } /** How a GestureMessage applies: a transient oneshot, a persistent loop, or stopping the current loop. */ export type GesturePlay = 'oneshot' | 'loop' | 'stop'; /** * Play/stop a gesture on the sender's own character (v19). The clip name is OPAQUE to the room — gesture * registration is client-side world data (every client compiled the same clips from the same bundle), so the * room validates only structurally (play enum, name bounds — GESTURE_LIMITS) and never against a declared set. * Routing by `play`: 'oneshot' → relayed as a ServerMessageType.Gesture broadcast (except sender); * 'loop'/'stop' → written to the sender's synced PlayerState.loopGesture (no relay — schema sync carries it). */ export interface GestureMessage { /** Registered clip name; '' is legal only with play:'stop' (stop clears whatever loop is active, no name match required). */ clip: string; play: GesturePlay; } /** * The oneshot relay payload (v19), broadcast on ServerMessageType.Gesture to every client except the sender. * Receivers resolve `clip` against their own world-registered set and no-op (with a warn) on an unknown name. */ export interface GestureEventMessage { /** The gesturing player's playerKey (a state.players map key). */ player: string; /** The registered clip name to play as a oneshot on that player's replica. */ clip: string; } /** * Structural bounds on gesture clip names — enforced by the ROOM on GestureMessage (bounds/charset hygiene, * never vocabulary: names are opaque world data) AND by the ENGINE's pose-DSL compiler at registration, so a * name that registers is always a name that replicates. Identifier-ish charset: starts alphanumeric, then * alphanumerics plus `_ - . :` — printable, unambiguous as a registry key, safe in synced state. */ export declare const GESTURE_LIMITS: { readonly maxClipNameLength: 64; readonly clipNamePattern: RegExp; }; /** * Replace the sender's ENTIRE attachment set (v20). Full-set replace, idempotent — the desired set IS the * message, so there is no attach/detach op ordering to get wrong and a lost message heals on the next send. * Attachments are persistent → the room writes them to synced PlayerState.attachments (late joiners * converge); no server relay channel exists. The room validates structurally only (ATTACHMENT_LIMITS + * https on `asset`) — asset URLs and preset names are opaque world data, the gesture-name trust shape. */ export interface AttachmentsMessage { attachments: AttachmentEntry[]; } /** * Structural bounds on the attachment set — enforced by the ROOM on AttachmentsMessage and mirrored by the * ENGINE facade before sending, so a set that attaches locally is a set that replicates. */ export declare const ATTACHMENT_LIMITS: { readonly maxAttachments: 8; readonly maxIdLength: 32; readonly idPattern: RegExp; readonly maxAssetUrlLength: 512; readonly maxSocketNameLength: 64; readonly socketNamePattern: RegExp; readonly maxPresetLength: 32; /** Per-axis |offset| bound, METERS — grip tuning, not teleportation. */ readonly maxOffsetM: 2; }; /** * Structural bounds on a player's carried emote set (v28) — enforced by the ROOM when it writes the seat's * PlayerState.emotes. Unlike ATTACHMENT_LIMITS these bound a SERVER-resolved set, not an inbound message: the * platform is the source, and the room still bounds it so a bad catalog row can't put an unbounded string (or a * url a secure page would refuse to load) into replicated state. A clip URL must be SERVABLE — https anywhere, * plus http on loopback so a dev stack's own artifact URLs survive (the room mirrors the backend's * isServableClipUrl). Over-count, over-length, unservable and unknown-slot entries are dropped silently, never * fatally; an unservable `audio` costs only the sound, since the clip is the emote. */ export declare const EMOTE_LIMITS: { /** One per wheel slot ('r0'..'r7' + sit + lie) — the set cannot be larger than the wheel it plays from. */ readonly maxEntries: 10; /** Bounds EVERY url on an entry — `clip`, `audio` and the optional `intro`/`outro` transitions: each is fetched by every client, so they share one bound. */ readonly maxClipUrlLength: 512; /** Bounds the wheel petal's display title. */ readonly maxLabelLength: 64; /** * PAIRED emotes: per-axis |offset| bound on EmoteEntry.roles, METERS in the initiator-local frame. A pair is two * people arm's length apart (a handshake, a hug, a high-five) — the room CLAMPS to this rather than dropping the * roles, because an over-reaching catalog row should cost composition, not the whole paired emote. */ readonly maxPairOffsetM: 3; /** * Ceiling on EmoteEntry.roles.offerAtS, SECONDS — a sanity bound sitting far above any emote clip, since the * REAL bound is the initiator clip's own length and only publish can know it (the room just clamps ≥ 0 to this). */ readonly maxOfferAtS: 600; }; /** * Plausibility bounds on the v21 IK targets — enforced by the ROOM on StateMessage (an out-of-bounds * target invalidates the whole upload, like any other bad state field) and mirrored by the ENGINE facade * before sending. Distances are measured from the SAME message's `position` (world METERS), so the bound * tracks the player instead of being a useless absolute world-coordinate cap. */ export declare const IK_LIMITS: { /** Max |reach − position| — generous arm's length for any plausible rig scale. */ readonly maxReachDistanceM: 3; /** Max |lookTarget − position| — interaction gaze range, not a landmark stare. */ readonly maxLookDistanceM: 50; }; /** * Forward a settled purchase's backend-minted receipt to the room (v22). The SDK treats the JWT as OPAQUE — * it never parses it — and the room owns every runtime check: signature, audience (helix-purchase-receipt), * worldId, and that the claimed `sub` is the sender's own seat. Re-forwarding is safe (the room dedupes by * purchaseId), so a repeated getPurchase poll can send the same receipt again. */ export interface PurchaseReceiptMessage { receipt: string; } /** * Structural bound on the forwarded receipt — MAX_MESSAGE_BYTES is otherwise the only ceiling, so without * this an oversize receipt would be a mystery drop instead of a documented reject at the sender. */ export declare const RECEIPT_LIMITS: { readonly maxReceiptLength: 4096; }; /** * Ask the room to RE-RESOLVE this player's avatar (v23) — the seat's PlayerState.avatarUrl stops being fixed at * join. A pure HINT: the payload carries no fields (a client can never name the URL replicated onto its seat), * the room re-reads the equipped avatar from the platform under the sender's own credential, and a refresh that * resolves to the same URL is a no-op. Rate-capped like the other human-paced channels (a wardrobe change). */ export type AvatarRefreshMessage = Record; /** * Ask the room to RE-RESOLVE this player's carried emote set (v28) — the seat's PlayerState.emotes stops being * fixed at join. A pure HINT with the same shape and the same reason as AvatarRefreshMessage: the payload carries * no fields, because the room re-reads the equipped emotes from the platform under the sender's own credential * and a client must never be able to name a clip URL that lands on its seat. Rate-capped like the avatar hint (a * wheel change is the same human-paced wardrobe gesture, and each hint costs the room a platform re-resolve). */ export type EmoteRefreshMessage = Record; /** * Join another player's PAIRED emote, or leave the one you are in (v28). A REQUEST, not a command: the room * validates it (the initiator is live, offering a `roles` emote right now, is not the sender, and is within * reach) and answers by writing the sender's pair fields — or by writing nothing at all. There is no refusal * message: the sender sees its own PlayerState.pairInitiator stay '' and its local preview simply ends, which * is the same outcome a race with the initiator stopping produces anyway. */ export interface EmoteJoinMessage { /** The initiator's player id (a `players` map key), or '' to LEAVE the pair this player is in (always allowed). */ initiator: string; } /** * Clock-sync probe. The client sends its local send time; the room replies (Pong) with this value echoed * plus its own clock, so the client estimates RTT (recv − send) and offset (serverTimeMs − send − RTT/2). */ export interface PingMessage { /** The client's local clock (epoch ms) at send — echoed verbatim in the Pong. */ clientTimeMs: number; } /** Reply to a Ping — the echoed client time + the server's clock at reply, for offset/RTT estimation. */ export interface PongMessage { finalRestVersion?: 1; moduleFinalRestVersion?: 1; finalRestModules?: Record; clientTimeMs: number; serverTimeMs: number; } /** * A2 player-seat divergence ack (spec §9). Sent to ONE client when the room's movement gate rejected its `state` * upload, OR proactively when a rule teleport/respawn re-anchored the seat (the mover must learn the move now, * not via its next rejected frame). Under Tier-1 lean (no server physics) the correction is a thin SNAP: the * client moves its local body to `position` (no input replay). The plain accepted case sends nothing — the * accepted position is already in synced state; this fires only on divergence or a server-authored move. */ export interface StateAckMessage { /** The highest client `seq` the room has processed for this seat — the client prunes its reconciliation buffer through it. */ seq: number; /** The room's authoritative position for this seat (the last ACCEPTED upload, or the teleport target) — the anchor the client snaps to. */ position: Vec3; /** * The seat's current teleport epoch (v27), bumped by every rule teleport/respawn. The client adopts it * ABSOLUTELY (it can go DOWN after a reconnect resets the seat) and clears its coalesced pending frame on * change — a pre-snap position must never be stamped with the new epoch. Optional: an older room omits it. */ teleportSeq?: number; } /** * One entity's slot in an EntityStateAckMessage — compact short keys + a [x,y,z] tuple, mirroring EntityStateBatchEntry * (the upload it answers). */ export interface EntityStateAckEntry { /** Entity id (the state.entities map key). */ e: string; /** The last per-entity `seq` the room ACCEPTED for this entity — the host re-anchors/prunes through it. */ seq: number; /** The room's authoritative anchored position [x, y, z], METERS — the host snaps its local sim to it. */ p: [number, number, number]; /** Anchored linear velocity [x,y,z], m/s — physics kinds only, so a reconciled body resets its velocity too (else a stale spin/drift survives). Omit for non-physics. */ lv?: [number, number, number]; /** Anchored orientation quaternion [x,y,z,w] — physics kinds only, so a reconciled body resets its spin. Omit for non-physics. */ q?: [number, number, number, number]; } /** * A2 owner-entity divergence ack (spec §9). Sent to ONE hosting client, BATCHED: one message per tick carries an entry * for every entity this host uploaded whose position the gate rejected (mirrors the EntityStateBatch upload channel). * The host re-anchors each entity's local sim to its anchor, so a sparse/throttled host stops being fenced by the gate * — replacing the engine's prior conservative `0.83 × gate-reach` guess with the room's actual accepted position. */ export interface EntityStateAckMessage { /** Module provenance for every local entity id in `acks`; absent means world entities. */ moduleId?: string; /** One entry per hosted entity whose upload the gate rejected this tick. Never empty (the room omits the message instead). */ acks: EntityStateAckEntry[]; } /** One member of an authoritative group-rest transform snapshot. */ export interface GroupRestSnapshotEntity { /** Present on explicit final commits; legacy snapshots retain their shape. */ epoch?: number; velocity?: Vec3; angularVelocity?: Vec3; id: string; pos: Vec3; rot: Quat; } /** * Broadcast once when a declared physics group transitions to fully at rest, and sent directly to late joiners * while that settled snapshot remains current. `seq` is monotonic within `group` for the room's lifetime. */ export interface GroupRestSnapshotMessage { scope?: FinalRestProvenance; commit?: { version: 1; transactionId: string; requestId: number; }; group: string; seq: number; entities: GroupRestSnapshotEntity[]; } export type ModuleWakeReason = 'always-on' | 'proximity' | 'engagement' | 'action' | 'import' | 'module' | 'external'; export interface ModuleFusedMessage { moduleId: string; serverTick: number; tickNodes: number; } export interface ModuleWakeMessage { moduleId: string; reason: ModuleWakeReason; serverTick: number; } export interface ModuleSleepMessage { moduleId: string; serverTick: number; } export type ModuleEngagementStatus = 'granted' | 'denied' | 'released' | 'revoked'; /** * Direct room response for one engaged-module authority transition. Grants are exclusive and carry * the epoch the SDK must stamp on later verbs. Denials intentionally expose no competing holder. */ export interface ModuleEngagementMessage { moduleId: string; action: string; status: ModuleEngagementStatus; /** Echoed for acquire/release responses; absent on an unsolicited server revocation. */ requestId?: number; /** Present for granted/released/revoked transitions; absent on denial. */ epoch?: number; } export interface ModuleEffectMessage { moduleId: string; commandId: string; index: number; serverTick: number; event: string; payload: Record; } /** * `helix:room-ticket` — the room's private hand-off of one ticket to one selected player (SPEC-U1 `openRoom`). * The ticket is redeemable ONLY by the account it was issued to (the backend's atomic redeem checks the user), * so it is safe on the wire; redeem it with `Helix.multiplayer.switchRoom({ ticket: ticketId })`. * * `expiresAt` is forwarded VERBATIM from the backend's instance-creation answer, which is why it is typed * loosely: an ISO-8601 string or a Unix epoch number. The SDK normalises it to epoch milliseconds before a * world sees it (HelixRoom.onTicket hands out a `RoomTicket`, not this wire shape). */ export interface RoomTicketMessage { /** The ticketed room type the instance was opened as. */ roomType: string; /** Single-use ticket id — the join API's `{ ticket }` body. */ ticketId: string; /** The room instance id (= the credential's `instanceKey`); also the rejoin endpoint's `:instanceId`. */ instanceId: string; /** * THIS ticket's redeem-by deadline (about five minutes), as the backend returned it: ISO-8601 string or Unix * epoch (seconds or milliseconds). It is the ticket's expiry, not the instance's — the room may live longer. */ expiresAt: string | number; } /** * A server→client broadcast event (Tier 2 Phase 2.5, spec §10.3). A world declares named events with a * payload schema; a `broadcast` rule-effect sends one on the event's NAME (not a fixed ServerMessageType), * received via `room.onMessage(name, cb)`. Payload values are the declared fields; a `ref` field serializes * to its `playerKey` string on the wire (so `who:"self"` and `who:{aggregate argmax}` are the same type). */ export type BroadcastPayload = Record; /** * Wire message-type names a world's declared event/action names MUST NOT shadow — the fixed client→server * and server→client channels that are themselves legal declared names. The manifest validator rejects a * declared name in this set so a `broadcast` can't collide with `pong` (or confuse the fixed channels). * Internal `__helix:*` channels are outside the declared-name grammar and need no reservation. Inlined in * @hypersoniclabs/helix-manifest with a cross-ref (the manifest takes no SDK dependency); keep the two in sync. */ export declare const RESERVED_MESSAGE_TYPES: readonly ["state", "ability", "action", "gesture", "attachments", "purchaseReceipt", "avatarRefresh", "emoteRefresh", "emoteJoin", "entityState", "entityStateBatch", "ping", "pong", "stateAck", "entityStateAck", "groupRestSnapshot", "moduleFused", "moduleWake", "moduleSleep", "moduleEngagement", "moduleEffect"]; /** Maps each client→server message type to its payload, so senders/handlers stay in lockstep. */ export interface ClientMessagePayloads { [ClientMessageType.State]: StateMessage; [ClientMessageType.Ability]: AbilityMessage; [ClientMessageType.Action]: ActionMessage; [ClientMessageType.Gesture]: GestureMessage; [ClientMessageType.Attachments]: AttachmentsMessage; [ClientMessageType.PurchaseReceipt]: PurchaseReceiptMessage; [ClientMessageType.AvatarRefresh]: AvatarRefreshMessage; [ClientMessageType.EmoteRefresh]: EmoteRefreshMessage; [ClientMessageType.EmoteJoin]: EmoteJoinMessage; [ClientMessageType.EntityState]: EntityStateMessage; [ClientMessageType.EntityStateBatch]: EntityStateBatchMessage; [ClientMessageType.Ping]: PingMessage; } /** Maps each server→client message type to its payload. */ export interface ServerMessagePayloads { [ServerMessageType.Pong]: PongMessage; [ServerMessageType.StateAck]: StateAckMessage; [ServerMessageType.EntityStateAck]: EntityStateAckMessage; [ServerMessageType.Gesture]: GestureEventMessage; [ServerMessageType.GroupRestSnapshot]: GroupRestSnapshotMessage; [ServerMessageType.ModuleFused]: ModuleFusedMessage; [ServerMessageType.ModuleWake]: ModuleWakeMessage; [ServerMessageType.ModuleSleep]: ModuleSleepMessage; [ServerMessageType.ModuleEngagement]: ModuleEngagementMessage; [ServerMessageType.ModuleEffect]: ModuleEffectMessage; [ServerMessageType.VehicleEntityRetired]: VehicleEntityRetiredMessage; [ServerMessageType.VehicleActionFences]: VehicleActionFencesMessage; [ServerMessageType.RoomTicket]: RoomTicketMessage; } /** * Per-connection rate ceilings (messages/second) the room enforces; over-rate clients are dropped. * State is the throttled input channel (plan D3); ability is bursty but bounded. * * `stateHz`/`entityStateHz` are the DEFAULT per-connection upload cadence. A world may raise BOTH to `maxUploadHz` * via `multiplayer.uploadHz: 20` — the room derives the actual ceiling from the trusted configUrl, the client from * its bundled manifest, so they agree without a handshake (a client that lies is simply rate-limited). Sending under * the cap is always legal, so this is purely a cap → no CONTRACT_VERSION bump. * SYNC: the {10, 20} tier set mirrors the manifest's MULTIPLAYER_CAPS.uploadHzAllowed — widen in lockstep. */ export declare const MESSAGE_RATE: { readonly stateHz: 10; readonly abilityHz: 20; readonly actionHz: 20; readonly gestureHz: 5; readonly attachmentsHz: 4; readonly purchaseReceiptHz: 2; readonly avatarRefreshHz: 1; readonly emoteRefreshHz: 1; readonly emoteJoinHz: 2; readonly entityStateHz: 10; readonly pingHz: 4; readonly maxUploadHz: 20; }; /** * Movement-plausibility gate — the canonical home for the bound the colyseus room ENFORCES and the engine's * client-side upload clamp MIRRORS, so both compute the SAME bound from the SAME inputs and never disagree. A * frame moving more than `maxSpeed × Δt × toleranceMul` from the last accepted position is implausible: the * server holds + acks it, the client pre-clamps its upload to stay inside it. `Δt` is the SANITIZED real * send-interval (the source-time `posT` delta — clamped, spoof-bounded; see nextSourceTime), NOT integer ticks — * time-based so the gate is cadence-agnostic and a non-tick-aligned send rate can't quantize the budget away. * This is a SANITY bound (anti-teleport), not cheat-proof movement (Tier 3). The room imports this; the * standalone engine mirrors the two values (it takes no SDK build dep — same SYNC pattern as the declared-state * mirror) → keep them in lockstep. */ export declare const MOVEMENT_GATE: { readonly toleranceMul: 1.25; readonly ownerEntityToleranceMul: 3; readonly maxCatchupMs: 300; readonly impulseMul: 6; readonly graceMs: 250; }; /** * Hard payload bound the room rejects above (defense against oversized frames), measured on the JSON form (a * cheap proxy; the msgpack wire is smaller). Sized (P1.B4) to admit a full EntityStateBatch — MAX_ENTITIES_PER_OWNER * (128) entities with modest vars in one message (≈75 JSON bytes/entry) — so the byte cap and the per-owner entity * cap don't disagree. MUST stay below the Colyseus transport `maxPayload` (16 KB, helix-colyseus-server src/index.ts) * in WIRE terms, so the room's reject fires before the transport drops the frame / closes the socket. A var-heavy * batch past this is a CLEAN DROP (the room's guard does not strike on an over-byte payload), never a kick. */ export declare const MAX_MESSAGE_BYTES = 12288;