Documentation
¶
Overview ¶
Package save persists and restores the simulation World as JSON.
The on-disk envelope ships a richer header from day one — version, generator, clock_t0, body_catalog_hash, payload — so future schema migrations and the v0.6 multiplayer `session` block can land without bumping every caller. See docs/state-of-game.md §3 v0.4.0 for the rationale.
Index ¶
Constants ¶
const SchemaVersion = 6
SchemaVersion is the on-disk version that Save writes today. v0.4.0 shipped v1; v0.6.0 bumped to v2 to add ManeuverNode.Event for the burn-at-next scheduler; v0.6.5 bumped to v3 to add Payload.Missions for the mission-scaffold slice; v0.7.6 bumped to v4 to add per-node Throttle. v0.8.0 left the version at 4 — the new RCS fields ride along as omitempty additions, with the loader filling defaults for older saves. v0.8.1 bumped to v5 — the first non-additive migration: `Craft *Craft` → `Crafts []Craft` + `ActiveCraftIdx`. v0.9.1 bumps to v6 — Craft.Stages becomes the source of truth for propulsion + mass; pre-v6 craft entries migrate by wrapping the v5 flat fields into a single-element Stages slice (see migrateV5Craft). Load accepts any version in [1, SchemaVersion]; pre-v6 envelopes are translated on load. Bumps that need real migration logic should add a dedicated upgrade pass keyed off File.Version.
Variables ¶
var ( ErrSchemaMismatch = errors.New("save: schema version mismatch") ErrCatalogMismatch = errors.New("save: body catalog hash mismatch") ErrCraftPrimary = errors.New("save: craft primary not found in loaded systems") )
Errors returned by Load.
Functions ¶
func DefaultPath ¶
DefaultPath returns the platform-appropriate save path. Honors $XDG_STATE_HOME on linux/macOS; falls back to ~/.local/state. Windows users can set $XDG_STATE_HOME explicitly.
Types ¶
type ActiveBurn ¶
type ActiveBurn struct {
Mode int `json:"mode"`
DVRemaining float64 `json:"dv_remaining"`
EndTimeNano int64 `json:"end_time_unix_nano"`
PrimaryID string `json:"primary_id"`
Throttle float64 `json:"throttle,omitempty"`
// TargetCraftIdx (v0.9.3+) — see Node.TargetCraftIdx; mirrored
// onto in-flight finite burns so a save mid-rendezvous-burn
// reloads with the burn still tracking its bound target.
TargetCraftIdx int `json:"target_craft_idx,omitempty"`
}
ActiveBurn mirrors sim.ActiveBurn. Throttle (v0.7.6+, schema v4) is omitempty so v1–v3 saves with an in-flight burn round-trip cleanly: absent → 0.0 unmarshals → world.go's stepThrust defaults to 1.0 (the universal pre-v0.7.6 behaviour).
type Craft ¶
type Craft struct {
Name string `json:"name"`
DryMass float64 `json:"dry_mass"`
Fuel float64 `json:"fuel"`
Isp float64 `json:"isp"`
Thrust float64 `json:"thrust"`
PrimaryID string `json:"primary_id"`
R Vec3 `json:"r"`
V Vec3 `json:"v"`
M float64 `json:"m"`
Monoprop float64 `json:"monoprop,omitempty"`
MonopropCapacity float64 `json:"monoprop_capacity,omitempty"`
RCSThrust float64 `json:"rcs_thrust,omitempty"`
RCSIsp float64 `json:"rcs_isp,omitempty"`
LoadoutID string `json:"loadout_id,omitempty"`
Role string `json:"role,omitempty"`
Glyph string `json:"glyph,omitempty"`
Color string `json:"color,omitempty"`
// v0.9.1+: per-stage breakdown, bottom-first. omitempty so pre-
// v6 saves don't write the field and v6 saves of single-stage
// craft still wire it out for consumers that want stage-level
// detail.
Stages []Stage `json:"stages,omitempty"`
// v0.8.3+: docked-composite components for Undock to restore.
// Empty for non-composite craft.
DockedComponents []DockedComponent `json:"docked_components,omitempty"`
// v0.8.1+ — per-craft burn state. Pre-v5 saves had Nodes /
// ActiveBurn / etc. on the Payload (one shared list); the
// migration on load splits the singular into the active craft's
// fields.
Nodes []Node `json:"nodes,omitempty"`
ActiveBurn *ActiveBurn `json:"active_burn,omitempty"`
AttitudeMode int `json:"attitude_mode,omitempty"`
EngineMode int `json:"engine_mode,omitempty"`
// Target (v0.9.3 polish): per-craft target binding. Pre-polish
// saves had a single payload-level Target; the load path now
// migrates that into the active craft's slot when no per-craft
// targets are present. omitempty so legacy saves with no target
// AND fresh untargeted craft both round-trip without writing the
// field.
Target *Target `json:"target,omitempty"`
// PitchTrim (v0.9.2+, schema v6 additive): signed pitch-trim
// offset in radians applied on top of the active BurnMode.
// omitempty so legacy saves with no trim load with PitchTrim=0
// (= no trim, the v0.9.2-pre behaviour).
PitchTrim float64 `json:"pitch_trim,omitempty"`
// CurrentAttitudeDir (v0.10.0+, schema v6 additive): the craft's
// physical nose unit vector. Slew makes attitude load-bearing —
// a craft can be caught mid-slew — so the real nose must round-
// trip or a reload teleports it. Pre-v0.10.0 saves lack the key →
// decodes to a zero Vec3 → the slew integrator's first-tick snap
// guard seeds it from the commanded direction (no teleport, no
// slew-from-garbage). No schema bump (additive). SlewRate is NOT
// persisted — it is re-derived from the loadout on load.
CurrentAttitudeDir Vec3 `json:"current_attitude_dir,omitempty"`
// Landed (v0.9.2+, schema v6 additive): true when the craft is
// parked on its primary's surface co-rotating with the ground.
// Pre-v0.9.2 saves load with Landed=false (= normal integration,
// the v0.9.2-pre behaviour).
Landed bool `json:"landed,omitempty"`
// LaunchLatDeg / LaunchLonDeg (v0.9.2+, schema v6 additive):
// body-fixed (lat, lon) of the launchpad spawn. Only meaningful
// when Landed=true.
LaunchLatDeg float64 `json:"launch_lat_deg,omitempty"`
LaunchLonDeg float64 `json:"launch_lon_deg,omitempty"`
}
Craft mirrors spacecraft.Spacecraft. Primary is referenced by ID; the rehydrated value is looked up across loaded systems on Load.
Monoprop / MonopropCapacity / RCSThrust / RCSIsp (v0.8.0+, schema v4) are omitempty so v1–v3 saves round-trip cleanly: absent → 0.0 in JSON, populated from spacecraft.DefaultRCSLoadout(DryMass) at load time so older saves inherit RCS without a migration.
Stages (v0.9.1+, schema v6) is the source of truth for propulsion + mass. Pre-v6 saves omit the field; the load path wraps the v5 flat fields (DryMass / Fuel / Isp / Thrust / Monoprop / etc.) into a single-element Stages slice via migrateV5Craft so the rehydrated Spacecraft has Stages populated regardless of save vintage. The flat fields stay on the wire for v6 too — they're derived shadow-mirror values that round-trip with the same numbers SyncFields would compute, so a v6 save loaded into a hypothetical v5 reader (none in production, but possible for tooling) would still see a coherent craft.
type DockedComponent ¶ added in v0.8.3
type DockedComponent struct {
Name string `json:"name"`
LoadoutID string `json:"loadout_id,omitempty"`
Role string `json:"role,omitempty"`
Glyph string `json:"glyph,omitempty"`
Color string `json:"color,omitempty"`
DryMass float64 `json:"dry_mass"`
FuelCapacity float64 `json:"fuel_capacity,omitempty"`
MonopropCapacity float64 `json:"monoprop_capacity,omitempty"`
Isp float64 `json:"isp,omitempty"`
Thrust float64 `json:"thrust,omitempty"`
RCSThrust float64 `json:"rcs_thrust,omitempty"`
RCSIsp float64 `json:"rcs_isp,omitempty"`
}
DockedComponent mirrors spacecraft.DockedComponent. v0.8.3+.
type File ¶
type File struct {
Version int `json:"version"`
Generator string `json:"generator"`
ClockT0 int64 `json:"clock_t0"`
BodyCatalogHash string `json:"body_catalog_hash"`
Payload Payload `json:"payload"`
}
File is the on-disk envelope.
type Node ¶
type Node struct {
TriggerTimeNano int64 `json:"trigger_time_unix_nano"`
Mode int `json:"mode"`
DV float64 `json:"dv"`
DurationNano int64 `json:"duration_nano"`
PrimaryID string `json:"primary_id"`
Event int `json:"event,omitempty"`
Throttle float64 `json:"throttle,omitempty"`
// TargetCraftIdx (v0.9.3+) is the one-based slate idx the node
// is bound to for target-relative modes / TriggerNextClosest
// Approach event. Zero = no target. Additive — pre-v0.9.3 saves
// load with TargetCraftIdx=0, which is correct semantics for
// non-target nodes. No schema bump required.
TargetCraftIdx int `json:"target_craft_idx,omitempty"`
}
Node mirrors sim.ManeuverNode. Event (v0.6.0+, schema v2) is omitempty so v1 saves round-trip cleanly: the field is absent on disk and unmarshals to zero (TriggerAbsolute), which matches the pre-v0.6 behaviour. v2 saves with non-zero Event encode the integer directly. Throttle (v0.7.6+, schema v4) is omitempty so v1–v3 saves round-trip cleanly — absent → 0.0 in JSON, mapped to 1.0 (full throttle, the prior universal behaviour) in worldFromPayload.
type Payload ¶
type Payload struct {
SystemIdx int `json:"system_idx"`
SimTimeNano int64 `json:"sim_time_unix_nano"`
BaseStepNano int64 `json:"base_step_nano"`
WarpIdx int `json:"warp_idx"`
Paused bool `json:"paused"`
Focus Focus `json:"focus"`
Target *Target `json:"target,omitempty"` // v0.9.0+ unified target slot. nil pointer (zero/None) → omitted on the wire.
Craft *Craft `json:"craft,omitempty"` // v1–v4 singular form; migrated to Crafts on load.
Crafts []Craft `json:"crafts,omitempty"`
ActiveCraftIdx int `json:"active_craft_idx,omitempty"`
Nodes []Node `json:"nodes,omitempty"`
ActiveBurn *ActiveBurn `json:"active_burn,omitempty"`
Missions []missions.Mission `json:"missions,omitempty"`
}
Payload carries the live simulation state. Anything derivable from the catalog (Systems, Calculator) is reconstructed on Load.
v0.8.1 / schema v5: `Craft *Craft` (singular pointer) replaced by `Crafts []Craft` (slice) + `ActiveCraftIdx`. Pre-v5 saves with a non-nil singular `Craft` field are translated by `migrateV4ToV5` in save_migrate.go on load.
type Stage ¶ added in v0.9.1
type Stage struct {
LoadoutID string `json:"loadout_id,omitempty"`
Name string `json:"name,omitempty"`
Glyph string `json:"glyph,omitempty"`
Color string `json:"color,omitempty"`
DryMass float64 `json:"dry_mass,omitempty"`
FuelMass float64 `json:"fuel_mass,omitempty"`
FuelCapacity float64 `json:"fuel_capacity,omitempty"`
Thrust float64 `json:"thrust,omitempty"`
Isp float64 `json:"isp,omitempty"`
MonopropMass float64 `json:"monoprop_mass,omitempty"`
MonopropCap float64 `json:"monoprop_cap,omitempty"`
RCSThrust float64 `json:"rcs_thrust,omitempty"`
RCSIsp float64 `json:"rcs_isp,omitempty"`
BallisticCoefficient float64 `json:"ballistic_coefficient,omitempty"`
}
Stage mirrors spacecraft.Stage on the wire. v0.9.1+. All numeric fields are omitempty so a single-stage craft with default RCS pool + zero monoprop residual still serialises compactly.
type Target ¶ added in v0.9.0
type Target struct {
Kind int `json:"kind"`
BodyIdx int `json:"body_idx,omitempty"`
CraftIdx int `json:"craft_idx,omitempty"`
}
Target mirrors sim.Target by value. v0.9.0+. The zero value (Kind=0=TargetNone, BodyIdx=0, CraftIdx=0) is suppressed by the payload's `omitempty` tag, so saves predating v0.9.0 round-trip without writing the field — and load fills sim.World.Target with the zero value, matching pre-target behaviour.