lease.fbs โ the Ephemeral Ownership Lease
Source: modules/Descent.Vtt.Protocol/schemas/lease.fbs at build time. Nothing was withheld from this file.
// ADR-050 โ the Ephemeral Ownership Lease, on the wire.//// Read schemas/README.md first; its three rules are assumed here.//// **Why a fifth schema rather than fields on command.fbs and delta.fbs.** A lease request// is not an intent: `SubmitIntent` carries a *proposal the rules engine adjudicates*, and a// lease is a claim on the transform path that produces no domain event at all. Folding it// into `SubmitIntent` would have put a message with no action name and no arguments through// the action-resolution path, where every field it does not use is a field somebody must be// told to ignore. And a lease grant must reach the client **faster than the tick** (ยง5.1.1// clause 6 pushes forced expiry immediately), so it cannot ride inside `WorldDelta`, whose// whole shape assumes tick cadence.//// **Every deadline on this wire is a tick sequence number, never a wall clock or a// duration.** Q-028 says so in as many words -- "expressed in tick sequence rather than wall// clock" -- and the wire is where that matters most: a duration would have to be interpreted// against the receiver's clock, which is the one thing a distributed lease must never depend// on. The client compares `expires_at_tick` against the `transform_tick` it is already being// sent in every `WorldDelta`.//// **What this schema deliberately does NOT carry.** No viewer id on the request. Same rule// as command.fbs: ยง8.2 forbids the client asserting its own identity, and the server// resolves the caller from the connection. `holder` appears only on the SERVER -> client// direction, where it is an answer rather than a claim.
include "snapshot.fbs";
namespace Descent.Vtt.Protocol;
/// Why a lease request did not result in the caller holding the lease.////// An enum rather than a bool, because "rejected" and "the room is not ready" require/// different client behaviour: the first retries after `expires_at_tick`, the second waits/// for hydration. Collapsing them would make the client poll through a hydration.enum LeaseRejection : ubyte { /// Another viewer holds it. `holder` and `expires_at_tick` are populated. Held = 0, /// The room has not finished hydrating (ยง7.1). Not a refusal -- a "not yet". NotReady = 1, /// No such entity in this room. UnknownEntity = 2, /// A renew or release from a viewer that is not the holder. NotHolder = 3,}
/// What the caller wants done with a lease.////// **`Unknown = 0` is the entire reason this is an enum and not a bare `ubyte`.** FlatBuffers/// gives an absent scalar its default, and the default of a `ubyte` is `0` โ so a truncated/// buffer, a zero-filled one, or one written by a peer that never set the field decodes as/// whatever value happens to sit at `0`. That value used to be `Request`, which made *silence/// on the wire* decode as a **claim on the transform path**: the one direction ADR-050 must/// never fail in, and the mirror of the argument `LeaseResponse.granted` already records for/// the server -> client leg. Zero now names a verb that `LeaseCodec` refuses.////// The values are therefore **1-based on purpose**; do not renumber them to close the gap.enum LeaseVerb : ubyte { /// Not a verb. Never written deliberately โ it is what an absent or truncated `verb` /// field reads as, and `LeaseCodec.TryDecode` answers it with `UnknownVerb`. Unknown = 0, /// Take the lease. Idempotent for the current holder. Request = 1, /// Extend a held lease by Q-028 ticks. Renew = 2, /// Hand the lease back -- the finalise path. Release = 3,}
/// Why a lease ended. Decides what the client does with the token it was dragging.enum LeaseEndReason : ubyte { /// The holder finalised. Hand-back keeps the position (ยง5.1.1 clause 5). Finalised = 0, /// Q-028 ticks elapsed with no renewal. The entity **returns** to `stale_anchor`. Expired = 1, /// GM override, entity destroyed, room paused, timeline checkout. Also returns. Forced = 2,}
/// Client -> server. Take, renew or drop the lease on one entity.////// One table for all three verbs rather than three tables: they share every field, and a/// client that can encode a renew can encode a request. The verb is the only difference and/// it is one byte.table LeaseCommand { /// ยง9.6: every packet carries this, field id 0. protocol_version: uint32;
/// The entity being claimed. **Not an authority claim** -- see the header note. entity: Uuid;
/// **An enum rather than the `ubyte` this was.** The original note here read "a ubyte is /// what this already is on the C# side, and the three values are exhaustive by /// construction" โ the second half was the defect. Three values are exhaustive only over /// what a *correct* client writes; the wire also carries what a truncated or hostile one /// writes, and there the absent value is `0`. See `LeaseVerb`. verb: LeaseVerb;
/// Correlates the grant or rejection back to the pointer event that caused it, so a /// client that issued two grabs quickly can tell which one was answered. correlation: Uuid;}
/// Server -> client. The answer, pushed rather than polled.////// **`granted` is a field and not the absence of `rejection`.** A client that inferred a/// grant from a missing field would treat a truncated or unknown-version message as/// permission to predict, which is the one direction this protocol must not fail in.table LeaseResponse { protocol_version: uint32;
/// Echoes `LeaseCommand.correlation`. correlation: Uuid;
entity: Uuid;
/// True only when the addressed viewer holds the lease. granted: bool;
/// Meaningful only when `granted` is false. rejection: LeaseRejection;
/// The tick the lease expires ON (Q-028: granted_tick + 40). The client renews before /// this and stops predicting at it. /// /// Populated on rejection too, when `rejection` is `Held`: it is when the entity next /// becomes free, so a rejected client retries once instead of polling. expires_at_tick: int64;
/// The current holder, when `rejection` is `Held`. Populated so the UI can say who has /// the token rather than that it is merely unavailable. holder: Uuid;}
/// Server -> client. A lease ended without the holder releasing it.////// Separate from `LeaseResponse` because it is unsolicited: there is no correlation to echo,/// and a client that received it as a response would have nothing to match it against.table LeaseRevoked { protocol_version: uint32; entity: Uuid; reason: LeaseEndReason;
/// Where the entity returns to. **Labelled stale on purpose** (ADR-050): this is where it /// was when the lease was taken, not where the drag had reached, because the drag was /// never committed. ยง5.1.1 requires the client render the return as deliberate rather /// than as a glitch, which it can only do if it knows the return is a return. stale_anchor_raw_x: int32; stale_anchor_raw_y: int32;}
/// The coalesced advisory uplink of ADR-050 clause 7.////// **One message per room per tick, not one per drag.** F-39 found that the server cannot/// filter a dragged entity out of an observer's AOI because it only knows the anchor -- so/// a GM dragging a hidden creature behind cover would broadcast its every coordinate. Lease/// holders therefore publish an advisory position for filtering only.////// **These values are non-authoritative by construction** and produce no events, no/// database writes and no trigger evaluation. They exist so ยง9.6.3's allowlist can be/// recomputed against where the entity actually is.table LeasePositionsUpdate { protocol_version: uint32; tick: int64; entities: [Uuid]; raw_x: [int32]; raw_y: [int32];}
root_type LeaseCommand;
/// Identifies a lease buffer on disk or in a capture.file_identifier "DVL1";