Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,52 @@ Example: `r.resource[ElectricCharge]`, `r.resource[LiquidFuel]`, `r.resource[Oxi
| `alarm.nextAlarm` | Next alarm to trigger |
| `alarm.timeToNext` | Time until next alarm (s) |

### `recovery.*` / `crash.*` / `flight.*` — Mission outcomes

Snapshot keys captured from `GameEvents` — readable from any scene (including Space Center / Tracking Station after the flight) so a dashboard can surface a "last outcome" panel.

| Key | Description |
|-----|-------------|
| `recovery.lastSummary` | Last `MissionRecoveryDialog` content (vesselName, recoveryFactor, scienceEarned, fundsEarned, parts[], resources[], crew[]) + FlightLogger stats |
| `recovery.hasRecent` | Whether a recovery snapshot is available |
| `crash.lastCrash` | Most recent notable-vessel crash — terrain, water, or non-collision loss (burn-up / structural). Debris/flags excluded. Full object below |
| `crash.hasRecent` | Whether a crash snapshot is available |
| `flight.events` | Current flight's `FlightLogger.eventLog` (live) |
| `flight.achievements` | Highest altitude / speed / G / partsLost / etc. |

> `PRELAUNCH` recovery (the cheap launchpad refund path) doesn't fire `onVesselRecoveryProcessingComplete` so it produces no snapshot. Flight-scene recoveries and the post-landing summary dialog both work.

<details><summary><code>crash.lastCrash</code> — full object</summary>

A single-slot "last notable crash", fed by three KSP signals: `onCrash` (terrain), `onCrashSplashdown` (water), and `onVesselWillDestroy` (a non-collision loss — re-entry burn-up, structural/aero break-up, or an impact so fast `CollisionEnhancer` destroyed the craft before a collision event fired). Persists across scenes; cleared on KSP restart.

**Excluded** (never recorded): vessels of type `Debris`, `Flag`, or `Unknown` — spent boosters and the like won't clobber the slot and hide the real crash (`SpaceObject`/asteroids are kept, being pilotable). The `onVesselWillDestroy` path additionally requires the **active** vessel, a situation other than `PRELAUNCH`, and that no revert / recovery / scene change is in progress.

| Field | Type | Notes |
|-------|------|-------|
| `eventKind` | string | `Crash` (terrain) · `CrashSplashdown` (water) · `Destroyed` (non-collision) |
| `vesselName` | string | |
| `vesselType` | string | KSP `VesselType` — `Ship`, `Probe`, `Lander`, `SpaceObject`, … (never Debris/Flag/Unknown) |
| `vesselId` | string | vessel GUID |
| `body` | string | celestial body, e.g. `Kerbin` |
| `situation` | string | KSP situation at death — `FLYING`, `LANDED`, `SPLASHED`, … |
| `latitude` / `longitude` / `altitude` | number | site of the event (4dp) |
| `ut` | number | universal time |
| `what` | string | what was struck — collision paths only; empty for `Destroyed` |
| `msg` | string | collision message; empty for `Destroyed` |
| `partsLost` | array | `{ partName, partTitle, partId, msg }` |
| `crewAboard` | string[] | crew aboard at the time |
| `kerbalsKilled` | string[] | crew killed |
| `events` | string[] | `FlightLogger` event log (e.g. `"… exploded due to overheating: 2201 / 2200 K"`) |
| `flightStats` | object | `FlightLogger` mission stats — `highestSpeed`, `highestAltitude`, `highestGee`, `flightEndMode`, … |

Two value-shape notes:

- **Collision** (`Crash` / `CrashSplashdown`): `partsLost` lists the parts that struck, coalesced over a 5 s window; `what` and `msg` are populated.
- **`Destroyed`**: `what` and `msg` are empty, and `partsLost` is whatever parts remained at destroy time — often `[]` for a burn-up, since parts cook off individually before the vessel-destroy fires. The `events` log is then the record of what happened.

</details>

### `m.*` — Map view

| Key | Description |
Expand Down
405 changes: 405 additions & 0 deletions Telemachus/src/CrashDataHandler.cs

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions Telemachus/src/FlightLogHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace Telemachus
{
/// <summary>Read-access to KSP's FlightLogger — same data the in-game Flight Results dialog renders. AlwaysEvaluable because the snapshot helper degrades gracefully outside flight.</summary>
public class FlightLogHandler : DataLinkHandler
{
public FlightLogHandler(FormatterProvider formatters)
: base(formatters) { }

[TelemetryAPI("flight.events",
"Pre-formatted event timeline from KSP's FlightLogger — the same " +
"strings the in-game Flight Results dialog renders in the Flight " +
"Events panel. Empty list outside flight.",
AlwaysEvaluable = true,
Plotable = false,
Category = "flight",
ReturnType = "object")]
object Events(DataSources ds) => FlightLoggerSnapshot.CaptureEvents();

[TelemetryAPI("flight.achievements",
"Running mission stats from KSP's FlightLogger — same data the " +
"Flight Results dialog renders in the Flight Achievements panel. " +
"Fields: missionTime, liftOff, highestAltitude, highestSpeed, " +
"highestSpeedOverLand, groundDistance, totalDistance, highestGee, " +
"partsLost (count), kerbalsKilled (count), missionEnd, " +
"flightEndMode. Returns null outside flight.",
AlwaysEvaluable = true,
Plotable = false,
Category = "flight",
ReturnType = "object")]
object Achievements(DataSources ds) => FlightLoggerSnapshot.Capture();
}
}
107 changes: 107 additions & 0 deletions Telemachus/src/FlightLoggerSnapshot.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;

namespace Telemachus
{
/// <summary>Shared snapshot helper for FlightLogger — most fields are private instance fields, accessed via cached reflection.</summary>
public static class FlightLoggerSnapshot
{
private static bool _reflectionResolved;
private static FieldInfo _highestAltitude;
private static FieldInfo _groundDistance;
private static FieldInfo _totalDistance;
private static FieldInfo _highestGee;
private static FieldInfo _highestSpeed;
private static FieldInfo _highestSpeedOverLand;
private static FieldInfo _liftOff;
private static FieldInfo _missionEnd;
private static FieldInfo _partsLost;
private static FieldInfo _kerbalsKilled;
private static FieldInfo _flightEndMode;

private static void EnsureReflection()
{
if (_reflectionResolved) return;
var t = typeof(FlightLogger);
const BindingFlags bf = BindingFlags.NonPublic | BindingFlags.Instance;
_highestAltitude = t.GetField("highestAltitude", bf);
_groundDistance = t.GetField("groundDistance", bf);
_totalDistance = t.GetField("totalDistance", bf);
_highestGee = t.GetField("highestGee", bf);
_highestSpeed = t.GetField("highestSpeed", bf);
_highestSpeedOverLand = t.GetField("highestSpeedOverLand", bf);
_liftOff = t.GetField("liftOff", bf);
_missionEnd = t.GetField("missionEnd", bf);
_partsLost = t.GetField("partsLost", bf);
_kerbalsKilled = t.GetField("kerbalsKilled", bf);
_flightEndMode = t.GetField("flightEndMode", bf);
_reflectionResolved = true;
}

private static double R4(double v) => Math.Round(v, 4);

private static T ReadField<T>(FieldInfo f, FlightLogger logger, T fallback)
{
if (f == null || logger == null) return fallback;
try
{
var v = f.GetValue(logger);
if (v is T typed) return typed;
return fallback;
}
catch (Exception)
{
return fallback;
}
}

/// <summary>Snapshot current FlightLogger state — returns null when FlightLogger.fetch isn't available (no flight scene).</summary>
public static Dictionary<string, object> Capture()
{
try
{
var logger = FlightLogger.fetch;
if (logger == null) return null;
EnsureReflection();
return new Dictionary<string, object>
{
["missionTime"] = R4(FlightLogger.met),
["liftOff"] = FlightLogger.LiftOff,
["highestAltitude"] = R4(ReadField(_highestAltitude, logger, 0.0)),
["highestSpeed"] = R4(ReadField(_highestSpeed, logger, 0.0)),
["highestSpeedOverLand"] = R4(ReadField(_highestSpeedOverLand, logger, 0.0)),
["groundDistance"] = R4(ReadField(_groundDistance, logger, 0.0)),
["totalDistance"] = R4(ReadField(_totalDistance, logger, 0.0)),
["highestGee"] = R4(ReadField(_highestGee, logger, 0.0)),
["partsLost"] = ReadField(_partsLost, logger, 0),
["kerbalsKilled"] = ReadField(_kerbalsKilled, logger, 0),
["missionEnd"] = ReadField(_missionEnd, logger, false),
["flightEndMode"] =
ReadField<object>(_flightEndMode, logger, null)?.ToString() ?? string.Empty,
};
}
catch (Exception e)
{
Debug.LogWarning("[Telemachus] FlightLoggerSnapshot.Capture failed: " + e.Message);
return null;
}
}

/// <summary>Copy of FlightLogger.eventLog so later mutations don't change captured snapshots.</summary>
public static List<string> CaptureEvents()
{
try
{
var log = FlightLogger.eventLog;
if (log == null) return new List<string>();
return new List<string>(log);
}
catch (Exception)
{
return new List<string>();
}
}
}
}
3 changes: 3 additions & 0 deletions Telemachus/src/KSPAPIBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ public KSPAPI(FormatterProvider formatters, VesselChangeDetector vesselChangeDet
APIHandlers.Add(new ScienceCareerDataLinkHandler(formatters));
APIHandlers.Add(new TimeWarpDataLinkHandler(formatters));
APIHandlers.Add(new TargetDataLinkHandler(formatters));
APIHandlers.Add(new RecoveryDialogHandler(formatters));
APIHandlers.Add(new CrashDataHandler(formatters));
APIHandlers.Add(new FlightLogHandler(formatters));

APIHandlers.Add(new CompoundDataLinkHandler(
new List<DataLinkHandler> {
Expand Down
Loading
Loading