Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

KSPExternalVesselHistory (KSPEVH)

In-game Kerbal Space Program 1.12.5 plugin that captures vessel and kerbal history events as they happen, persists them as append-only JSONL journals, and exposes them over MQTT (live event stream + retained per-entity snapshots) and HTTP (queryable indexed history, plus a Server-Sent-Events live stream and a bundled mission-control test console).

Companion to:

  • KSPBridge — real-time MQTT vessel telemetry (current state).
  • KSPExternalVesselUtilities (KSPEVU) — vessel geometry export to glTF over HTTP+MQTT (what the ship looks like).

The three together let any consumer assemble what's happening now (KSPBridge), what the ship looks like (KSPEVU), and what the ship has done (KSPEVH) for a single vessel — keyed on the same Vessel.id / persistentId correlation fields, all over one MQTT broker.

Status — v0.3.0 (alpha)

The plugin builds, loads, hooks GameEvents, and writes JSONL journals + retained MQTT snapshots. It is save-aware (per-save physical separation), branch-aware (each quickload/revert starts a new branch so reload chronology stays clean), enriches every recorded event with geo / motion / orbit / state context, and ships two self-contained browser views: the mission-control test page at http://localhost:8091/ and the astronaut career page at http://localhost:8091/career.html.

Each kerbal snapshot now carries identity (trait, experienceLevel, gender), classification (type, rosterStatus), and live position (currentVesselPid, currentVesselName, currentBody, currentSituation, biome, latitude, longitude, altitude) fields lifted directly from the live CrewRoster and game state. Each vessel snapshot carries the matching geo / motion / orbit / state set so consumers can query "where is X right now and what state is it in" without scanning the per-event JSONL.

Coverage of the full event catalogue is still partial — high-value events (observe, launch, situation change, SOI change, staging, recovery, crash, crew board / EVA / transfer / killed, career milestones, vessel destroy) are wired up; lineage (decouple/couple), science, maneuvers, and several kerbal-career events remain TODO.

Not yet ready for end-user installation. Code is public for review and feedback.

What it captures

Every event records:

  • ut — in-game universe time (seconds).
  • realTs — wall-clock UTC ISO 8601 timestamp.
  • type — one of the event-type constants.
  • vesselPid + vesselPersistentId — vessel correlation keys matching KSPBridge's wire format.
  • crew[] — kerbal names involved (full vessel roster on vessel-state events; subject only on transitions).
  • data{} — event-specific payload (body name, situation, altitude, experiment id, etc.).
  • branch — branch identifier within the active save (see below).
  • category — one of "vessel", "crew", "transition" — determines which journal file the event lands in.

Vessel events covered today: vessel_observed, vessel_launched, vessel_situation_changed, vessel_soi_changed, vessel_stage_activated, vessel_recovered, vessel_destroyed, vessel_crashed.

Kerbal-transition events: kerbal_boarded, kerbal_eva, kerbal_transferred, kerbal_died.

Cross-cutting: progress_completed (career milestones).

Planned: vessel_decoupled / vessel_coupled (lineage graph from onPartCouple / onPartUndock), vessel_renamed, science_collected, maneuver_executed, kerbal_recruited / kerbal_level_changed / kerbal_stranded / kerbal_recovered.

Save awareness, branching, and categorization

Save awareness. Every save is fully isolated on disk. KSP saves named default, Career_Hard, Sandbox_Test each get their own subdirectory under data/saves/. Vessel pids and kerbal names that overlap between saves (very common — Jebediah Kerman shows up in nearly every save) never share files. The plugin tracks HighLogic.SaveFolder via the onGameStateLoad hook and routes journal writes accordingly.

Branching. When you quickload (F9), revert flight, or load a different save, KSP rewinds in-game time. Without branch awareness the journal would have events at UT=1100 followed by events at UT=1050, and "what happened most recently" would be ambiguous. The plugin solves this by treating each reload as a new branch: currentBranch increments, every subsequent event carries the new branch id, snapshots reflect only the current branch. Older-branch events stay in the JSONL files forever (append-only, crash-safe) but are filtered out of "current state" projections. Branch boundaries are recorded in index.json.branches with the real wall-clock timestamp and reason ("initial", "game_state_load", or "ut_rollback_inferred" from the safety-net heuristic).

Event categorization. Each emitted event carries a category that determines which logs receive it:

Category Example types Filed to
vessel observed, launched, situation_changed, soi_changed, stage_activated vessel JSONL only
crew recruited, level_changed, stranded, observed kerbal JSONL only
transition boarded, eva, transferred, died, recovered/crashed/destroyed when crewed both vessel and kerbal JSONLs

Cross-filing is bounded by transition count (≤20 per typical mission), so per-kerbal queries stay fast (single-file linear read) without paying duplication tax on every staging or situation-change event. Vessel-state events with crew aboard still update the kerbal snapshot (so kerbals appear in the index the moment their ship is observed) without copying the event line into the kerbal JSONL.

crewHistory on vessel snapshots and vesselsServed on kerbal snapshots maintain the timeline join key — open entries (toUt: null) mean "currently aboard"; transitions close them with the reason (eva, transferred, died, recovered, destroyed).

How it publishes

Two flavours, parallel to KSPBridge's pattern.

Channel Topic / Path Retained Purpose
MQTT ksp/history/event no Streaming, one message per event.
MQTT ksp/history/vessel/<pid>/snapshot yes Current vessel summary.
MQTT ksp/history/vessel/<pid>/recent yes Last N vessel events.
MQTT ksp/history/kerbal/<name>/snapshot yes Current kerbal summary.
MQTT ksp/history/kerbal/<name>/recent yes Last N kerbal events.
MQTT ksp/history/_bridge/status yes Online flag + version, with LWT on disconnect.
HTTP GET / Bundled mission-control test page (HTML).
HTTP GET /health Plugin status, current save + branch, MQTT/SSE counts.
HTTP GET /saves.json List of every save we have data for.
HTTP GET /save/<save>/health Save-scoped health.
HTTP GET /save/<save>/vessels.json Vessel index for one save.
HTTP GET /save/<save>/kerbals.json Kerbal index for one save.
HTTP GET /save/<save>/vessel/<pid> Vessel snapshot.
HTTP GET /save/<save>/vessel/<pid>/events?branch=N&limit=N Recent events (JSON array, branch-filterable).
HTTP GET /save/<save>/vessel/<pid>/events.jsonl Raw JSONL stream of all events ever.
HTTP GET /save/<save>/kerbal/<name> Kerbal snapshot.
HTTP GET /save/<save>/kerbal/<name>/events?branch=N&limit=N Kerbal-side events.
HTTP GET /save/<save>/kerbal/<name>/events.jsonl Raw JSONL.
HTTP GET /save/<save>/kerbal/<name>/full?branch=N Server-side join: kerbal log + each served vessel's slice, UT-ordered.
HTTP GET /save/<save>/kerbal/<name>/portrait.png Captured in-flight portrait, or bundled silhouette fallback.
HTTP GET /stream Server-Sent Events live push of every recorded event.

The unprefixed routes (/vessels.json, /kerbal/<name>, etc.) default to the active save. The /save/<name>/... prefix lets you address any save on disk, current or not.

Bulky reads go through HTTP; MQTT carries small notifications and retained per-entity summaries. Same split-of-roles KSPEVU uses for geometry (.glb over HTTP, ready notification over MQTT).

Kerbal portraits

Whenever a kerbal appears in KSP's portrait gallery (bottom-right of the Flight scene during a crewed mission), the plugin captures the live RenderTexture, encodes it as PNG, and writes it to data/saves/<save>/kerbals/portraits/<name>.png. Capture is per-save — a "Jeb" in save A and a "Jeb" in save B keep their own files.

GET /save/<save>/kerbal/<name>/portrait.png returns the captured PNG if present, otherwise a server-side fallback (WebRoot/img/no-portrait.svg, a kerbal-shaped silhouette with "NO IMAGE" caption). The route never 404s — the client <img> always renders something.

Coverage is "any kerbal you've flown with" — which is the same population that would show up on any career-style page. Recruited-but-unflown kerbals get the silhouette until their first mission.

Mission-control test page

http://localhost:8091/ serves a self-contained HTML console with:

  • Status header — HTTP / MQTT / SSE LEDs, current save name, current branch id, uptime, vessels & kerbals tracked, events received.
  • Vessels panel — clickable list of every vessel observed in the active save's current branch.
  • Kerbals panel — clickable list of every kerbal seen, even if the only event for them so far is "observed aboard a vessel."
  • Live event ticker — Server-Sent Events stream, newest at top, with category-coded color (cyan for vessel, gold for transition).
  • Detail panel — snapshot fields plus the entity's last 20 events.
  • CAREER link in the header to the astronaut career page.

Useful as a smoke test, a debug surface, and a reference implementation for any consumer of the HTTP/SSE API.

Astronaut career page

http://localhost:8091/career.html serves a different view — a grid of trading-card-style profiles, one per kerbal in the active save. Cards deal in from above with a staggered 3D animation, settle into the grid, and flip on click to reveal a mission record on the back.

  • Card front: portrait (captured from the in-flight gallery, or the silhouette fallback if not yet captured), name, a cyan JOB · LEVEL · ♂/♀ role line (e.g. PILOT · LV 3 · ♂), the most recent event, total event count, current UT.
  • Card back: Job / Level / Gender / Vessels served / Mission days / Total events / Last event / Last seen, plus the per-vessel vesselsServed[] history (vessel name + duration + departure reason) and a status badge (ACTIVE / RECOVERED / KIA / UNASSIGNED derived from vesselsServed open/close state).
  • Filter toolbar: two filter rows (TYPE: All / Crew / Tourist / Applicant / Unowned, STATUS: All / Available / Assigned / Dead / Missing), AND-combined across rows, multi-select within each row. An ACTIVE preset chip resets to Crew + (Available, Assigned). Selection persists across reloads via localStorage.
  • Section grouping: GROUP BY: VESSEL (default — vessel name as amber section header, "In orbit around Duna" / "Landed in Mun Highlands" as a cyan sublabel) or BODY (Kerbin first, then Mun / Minmus / etc) or NONE (flat grid). Kerbals not currently aboard a vessel land in a "Kerbal Space Center" section at the bottom.
  • RELOAD refetches the roster; FLIP ALL toggles every card simultaneously.

Roster comes from GET /kerbals.json; portraits from GET /kerbal/<name>/portrait.png; back-of-card details lazy-loaded from GET /kerbal/<name> on first flip.

Snapshot enrichment

Every kerbal and vessel snapshot now carries typed top-level fields lifted from the live game state. Consumers can filter, group, and display without scanning the per-event JSONL or chasing fields out of the latest event's data dict.

KerbalSnapshot / KerbalSummary (also surfaced in /kerbals.json and /save/<save>/kerbals.json):

  • Identity: trait ("Pilot" / "Engineer" / "Scientist" / "Tourist"), experienceLevel (0-5 stars), gender ("Male" / "Female").
  • Classification: type (KerbalType enum — Crew / Tourist / Applicant / Unowned), rosterStatus (RosterStatus enum — Available / Assigned / Dead / Missing).
  • Live position: currentVesselPid, currentVesselName, currentBody, currentSituation (LANDED / ORBITING / FLYING / etc), biome, biomeDisplay, latitude, longitude, altitude.
  • Off-vessel kerbals (Available at KSC, Dead, Missing) have null current-position fields so consumers can distinguish "currently flying" from "in standby".

VesselSnapshot / VesselSummary (also surfaced in /vessels.json and /save/<save>/vessels.json):

  • Identity: vesselName, currentBody.
  • Geo: biome, biomeDisplay, latitude, longitude, altitude.
  • Motion: surfaceSpeed, orbitalSpeed, verticalSpeed.
  • Orbit: apoapsis, periapsis, inclination, eccentricity, period.
  • State: mass, currentStage, liquidFuel, oxidizer, electricCharge, monoPropellant.
  • Numeric fields are nullable (double? / int?) so consumers can distinguish "never observed for this vessel" from "observed and 0".

Population: every event write copies enrichment from the event's data dict into the typed snapshot fields (preserve-on-absent — events that don't carry a key keep the prior snapshot value, never blanking it). On every save load, EnrichKerbalSnapshotsFromRoster and EnrichVesselSnapshotsFromGameState walk all snapshot files and rewrite from live CrewRoster / flightState.protoVessels / FlightGlobals.Vessels. Both passes are idempotent.

Event payload enrichment

Nine event types now record full vessel context in their data payload via the EventEnrichment helper. The same field names are used across every event so consumers don't have to branch on event type.

Event Geo Motion Orbit State
vessel_observed
vessel_situation_changed
vessel_launched
vessel_crashed
kerbal_died
vessel_soi_changed
vessel_recovered
vessel_destroyed
vessel_stage_activated

Field set:

  • Geo: biome, biomeDisplay, latitude, longitude, altitude, radarAltitude.
  • Motion: surfaceSpeed, orbitalSpeed, verticalSpeed, mach (when in atmosphere), gForce (when non-zero).
  • Orbit: apoapsis, periapsis, inclination, eccentricity, period.
  • State: mass, currentStage, liquidFuel, oxidizer, electricCharge, monoPropellant.

These fields are persisted in the per-event JSONL forever (canonical historical record) and surface on the latest snapshot via the promoted typed fields described above. Adding a new field to the helper is one line — every opted-in handler gets it for free.

On-disk layout

GameData/KSPEVH/data/
└── saves/
    ├── <save_name>/
    │   ├── index.json                  vessels[], kerbals[], branches[], currentBranch
    │   ├── vessels/
    │   │   ├── <pid>.json              snapshot — current state only
    │   │   └── <pid>.events.jsonl      append-only event log (all branches)
    │   └── kerbals/
    │       ├── <name>.json             snapshot — current state only
    │       ├── <name>.events.jsonl     career-only events (board/EVA/transfer/die/etc.)
    │       └── portraits/
    │           └── <name>.png          captured in-flight portrait (one per kerbal)
    ├── <other_save>/...
    └── _premigration/                  pre-save-aware data, if you upgraded from v0.1.x

Each <save_name> is the raw HighLogic.SaveFolder from KSP. To copy a save's history to another machine, copy that one subdirectory. To wipe a save without losing the others, delete it.

JSONL (one event per line, no commas, no array wrapping) was chosen over a single big JSON because:

  • Append is constant-cost regardless of log size.
  • Survives partial writes — a crash mid-write corrupts only the last line, not the entire log.
  • Streams cleanly over HTTP for consumers that want incremental reads.

A future patch may add a SQLite mode for very long-running saves; the JSONL format stays as the canonical interchange format and default storage.

Forensic traversal — diving the data manually

Everything on disk is plain text. Anyone with PowerShell, Notepad, or cat can investigate. Snapshots (.json) are pretty-printed single objects; event logs (.events.jsonl) are one JSON object per line. Common queries:

List every event ever recorded for a vessel:

Get-Content "data\saves\<save>\vessels\<pid>.events.jsonl"

Filter by event type:

Select-String -Path "data\saves\<save>\vessels\*.events.jsonl" `
  -Pattern '"type":"vessel_situation_changed"'

Limit to the current branch (consult index.json.currentBranch for the value):

Get-Content "data\saves\<save>\vessels\<pid>.events.jsonl" |
  Where-Object { $_ -match '"branch":2' }

Find a kerbal's events in a UT range:

Get-Content "data\saves\<save>\kerbals\Jebediah Kerman.events.jsonl" |
  Where-Object {
    $e = ($_ | ConvertFrom-Json)
    $e.ut -ge 1000 -and $e.ut -le 2000
  }

See branch boundaries (when each branch started, why):

(Get-Content "data\saves\<save>\index.json" | ConvertFrom-Json).branches

Find which vessel a kerbal was on at a given UT — open kerbals/<name>.json and walk vesselsServed for the entry whose fromUt ≤ targetUt < toUt (or where toUt is null, meaning still aboard).

Find who was on a vessel at a given UT — same thing in reverse, walk vessels/<pid>.json's crewHistory.

Settings

GameData/KSPEVH/Settings.cfg:

KSPEVH
{
    broker_host  = localhost
    broker_port  = 1883
    topic_prefix = ksp/history
    client_id    = kspevh
    http_port    = 8091
    data_dir     = data
    recent_events_count = 50
}

Defaults assume Mosquitto on the same machine as KSP. For a remote broker, point broker_host at its hostname or IP. The recent_events_count value caps the in-memory ring buffer driving the MQTT recent topic and the default page size for HTTP /events routes; the on-disk JSONL is unbounded.

Building

Same conventions as KSPBridge / KSPEVU: requires Visual Studio 2022 or 2026 with .NET desktop development workload + .NET Framework 4.7.1 targeting pack. dotnet build -c Release works too. Post-build target deploys DLLs + WebRoot/index.html + Settings.cfg (only if missing) + KSPEVH.version + LICENSE + THIRD-PARTY-NOTICES.md to <KSP install>/GameData/KSPEVH/.

JSON serialization is handled by an embedded copy of MiniJSON (Calvin Rien, MIT, ~330 lines in src/KSPEVH/History/MiniJson.cs). KSPEVH does not depend on Newtonsoft.Json — Unity 2019.4's Mono runtime cannot resolve System.Runtime.Serialization v4.0.0.0 which Newtonsoft.Json's JsonWriter static constructor requires. MiniJSON has zero external dependencies, so the problem cannot recur.

License

MIT. See LICENSE.

Bundled libraries: see THIRD-PARTY-NOTICES.md.

About

Save-aware, branch-aware vessel and kerbal history recorder for KSP 1.12. Append-only JSONL journals, retained MQTT snapshots, queryable HTTP API, live SSE stream. Companion to KSPBridge and KSPEVU.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages