Skip to content

feat: career-mode handler surface (tech / kc / sci / contracts / launch / actiongroups)#86

Open
jonpepler wants to merge 7 commits into
TeaGuild:mainfrom
jonpepler:telemachus/career-mode
Open

feat: career-mode handler surface (tech / kc / sci / contracts / launch / actiongroups)#86
jonpepler wants to merge 7 commits into
TeaGuild:mainfrom
jonpepler:telemachus/career-mode

Conversation

@jonpepler
Copy link
Copy Markdown
Contributor

@jonpepler jonpepler commented May 11, 2026

⚠️ I've used Claude to make these changes and draft most of the description below. Built, tested locally in KSP, and happy with the result, so sharing here.

Summary

Six new handlers + ~50 data keys covering more of the career-mode UI surface - useful for external dashboards that want to mirror or drive R&D, KSC, science, contracts, and launch/recovery from outside Flight. All new keys are AlwaysEvaluable = true so they're queryable from Space Center / Editor / Tracking Station.

Builds on #84 (now merged) for the AlwaysEvaluable attribute to take effect on the new action keys.

The branch has grown since the first review - a Strategies / Administration Building handler and a CurrencyModifierQuery-aware effective-cost layer have been added on top of the original six handlers. See "Updates since first review" below.

New handlers

Handler Sample keys
TechTreeDataLinkHandler tech.nodes, tech.unlock[id], tech.unlockedIds, tech.unlockedPartCount, tech.affordable
KscDataLinkHandler kc.facilityLevels, kc.upgradeFacility[name], kc.crewRoster, kc.scene, kc.savedShips, kc.partsAvailable
ScienceInstrumentsDataLinkHandler sci.instruments, sci.experimentBreakdown, sci.experiments, sci.canRecoverTotal, sci.canTransmitTotal, sci.deploy[partFlightId, experimentId], sci.transmit[partFlightId]
ContractsDataLinkHandler contracts.active, contracts.offered, contracts.completedRecent, contracts.accept[id], contracts.decline[id], contracts.cancel[id]
LaunchDataLinkHandler ksp.launch[ship, facility, site, crew], ksp.recover, ksp.revertToLaunch, ksp.revertToEditor[vab|sph], ksp.toSpaceCenter, ksp.toTrackingStation, ksp.canRevert*
ActionGroupBindingsDataLinkHandler f.ag.bindings (which parts/modules/actions are bound to each AG)
StrategiesDataLinkHandler strategies.all, strategies.activate[id, factor], strategies.deactivate[id]

Design notes

Contracts long ids as strings. Contract IDs frequently exceed 2^53 and silently lose precision when serialised as JSON numbers. contracts.* rows emit "id": "<digits>" and the action handlers accept both string and number forms for back-compat. Contract parameter rows include the parameter's runtime type plus typed fields (parameterType / minAltitude / maxAltitude / body / situation / partName) so consumers can render real progress bars without name-string heuristics.

Action handlers fire from any scene. ksp.launch refuses outside SC/Editor; ksp.revertToLaunch refuses outside Flight; tech.unlock refuses unaffordably-priced nodes; etc. - internal state checks, not blanket scene gates.

ksp.launch always builds a VesselCrewManifest even for unmanned launches. Passing null NREs inside setStartupNewVessel and leaves Flight half-initialised (frozen HUD, sentinel values).

Recovery / crash / flight-log keys are NOT in this PR - they're separately submitted as event-driven snapshot handlers in #85.

Updates since first review

Two follow-on areas have been added in the same career-mode surface. Both ride this branch.

Tech-tree enrichment

tech.nodes rows now include:

  • description - parsed from the tech tree's underlying RDNode config and run through Localizer.Format so it matches what KSP shows in the R&D dialog.
  • parts - one entry per AvailablePart assigned to the node, with name / title / manufacturer / category / entryCost / purchased. Built once per game-load by indexing PartLoader.LoadedPartsList by TechRequired.

kc.facilityLevels rows now include currentLevelText and nextLevelText (from UpgradeableFacility.GetLevelText(int)) so consumers can show KSP's multi-line upgrade dialog text without mirroring it locally.

Strategies / Administration Building

A new StrategiesDataLinkHandler adds three keys: strategies.all, strategies.activate[id, factor], strategies.deactivate[id]. The interesting bit is that all three work from any scene.

KSP's Strategy.CanBeActivated / Activate / Deactivate dereference Administration.Instance, a MonoBehaviour that's only alive while the Admin Building dialog is open. That makes the stock methods unusable from a remote dashboard when the player isn't in front of the dialog. The handler replicates each check and mutation against publicly-accessible state (StrategySystem, GameVariables, ScenarioUpgradeableFacilities, Funding, Reputation, ResearchAndDevelopment, Planetarium) so the same operations work from Space Center, Editor, Flight, or Tracking Station. Reflection is used only to write Strategy.isActive and Strategy.dateActivated; everything else is a public API call. Behaviour matches stock KSP 1.12.

The replica section is cleanly fenced inside the handler with header/footer comments so the boundary against stock behaviour is obvious to anyone reading the file.

Currency-modifier effective costs

KSP's {Funding, ResearchAndDevelopment, Reputation}.Add* methods apply the mutation before firing GameEvents.Modifiers.OnCurrencyModifierQuery, so consumers can't adjust the deduction by listening - they have to pre-query the modifier separately. Without that step, dashboards showing the nominal cost of a launch / facility upgrade / tech purchase will be wrong whenever a strategy is active (e.g. Aggressive Negotiations advertises -1.5% on launches and R&D purchases, and -0.05% on facility construction).

A new CurrencyModifiers helper wraps the pre-query in a one-liner. Existing rows now include a sidecar field next to each cost:

Existing field New sidecar Transaction reason
kc.facilityLevels.upgradeFunds upgradeFundsEffective StructureConstruction
kc.savedShips.requiresFunds requiresFundsEffective VesselRollout
tech.nodes.scienceCost scienceCostEffective RnDTechResearch
tech.nodes.parts[].entryCost entryCostEffective RnDPartPurchase

strategies.all also includes effectiveCostReputation per strategy, which mirrors Reputation.addReputation_granular's per-unit walk against GameVariables.reputationSubtraction. The rep curve makes losses near the cap cost meaningfully more than at zero - a 14.5-nominal cost can deduct ~27 at 976 rep, so the nominal figure on its own is misleading.

Validation

Tested locally on KSP 1.12.x with a custom HTTP/WS dashboard, in a career save:

  • Tech tree: tech.nodes listed every node + its unlocked state from the Space Center; tech.unlock[experimentalRocketry] deducted science and unlocked the node in the R&D UI on the next tick. Per-node description and parts matched the in-game R&D dialog content.
  • KSC: kc.facilityLevels returned the expected building list with current/max upgrade levels and currentLevelText / nextLevelText matching the in-game upgrade dialog; kc.upgradeFacility[LaunchPad] deducted funds and started the upgrade animation. kc.crewRoster exposed the full kerbal roster including experience and current assignment fields.
  • Contracts: contracts.active rows included parameterType plus the typed fields for the well-known parameter classes (altitude, body, situation, part). Long contract IDs round-tripped correctly as strings.
  • Launch: ksp.launch[Stayputnik X, VAB, LaunchPad, Jeb;Bill] launched the saved craft with the named crew aboard. Refuses outside SC/Editor as designed.
  • Scene-transition battery: ran the full sequence Flight → Space Center → Tracking Station → (refused-launch from TS) → Space Center → launch → recover → Tracking Station. All six ksp.* scene actions worked correctly.
  • Action groups: f.ag.bindings returned one row per (action, group) pair on a vessel with multiple custom bindings.
  • Strategies: strategies.all returned from the Space Center without the Admin dialog being open; the currently-active strategy showed isActive: true / canDeactivate: true and every other strategy was blocked by the stock 1-active-limit gate with the localised human-readable reason. Round-tripped deactivate + activate end-to-end (Aggressive Negotiations → off, Fundraising Campaign → on at factor 0.05); change was reflected immediately in both strategies.all and the in-game Admin Building dialog. Per-strategy effectiveCostReputation matched the post-curve deduction observed on real activation to within ~1 unit.
  • Currency-modifiers: tested with multiple strategies and confirmed their modifiers are correctly applied into values marked effective.
  • No regression in any existing Flight-scene key while these were queried.

jonpepler added 3 commits May 11, 2026 21:23
Six new handlers and ~50 new data keys covering more of the
career-mode UI surface — useful for external dashboards that
want to mirror or drive R&D, KSC, science, contracts, and
launch/recovery from outside Flight:

  TechTreeDataLinkHandler            tech.nodes / tech.unlock / …
  KscDataLinkHandler                 kc.facilities / kc.crewRoster …
  ScienceInstrumentsDataLinkHandler  sci.instruments / sci.breakdown
  ContractsDataLinkHandler           contracts.active / accept / …
  LaunchDataLinkHandler              ksp.launch / recover / revert*
  ActionGroupBindingsDataLinkHandler f.ag.bindings

- All action keys are AlwaysEvaluable = true so they're invokable
  from Space Center / Editor / Tracking Station, not just Flight.
  Each handler refuses internally when the underlying state isn't
  ready (e.g. ksp.launch refuses outside SC/Editor or with an
  active vessel).

- Contracts:
  * Contract IDs serialise as JSON strings (`"id": "12345..."`)
    not numbers, because they often exceed 2^53 and silently lose
    precision in JS.
  * Parameter rows include the parameter's runtime type plus the
    well-known typed fields (minAltitude/maxAltitude/body/situation/
    partName) so consumers can render proper progress bars without
    name-string heuristics.

- Launch: VesselCrewManifest is always built from the .craft,
  even unmanned. Passing null NREs inside setStartupNewVessel and
  leaves Flight half-initialised (frozen HUD, sentinel values).
Rebuilt the schema + openapi to pick up the tech.*, kc.*, sci.*,
contracts.*, ksp.* and f.ag.bindings entries.

`tools/generate-openapi.ts` also now omits the `sourceFile`
field from the committed `docs/api-schema.json`. The field is
populated with an absolute path from the build machine, so it
differs between contributors and produces a large per-rebuild
diff on the committed artifact. Nothing in the docs site
consumes it; keeping it out of the committed JSON keeps the
review surface focused on actual schema changes. The field is
still present on the in-memory entries the generator works with.
Both additions live-verified against a running KSP install.

tech.nodes (TechTreeDataLinkHandler)
  Full tech tree — every node with id, title, description,
  scienceCost, state, parents, parts. AlwaysEvaluable=true so
  the career UI can read it from the Space Center scene without
  an active vessel.

  - Description parses out of tree.GetTreeConfigNode()'s RDNode
    entries and routes through Localizer.Format, so #autoLOC
    tokens resolve to readable English (e.g. "How hard can Rocket
    Science be anyway?" for Basic Rocketry).
  - Parts list builds from PartLoader.LoadedPartsList, grouping
    by AvailablePart.TechRequired. Per-part payload: name
    (internal id), title (display), manufacturer, category
    (PartCategories enum name), entryCost, purchased
    (PartTechAvailable + PartModelPurchased).
  - Parents read directly from ProtoRDNode.parents (elements
    are ProtoRDNode, not wrapped — ProtoRDNode lacks a `.parent`
    field).
  - Two session-stable indexes (_descriptionsByTech,
    _partsByTech) built lazily on the first call and reused for
    the lifetime of the KSP process; payload itself sticky-cached
    during the IsTransientLoadingState() window the way
    unlockedIds / affordable already are.

kc.facilityLevels (KscDataLinkHandler)
  Each facility entry now carries currentLevelText +
  nextLevelText — multi-line descriptions matching the bullet-
  point block KSP's upgrade dialog renders. nextLevelText is
  the empty string when the facility is already at max tier.

  Pulls from UpgradeableFacility.GetLevelText(int lvl) on the
  facilityRefs[0] instance via
  ScenarioUpgradeableFacilities.protoUpgradeables, same access
  path the existing upgradeFunds lookup uses. Wrapped in
  try/catch matching the surrounding style — best-effort, empty
  strings when refs unavailable (e.g. before SC scene loaded).

Curls verified against the live Telemachus instance:
  tech.nodes
    → hundreds of nodes, descriptions resolved, parts list with
      manufacturer + category + entryCost + purchased per part.
  kc.facilityLevels
    → admin currentLevelText starts at tier 1 wording, vab at
      level 2 / max 2 returns nextLevelText "" (correctly empty
      at max).

Builds clean under dotnet 10 against the bundled Assembly-CSharp.
KSP restart required to pick up the new TelemetryAPI entries.
@jonpepler jonpepler force-pushed the telemachus/career-mode branch from ba6ff93 to f23f7f2 Compare May 13, 2026 10:44
jonpepler added 3 commits May 13, 2026 17:09
Adds three TelemetryAPIs under a new StrategiesDataLinkHandler:
strategies.all (read), strategies.activate[id, factor] (action), and
strategies.deactivate[id] (action). The handler bypasses
Administration.Instance — a MonoBehaviour live only while the Admin
Building dialog is open — by replicating Strategy.CanBeActivated /
Activate / Deactivate against StrategySystem, GameVariables,
ScenarioUpgradeableFacilities, Funding, Reputation, ResearchAndDevelopment,
and Planetarium so the operations work from any scene. Reflection is
used only to write Strategy's private `isActive` and `dateActivated`
fields; everything else is a public API call.

strategies.all exposes a sidecar `effectiveCostReputation` per strategy
that mirrors Reputation.addReputation_granular's per-unit walk against
GameVariables.reputationSubtraction so consumers see the real rep loss
on activate (the nominal 14.5 charged near rep cap can deduct ~27).

Adds CurrencyModifiers — a one-liner wrapper around
GameEvents.Modifiers.OnCurrencyModifierQuery — and uses it to bake
sidecar effective-cost fields onto existing endpoints so consumers
see the post-strategy charge alongside the nominal:

  - kc.facilityLevels.upgradeFundsEffective (StructureConstruction)
  - kc.savedShips.requiresFundsEffective (VesselRollout)
  - tech.nodes.scienceCostEffective (RnDTechResearch)
  - tech.nodes.parts[].entryCostEffective (RnDPartPurchase)

README, OpenAPI, and JSON schema updated to match.
The career-mode handler rows leaked internal KSP and Telemachus
terminology into user-facing descriptions. Reword so the readme
reads as an overview and the OpenAPI schema remains the technical
reference:

  - Drop CurrencyModifierQuery / TransactionReasons references
    from tech.nodes, kc.facilityLevels, kc.savedShips — say
    "nominal and strategy-modified" instead.
  - Replace AlwaysEvaluable phrasing with "Callable from any
    game scene" in the tech.* section blurb.
  - Trim kc.crewRoster row from a field list to a summary.
  - Promote strategies.* to its own section with a one-line note
    on the in-any-scene activate / deactivate path.
  - Split the trailing WIP sci.* / career.* / comm.* table into
    a dedicated career.* and comm.* section (now both
    production-ready), drop the duplicate sci.* rows that
    overlapped the existing sci.* section, and remove the WIP tag.
  - Move f.ag.bindings into the f.* action-groups table rather
    than its own one-line subsection.
The trailing "…" suggested an open-ended enumeration but the four
scene names listed are the complete set. Use an explicit "or"
instead so readers don't go hunting for the missing ones.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant