Schemas
These are the JSON Schemas (draft 2020-12) that describe the encounter-log data produced by the Farever Companion DPS meter. The newest version is selected by default and is the recommended target for new integrations. Versions are immutable and permanently served at /logs-schema/{version}.json. The source of truth lives in the GitHub repository.
Encounter Log v2
EncounterLogV2
The canonical on-disk artifact for one closed encounter. Adds a status-effect timeline (applied/refreshed/expired) plus an encounter-open status snapshot and game-clock server timestamps (combat start, per-event, status start) on top of the v1 participants, combat events, and salvage metadata.
- Schema ID
- https://farevercompanion.com/logs-schema/v2.json
- Released
v2.json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "EncounterLogV2",
"description": "The canonical on-disk artifact for one closed encounter. Written atomically\nto `<logs_dir>/<started_at_unix>-<log_id>.json`.\n\nThe format version is not encoded in this type's name — it lives in the\n`schema_version` field and the `$schema` URL, both keyed off\n[`SCHEMA_VERSION`]. The published schema's `title` is likewise stamped from\nthat constant by [`encounter_log_schema`], so it never drifts from the\nversion.",
"type": "object",
"properties": {
"$schema": {
"description": "Reference to the published JSON Schema this log conforms to — the\nversion-keyed URL from [`schema_url`]. Lets a consumer or editor locate\nand validate the document directly. Serialised first, as `$schema`.\n`#[serde(default)]` keeps pre-`$schema` logs deserialisable (the current\nURL is filled in on read), so this stays an additive change that needs\nno `schema_version` bump.",
"type": "string",
"default": "https://farevercompanion.com/logs-schema/v2.json"
},
"schema_version": {
"type": "integer",
"format": "uint8",
"minimum": 0,
"maximum": 255
},
"log_id": {
"$ref": "#/$defs/LogId"
},
"encounter_id": {
"type": "integer",
"format": "uint64",
"minimum": 0
},
"started_at_unix": {
"type": "integer",
"format": "int64"
},
"ended_at_unix": {
"type": "integer",
"format": "int64"
},
"started_ms": {
"type": "integer",
"format": "uint64",
"minimum": 0
},
"ended_ms": {
"type": "integer",
"format": "uint64",
"minimum": 0
},
"duration_ms": {
"type": "integer",
"format": "uint64",
"minimum": 0
},
"combat_start_server_ms": {
"description": "Game-clock timestamp (ms) at which combat started for this encounter\n(`ent.Hero.combatStartTime` × 1000, on the `serverNow` base). The\nauthoritative, party-shareable fight-start anchor — unlike the\nwall-clock `started_ms`/`started_at_unix`, this is the game clock the\nper-event `server_ms` is measured against, so `server_ms −\ncombat_start_server_ms` is the in-game time since the fight opened.\n`None` when `combatStartTime` couldn't be read. New in schema v2.",
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 0
},
"hero_uid": {
"type": "integer",
"format": "int64"
},
"hero_database_id": {
"type": [
"integer",
"null"
],
"format": "int64"
},
"location": {
"description": "Where the encounter happened (instance/dungeon vs open world, plus a\nstable location id). Sampled once at `EncounterStart`. Omitted when\nthe probe couldn't resolve anything (booting / mid-transition); an\nadditive field that needed no `schema_version` bump — see\n[`EncounterLocation`].",
"anyOf": [
{
"$ref": "#/$defs/EncounterLocation"
},
{
"type": "null"
}
]
},
"salvage_summary": {
"$ref": "#/$defs/SalvageSummary"
},
"participants": {
"type": "array",
"items": {
"$ref": "#/$defs/LogParticipant"
}
},
"events": {
"type": "array",
"items": {
"$ref": "#/$defs/LogEvent"
}
},
"broken_events": {
"type": "array",
"items": {
"$ref": "#/$defs/BrokenEvent"
}
},
"encounter_start_status": {
"description": "Active-status snapshot taken at encounter open (self + party). Empty\nwhen no statuses were active at the moment the encounter opened, or\nwhen the encounter was recovered from scratch (mid-session crash).\nNew in schema v2.",
"type": "array",
"items": {
"$ref": "#/$defs/StatusSnapshotEntry"
}
},
"status_events": {
"description": "Status timeline: one entry per status Applied / Refreshed / Expired\nobserved during the encounter. Empty when no status events occurred or\non recovery. In-memory only, like `participants`; not persisted in the\nscratch dir, so a mid-encounter crash recovers with an empty vec.\nNew in schema v2.",
"type": "array",
"items": {
"$ref": "#/$defs/StatusEvent"
}
}
},
"required": [
"schema_version",
"log_id",
"encounter_id",
"started_at_unix",
"ended_at_unix",
"started_ms",
"ended_ms",
"duration_ms",
"hero_uid",
"salvage_summary",
"participants",
"events",
"broken_events"
],
"$defs": {
"LogId": {
"description": "Persistent unique identifier for one Encounter Log. UUIDv7 gives\ntime-ordered, globally-unique keys without platform dependencies.",
"type": "string",
"format": "uuid"
},
"EncounterLocation": {
"description": "Where the encounter took place, sampled once at `EncounterStart` from the\nlive game (probe-core `current_location`). Every field is best-effort: a\nmemory read can come back empty during boot / a zone transition, so each\nis optional and the whole struct is omitted from the log when nothing\ncould be resolved. Adding fields here is additive — it needs no\n`schema_version` bump.\n\nThe instance/open-world distinction comes from the game's own\n`GameLayer.mainActivity` binding (set only for activities that own their\nlayer/scope), NOT from `Player.activityCtx` (which is an array that\nincludes non-instanced open-world overlays).",
"type": "object",
"properties": {
"in_instance": {
"description": "`true` when the local player was inside an instanced activity (its own\nlayer/scope — dungeon, arena, fight-stone, …) at encounter start;\n`Some(false)` in the open world. `None` only when the anchor walk\ncouldn't resolve (rare: still booting / mid zone-transition).",
"type": [
"boolean",
"null"
]
},
"in_dungeon": {
"description": "`true` when the instanced activity is specifically a dungeon — the\ngame's `st.Activity::isDungeon`, i.e. an `st.activity.Dungeon` or its\n`st.activity.Boss` subclass. `Some(false)` in the open world or a\nnon-dungeon instance; `None` when unresolved.",
"type": [
"boolean",
"null"
]
},
"id": {
"description": "Stable CDB id of the current location — the validation-friendly key.\nIn an instance this is the activity-kind id (`mainActivity.kind`);\nin the open world it is the current zone id when resolvable. `None`\nwhen unresolved.",
"type": [
"string",
"null"
]
},
"name": {
"description": "Localized display name of the location when available (cosmetic; may\nbe absent on dev rows or before the text bank has loaded).",
"type": [
"string",
"null"
]
},
"activity_class": {
"description": "Runtime activity subtype — the leaf class name of `mainActivity`\n(e.g. `\"Dungeon\"`, `\"Boss\"`, `\"FightStone\"`). `None` in the open\nworld. A self-describing discriminator that lets the log distinguish\ninstance subtypes without a future schema change.",
"type": [
"string",
"null"
]
}
}
},
"SalvageSummary": {
"description": "Counters that let consumers triage a log's quality at a glance without\nstreaming the full event list.",
"type": "object",
"properties": {
"is_recovered": {
"description": "True when this log was reconstructed from scratch dir at startup\n(i.e. the previous session crashed or quit mid-encounter).",
"type": "boolean"
},
"partial_event_count": {
"description": "Events in `events` with `t: 1` (readable `time_ms`, some fields\nmissing or unreadable).",
"type": "integer",
"format": "uint32",
"minimum": 0
},
"broken_event_count": {
"description": "Records in `broken_events` (unrecoverable `time_ms`).",
"type": "integer",
"format": "uint32",
"minimum": 0
},
"partial_participant_count": {
"description": "Participants in `participants` with `t: 1`.",
"type": "integer",
"format": "uint32",
"minimum": 0
}
},
"required": [
"is_recovered",
"partial_event_count",
"broken_event_count",
"partial_participant_count"
]
},
"LogParticipant": {
"description": "Participant identity in the canonical log. The `t` field (0 or 1) discriminates between a fully-parsed participant and a partially-recovered one.",
"oneOf": [
{
"description": "A fully-resolved participant identity and loadout (t: 0).",
"type": "object",
"properties": {
"t": {
"description": "Discriminant: 0 = fully parsed participant.",
"const": 0
}
},
"required": [
"t"
],
"allOf": [
{
"properties": {
"t": true
}
},
{
"$ref": "#/$defs/ParticipantHeader"
}
]
},
{
"description": "A participant record where some fields could not be read (t: 1). salvaged_fields are flattened at the top level.",
"type": "object",
"properties": {
"t": {
"description": "Discriminant: 1 = partially parsed participant.",
"const": 1
}
},
"required": [
"t"
],
"allOf": [
{
"properties": {
"t": true
}
},
{
"$ref": "#/$defs/PartialParticipantHeader"
}
]
}
]
},
"ParticipantHeader": {
"description": "Identity + loadout for a clean participant read.",
"type": "object",
"properties": {
"uid": {
"description": "Stable join key. The same `ParticipantId` the events carry as\n`source_uid` / `target_uid`; an unsigned `StableUnitId` hash, NOT\nthe game's raw `__uid` (which the log records separately as the\ntop-level `hero_uid`).",
"$ref": "#/$defs/ParticipantId"
},
"name": {
"type": [
"string",
"null"
]
},
"is_hero": {
"type": "boolean"
},
"level": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0
},
"class_id": {
"type": [
"string",
"null"
]
},
"weapons": {
"type": [
"array",
"null"
],
"items": {
"$ref": "#/$defs/Weapon"
}
},
"gear": {
"type": [
"array",
"null"
],
"items": {
"$ref": "#/$defs/GearPiece"
}
},
"talents": {
"description": "Picked talents for hero participants: internal skill-sheet CDB row id\n(e.g. `\"Warrior_Hemorrhage\"`) → rank (>= 1; absent key == unpicked,\nmatching the game's set-rank closure which removes the key at rank 0).\n`BTreeMap` keeps the on-disk key order deterministic.\n\nBest-effort: whether `HeroSpecialization.talents` is replicated for\nREMOTE party-member heroes is unverified (hxbit visibility groups may\nfilter it), so an unreadable/empty map serialises as absent rather\nthan demoting the participant to Partial. `None` for NPCs.",
"type": [
"object",
"null"
],
"additionalProperties": {
"type": "integer",
"format": "uint8",
"minimum": 0,
"maximum": 255
}
}
},
"required": [
"uid",
"is_hero"
]
},
"ParticipantId": {
"description": "Stable identity shared by a combat-log participant and every event\n(`source_uid` / `target_uid`) that references it. This is the\n`StableUnitId`-derived hash the bridge stamps onto each damage / heal /\nshield source and target — an OPAQUE 64-bit identifier (an FNV-1a hash\nover `(combat_id, spawn_tick, name)`, so the full 64-bit range is in\nplay and the top bit is frequently set), which is why it is carried\nUNSIGNED. A participant's `uid` and an event's `source_uid` /\n`target_uid` are the *same* `ParticipantId` and join by equality.\n\n`#[serde(transparent)]` keeps the on-disk shape a bare JSON number — its\nintroduction needed no `schema_version` bump.\n\n## Why a named newtype\n\nThis is deliberately NOT the game's raw `__uid` (a small, sequential,\ngenuinely-signed `i64` that the log carries separately as `hero_uid`\nand never uses as a participant/event join key). The two values live\nin different namespaces.\n\nThe combat-log once serialised the participant side of the join as a\nsigned `i64` while the event side stayed an unsigned `u64`: for any\nhash with the top bit set, the participant's `uid` came out negative\n(e.g. `-6558438490268822143`) while the same entity's `source_uid`\ncame out positive (`11888305583440729473`) — bit-identical, but two\ndifferent JSON numbers, so consumers could never join them. Modelling\nthe join key as one named unsigned type makes that representation\nmismatch a *compile error* rather than a silent data bug, and lets the\nrepresentation be changed in exactly one place if it ever must be.",
"type": "integer",
"format": "uint64",
"minimum": 0
},
"Weapon": {
"description": "One weapon slot in a participant's loadout.\n\nBeyond the original `slot` + `item_id` identity pair, the live\n`st.Equipment.content` walk (probe-core `hero_equipment`) fills in\nper-item instance state, and the CDB catalog (when loaded) adds the\nderived enrichment fields (`ilevel`, `stats`, `augment_stats`,\n`upgrade_special`, `affix_factor`). Every optional field is an\n`Option` with `skip_serializing_if` so pre-existing logs (and the\nlegacy `weaponInHand`/arsenal fallback path, which knows ids only)\nkeep their exact on-disk shape — additive, needing no `schema_version` bump.",
"type": "object",
"properties": {
"slot": {
"type": "string"
},
"item_id": {
"type": "string"
},
"level": {
"description": "Per-instance item level. `None` when the instance carries the\nsentinel 0, meaning \"use the CDB row's level\".",
"type": [
"integer",
"null"
],
"format": "int32"
},
"ilevel": {
"description": "DERIVED: effective iLevel (def iLevel + flawless bonus + stars ×\nupgrade bonus) — the input the stat generation runs at.",
"type": [
"integer",
"null"
],
"format": "int32"
},
"upgrade_level": {
"description": "The in-game \"enhancement\" star count, 0..=5 (internal game field\nname: `upgradeLevel`). `Some(0)` is meaningful (un-enhanced) and\nis kept distinct from `None` (unreadable).",
"type": [
"integer",
"null"
],
"format": "uint8",
"minimum": 0,
"maximum": 255
},
"flawless": {
"description": "Bit 0 of the instance's `flags` EnumFlagsData (Flawless quality).",
"type": [
"boolean",
"null"
]
},
"rarity": {
"description": "Resolved rarity id: the per-instance override\n(`st.item.Weapon.rarity`) when present, else the CDB row's rarity\n(catalog-resolved). Without a loaded catalog this degrades to the\nraw override only.",
"type": [
"string",
"null"
]
},
"augments": {
"description": "Socketed augment item ids (internal game field name: `Gear.slots`).",
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
},
"in_hand": {
"description": "`Some(true)` on the one equipped weapon that unambiguously matches\nthe hero's `weaponInHand` decode, which is tag-gated on the\n`client.WeaponState` `InHand` constructor — holstered states\n(`PrimaryHolster` / `SecondaryHolster`) never match. `None`\neverywhere else (including when the match is ambiguous or the\nvenum payload is a weapon-kind row rather than the item row).",
"type": [
"boolean",
"null"
]
},
"stats": {
"description": "DERIVED: base stat lines, replayed from the game's generation\nformulas. `Some([])` is meaningful — the item genuinely has no\nstats; `None` means the catalog wasn't loaded at sample time.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/$defs/StatLine"
}
},
"augment_stats": {
"description": "DERIVED: socket contributions, kept apart from `stats` so the two\nsources stay distinguishable.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/$defs/StatLine"
}
},
"upgrade_special": {
"description": "DERIVED: the enhancement special, when unlocked.",
"anyOf": [
{
"$ref": "#/$defs/UpgradeSpecial"
},
{
"type": "null"
}
]
},
"affix_factor": {
"description": "DERIVED: the slot's `affixFactor` when != 1.0 (Slot_Weapon2 =\n0.4). The stat lines stay tooltip-style (unscaled).",
"type": [
"number",
"null"
],
"format": "double"
}
},
"required": [
"slot",
"item_id"
]
},
"StatLine": {
"description": "One computed flat-attribute stat line (e.g. `Armor +286`). The values\nare DERIVED: the game stores no per-item stats, it regenerates them\nfrom `{item row, effective iLevel}`; the sampler replays that\ncomputation via `farever-item-stats` when the CDB catalog is loaded.",
"type": "object",
"properties": {
"attribute": {
"type": "string"
},
"value": {
"type": "integer",
"format": "int64"
}
},
"required": [
"attribute",
"value"
]
},
"UpgradeSpecial": {
"description": "The enhancement-unlocked weapon special: granted at\n`upgrade_level >= GearUpgrades.SkillUnlockLevel` (3) when the skill\n`\"<itemType>_Upgrade\"` exists. `rank` is the 0-based resolved-rarity\nindex (Common 0 .. Legendary 4, no +1).",
"type": "object",
"properties": {
"skill": {
"type": "string"
},
"rank": {
"type": "integer",
"format": "uint8",
"minimum": 0,
"maximum": 255
}
},
"required": [
"skill",
"rank"
]
},
"GearPiece": {
"description": "One gear piece in a participant's loadout. Same per-instance state and\nderived enrichment as [`Weapon`] minus the weapon-only `in_hand` flag\nand `upgrade_special`; see the field docs there. (`rarity` here is\npurely catalog-derived — gear has no instance override.)",
"type": "object",
"properties": {
"slot": {
"type": "string"
},
"item_id": {
"type": "string"
},
"level": {
"type": [
"integer",
"null"
],
"format": "int32"
},
"ilevel": {
"description": "DERIVED: effective iLevel; see [`Weapon::ilevel`].",
"type": [
"integer",
"null"
],
"format": "int32"
},
"upgrade_level": {
"description": "In-game \"enhancement\" star count (internal name: `upgradeLevel`).",
"type": [
"integer",
"null"
],
"format": "uint8",
"minimum": 0,
"maximum": 255
},
"flawless": {
"type": [
"boolean",
"null"
]
},
"rarity": {
"description": "DERIVED: the CDB row's rarity id (catalog-resolved).",
"type": [
"string",
"null"
]
},
"augments": {
"description": "Socketed augment item ids (internal name: `Gear.slots`).",
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
},
"stats": {
"description": "DERIVED: base stat lines; see [`Weapon::stats`].",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/$defs/StatLine"
}
},
"augment_stats": {
"description": "DERIVED: socket contributions; see [`Weapon::augment_stats`].",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/$defs/StatLine"
}
},
"affix_factor": {
"description": "DERIVED: the slot's `affixFactor` when != 1.0.",
"type": [
"number",
"null"
],
"format": "double"
}
},
"required": [
"slot",
"item_id"
]
},
"PartialParticipantHeader": {
"description": "A participant record where some fields couldn't be read.",
"type": "object",
"properties": {
"missing_fields": {
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": true,
"required": [
"missing_fields"
]
},
"LogEvent": {
"description": "One chronological event in the canonical encounter log. The `t` field (0 or 1) discriminates between a fully-parsed event and a partially-recovered one.",
"oneOf": [
{
"description": "A fully-parsed combat event (t: 0). SidecarEvent fields are flattened at the top level alongside the discriminant; Option fields are omitted when None.",
"type": "object",
"properties": {
"t": {
"description": "Discriminant: 0 = fully parsed event.",
"const": 0
}
},
"required": [
"t"
],
"allOf": [
{
"properties": {
"t": true
}
},
{
"$ref": "#/$defs/SidecarEventSerde"
}
]
},
{
"description": "A partially-parsed event where time_ms was recoverable but other fields may be missing (t: 1).",
"type": "object",
"properties": {
"t": {
"description": "Discriminant: 1 = partially parsed event.",
"const": 1
}
},
"required": [
"t"
],
"allOf": [
{
"properties": {
"t": true
}
},
{
"$ref": "#/$defs/PartialEvent"
}
]
}
]
},
"SidecarEventSerde": {
"description": "Shadow of `SidecarEvent` with serde support, used for on-disk encoding.",
"type": "object",
"properties": {
"time_ms": {
"type": "integer",
"format": "uint64",
"minimum": 0
},
"server_ms": {
"description": "Game-clock timestamp (ms) of the event (`serverNow` × 1000), read once\nper pump tick. Distinct from `time_ms` (a wall-clock monotonic counter):\nthe game clock can pause / dilate, so this is the only timestamp on the\ngame/server clock and the one to use against `combat_start_server_ms`.\n`None` when `serverNow` couldn't be read that tick.",
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 0
},
"source_uid": {
"anyOf": [
{
"$ref": "#/$defs/ParticipantId"
},
{
"type": "null"
}
]
},
"source_name": {
"type": [
"string",
"null"
]
},
"target_uid": {
"$ref": "#/$defs/ParticipantId"
},
"target_name": {
"type": [
"string",
"null"
]
},
"skill": {
"type": [
"string",
"null"
]
},
"effect": {
"$ref": "#/$defs/SidecarEffectKindSerde"
},
"amount": {
"type": "number",
"format": "double"
},
"overheal": {
"type": [
"number",
"null"
],
"format": "double"
},
"crit": {
"type": "boolean"
},
"kill": {
"type": "boolean"
},
"mitigated": {
"type": "number",
"format": "double"
}
},
"required": [
"time_ms",
"target_uid",
"effect",
"amount",
"crit",
"kill",
"mitigated"
]
},
"SidecarEffectKindSerde": {
"type": "string",
"enum": [
"Damage",
"Heal",
"Shield",
"Dodge",
"Other"
]
},
"PartialEvent": {
"description": "A timeline event with `time_ms` recoverable but some other fields missing.",
"type": "object",
"properties": {
"time_ms": {
"type": "integer",
"format": "uint64",
"minimum": 0
},
"missing_fields": {
"type": "array",
"items": {
"type": "string"
}
},
"parse_error": {
"type": "string"
}
},
"required": [
"time_ms",
"missing_fields",
"parse_error"
],
"additionalProperties": true
},
"BrokenEvent": {
"description": "A record whose `time_ms` is unrecoverable; cannot be placed in the\ntimeline. Lives in `broken_events`, not `events`.",
"type": "object",
"properties": {
"line_index": {
"type": "integer",
"format": "uint64",
"minimum": 0
},
"raw_line": {
"type": "string"
},
"parse_error": {
"type": "string"
},
"partial_fields": true
},
"required": [
"line_index",
"raw_line",
"parse_error"
]
},
"StatusSnapshotEntry": {
"description": "One active status captured in the encounter-open status snapshot.",
"type": "object",
"properties": {
"target_uid": {
"description": "The unit the status sits on — the status *target*\n(joins ParticipantHeader::uid).",
"$ref": "#/$defs/ParticipantId"
},
"target_name": {
"description": "Display name of the target, when resolved. Joinable via `participants`\ntoo; carried inline to match the damage-event shape.",
"type": [
"string",
"null"
]
},
"status_id": {
"description": "CDB status id (e.g. \"ShieldOfSpark\"); the Status.kind row id.",
"type": "string"
},
"source_uid": {
"description": "The unit that applied the status — the *source* (`Status.instigator`).",
"anyOf": [
{
"$ref": "#/$defs/ParticipantId"
},
{
"type": "null"
}
]
},
"source_name": {
"description": "Display name of the source, when resolved.",
"type": [
"string",
"null"
]
},
"stacks": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0
},
"remaining_duration_ms": {
"description": "Time left until the status expires, in ms. The game model is\n`remaining = max(0, Status.duration − (serverNow − Status.startTime))`\non a game clock measured in seconds (× 1000 → ms), replicating\n`st.skill.BaseSkill::getDurationLeft`. The probe reads the layer-global\n`serverNow` (`obj.layer → _time → _time → serverNow`) once per snapshot;\nsee `farever-probe-core`'s status_tracker. `None` when an input\n(`duration`, `startTime`, or `serverNow`) couldn't be read.",
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 0
},
"start_time_ms": {
"description": "The game-clock timestamp (ms) at which this status began\n(`Status.startTime` × 1000, on the `serverNow` base). Lets a consumer\nrecompute remaining against any later reference:\n`remaining = duration_ms − (ref_server_ms − start_time_ms)`. `None`\nwhen `startTime` couldn't be read.",
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 0
}
},
"required": [
"target_uid",
"status_id"
]
},
"StatusEvent": {
"description": "One status timeline event (applied / refreshed / expired) during an encounter.",
"type": "object",
"properties": {
"time_ms": {
"type": "integer",
"format": "uint64",
"minimum": 0
},
"kind": {
"$ref": "#/$defs/StatusEventKind"
},
"target_uid": {
"description": "The unit the status sits on — the status *target*\n(joins ParticipantHeader::uid).",
"$ref": "#/$defs/ParticipantId"
},
"target_name": {
"type": [
"string",
"null"
]
},
"status_id": {
"type": "string"
},
"source_uid": {
"description": "The unit that applied the status — the *source* (`Status.instigator`).",
"anyOf": [
{
"$ref": "#/$defs/ParticipantId"
},
{
"type": "null"
}
]
},
"source_name": {
"type": [
"string",
"null"
]
},
"stacks": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0
},
"duration_ms": {
"description": "The status's configured total lifetime at this event, in ms (game-clock\nseconds × 1000). Extendable in-game (`rpcExtendDuration`), so a\nRefreshed event can carry a larger value than its Applied. This is the\ntotal granted, NOT time-remaining (see `remaining_duration_ms`).",
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 0
},
"start_time_ms": {
"description": "The game-clock timestamp (ms) at which this status began\n(`Status.startTime` × 1000, on the `serverNow` base). Immutable for a\ngiven status — a Refreshed event extends `duration_ms`, not the start.\nCombined with a per-event `server_ms`, lets a consumer reconstruct the\nexact remaining at any point. `None` when `startTime` couldn't be read.",
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 0
}
},
"required": [
"time_ms",
"kind",
"target_uid",
"status_id"
]
},
"StatusEventKind": {
"type": "string",
"enum": [
"Applied",
"Refreshed",
"Expired"
]
}
}
}