BeamNG.Earth
A 1:1 scale streamed Earth for BeamNG.drive: real elevation and satellite imagery, floating origin, LOD streaming from a companion server
A Kerbal Space Program add-on, a website and a Discord bot that let players hire each other. Write a contract, fly it, hand the craft over, collect the reward. Contracts, auctions, a craft marketplace
GamesC# · PythonGitHub @Boundless-MissionsOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
A Kerbal Space Program add-on, a website and a Discord bot that let players hire each other. Write a contract, fly it, hand the craft over, collect the reward. Contracts, auctions, a craft marketplace and weekly missions, reachable from the game, from this site and from Discord.
Taken from the repository README
Loading the full README. The saved excerpt is shown below.
A KSP 1.12.x plugin that connects a player's game to the Boundless Missions
backend: player-issued contracts, reverse auctions, rescue missions, craft and
vessel transfers between players, real-time notifications, cinematic milestone
captures, and automated mod-dependency management, all from inside the stock
game.Read the full README ↗A KSP 1.12.x plugin that connects a player's game to the Boundless Missions backend: player-issued contracts, reverse auctions, rescue missions, craft and vessel transfers between players, real-time notifications, cinematic milestone captures, and automated mod-dependency management, all from inside the stock game.
The hard part is not the networking. It is that two players never have the same install: different mods, different part sets, different life-support rules, different rendering stacks. Most of this codebase exists so a craft built on one machine arrives intact, or degrades honestly, on another.
SurfacePlacementRescueWaypointsStreamerModeWeb/┌─────────────────────────────────────────────────────────────────┐
│ KSP Game Process (Unity 2019.4 / Mono / .NET 4.7.2) │
│ │
│ ┌─────────────────────┐ ┌────────────────────┐ │
│ │ GeneKermanMod │◄──►│ ApiClient │──► HTTP/S │
│ │ (MonoBehaviour │ │ (UnityWebRequest) │ REST API │
│ │ Singleton) │ └────────────────────┘ │
│ │ │ ┌────────────────────┐ │
│ │ • Lifecycle mgmt │◄──►│ NotificationSocket│──► WebSocket │
│ │ • Coroutine host │ │ (websocket-sharp) │ │
│ │ • ClientState │ └────────────────────┘ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌──────────▼──────────────┐ ┌─────────────────────────────┐ │
│ │ UI │ │ Web/ (loopback bridge) │ │
│ │ uGUI sidebar (primary) │ │ 127.0.0.1:<ephemeral> │──►│─► browser
│ │ IMGUI gates (consent, │ │ static WebUI + /gk + proxy │ │
│ │ update, suspension, …) │ │ (opt-in, off by default) │ │
│ └──────────┬──────────────┘ └─────────────────────────────┘ │
│ │ │
│ ┌──────────▼───────────────────────────────────────────────┐ │
│ │ Transfer Pipeline (export order = strip order reversed) │ │
│ │ ScaleBridge(bake) ─► FlagTransfer ─► TweakScaleGuard │ │
│ │ ─► TextureTransfer ─► RealFuelsTransfer │ │
│ │ ─► CkanGenerator ─► CraftThumb │ │
│ │ CraftInstaller ─► PartAliases ─► reconcile ─► Ships/ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────▼───────────────────────────────────────────────┐ │
│ │ Visual │ │
│ │ VesselRenderer (blueprints, layer 30, dual-pass alpha) │ │
│ │ DecalCapture (ConformalDecals re-issue) │ │
│ │ CinematicCapture (in-game hero shots, flight camera) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────▼───────────────────────────────────────────────┐ │
│ │ Contract System │ │
│ │ ContractIntegration ◄─► ContractConstraints │ │
│ │ EditorPartEnforcer ◄─► PartClassifier ◄─► CraftDeltaV │ │
│ │ OrbitConstraint ◄─► SubmissionSession │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────▼───────────────────────────────────────────────┐ │
│ │ Rescue & Life Support │ │
│ │ RescueImmunityGuardian ◄─► LifeSupport/ adapters │ │
│ │ TraitRepair (roster repair) │ │
│ │ CheckpointDetector (proximity/SOI/event scanning) │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
The mod is a single Unity MonoBehaviour (GeneKermanMod) that attaches to a
persistent GameObject. It communicates with a remote server over HTTP REST
(via ApiClient) and WebSocket (via NotificationSocket). All networking uses
Unity's UnityWebRequest for HTTP and the external websocket-sharp library
for WebSocket.
Nothing is transmitted before consent. Consent (KSP add-on rule 8.1) gates
the whole surface: until the player accepts in ConsentWindow,
ApiClient.TransmissionBlocked is true and no request leaves the process.
KSP Mod Side/
├── build.sh # Build + deploy script (3 dev instances)
├── BoundlessMissions.netkan # CKAN metadata
├── PACKAGING.md # Release/zip procedure
├── logo.png / logo_38.png # Toolbar icons
├── Iconpack-1/ # UI icon assets
├── dist/ # Packaged release zips
├── LICENSE # GPL-3.0
├── GeneKerman/ # C# source (the plugin project)
│ ├── GeneKerman.csproj # .NET 4.7.2 project file
│ ├── lib/ # Pre-built dependency (websocket-sharp.dll)
│ ├── LifeSupport/ # Reflection-only adapters, one per LS mod
│ ├── Web/ # Loopback HTTP bridge for the browser UI
│ ├── UI/ # IMGUI windows (gates only (see UI System))
│ │ └── Gui/ # uGUI sidebar: theme, builder, panels, windows
│ │ └── Panels/ # The screens (missions, contracts, tools, …)
│ ├── *.cs # Core systems (see Source File Map)
│ ├── bin/ # Build output (GeneKerman.dll)
│ └── obj/ # Intermediate build files
│
├── WebUI/ # Browser-UI source (React + Vite)
│ ├── vite.config.ts # Builds into GameData/…/WebUI/; stamps the
│ │ # manifest and THIRD-PARTY-NOTICES.txt
│ ├── src/ # Screens and components (the .tsx sources)
│ └── package.json # Runtime deps: React, lucide, clsx, cva, …
│
└── GameData/BoundlessMissions/ # Deployable mod folder (copied into KSP)
├── GeneKerman.version # AVC version file (KSP 1.12.x)
├── Patches/
│ └── GeneKermanScale.cfg # ModuleManager patch: @PART[*]:FINAL
├── Plugins/ # GeneKerman.dll + websocket-sharp.dll
├── PluginData/ # Runtime data (see Configuration)
├── Textures/ # Toolbar icons, UI icons
└── WebUI/ # Built browser-UI bundle served by Web/
# (+ THIRD-PARTY-NOTICES.txt, generated)
A second GameData folder, GameData/GeneKerman/Flags/, is created at runtime by
FlagTransfer to hold content-addressed flag images that arrive with crafts.
The source project is named
GeneKerman; the deployed GameData folder isBoundlessMissions. This is intentional: do not "fix" one to match the other.
Line counts are approximate and drift with every commit; they are here to show relative weight, not as a contract.
| File | Lines | Role |
|---|---|---|
GeneKermanMod.cs | 2,047 | Entry point. MonoBehaviour singleton, lifecycle, state machine, toolbar, coroutine host, notification dispatch, local notifications |
ClientState.cs | 936 | The account, headless. Profile, missions, contracts and the notification feed: fetch, cache, local-notification merge, de-dup, unread count, every action coroutine. Exposed by reference via GeneKermanMod.State |
ApiClient.cs | 1,831 | HTTP networking. All REST calls, settings/session persistence, version gating (426), device binding (403), suspension (403 suspended) |
NotificationSocket.cs | 451 | WebSocket push. Real-time notifications, ticket auth, keepalive, exponential backoff reconnect |
Consent.cs | 163 | Privacy gate. First-run opt-in record, server-driven policy version, live re-read on file change |
MiniJSON.cs | 356 | JSON library. Lightweight serializer/deserializer, no external dependencies |
| File | Lines | Role |
|---|---|---|
VesselTransfer.cs | 1,541 | Vessel serialization. Export/import live vessels via ConfigNode, crew embedding (GKCREW), trait application, fleet transfers, ownership tags, save removal + CrewFate |
CraftInstaller.cs | 336 | Craft file writer. Strips side-channel blocks in reverse order, reconciles, places craft in `Ships/VAB |
CraftDelivery.cs | 148 | Delivery coroutine. Fetch + install the craft a completed contract earned, with one completion callback |
GiftInbox.cs | 201 | Quicksend receiver. Poll/accept/decline craft offers from other players |
FlagTransfer.cs | 674 | GKFLAG. Embed/extract custom flag images, content-addressed SHA-256 naming, runtime GameDatabase injection, dangling-reference reset |
ScaleBridge.cs | 466 | TweakScale bake. Copies computed scale/mass/module stats off live parts, strips TweakScale, re-anchors pos, writes the layout pin |
GeneKermanScale.cs | 379 | Scale applicator. Dormant PartModule on every prefab; re-applies absolute values without TweakScale's exponent math |
TweakScaleGuard.cs | 283 | GKTSVER. Version warning: fires only when a part is actually rescaled, never on the mere presence of a TweakScale module |
ScaleEditorReapply.cs | 68 | Editor undo/redo fix. Re-asserts scaled geometry after the editor rebuilds the part tree |
TextureTransfer.cs | 884 | GKTU. Carries a Textures Unlimited paint job: resolves texture sets → defining GameData folder, reconciles recolour modules per prefab on import |
| File | Lines | Role |
|---|---|---|
SubmissionSession.cs | 1,285 | Submission rules + upload. Mission-type classification rules, scene/vessel validation, render capture and freshness, the packing coroutine. No UI |
SubmissionPreview.cs | 168 | Review images. Fetches the blueprints/telemetry a contractor submitted, one completion callback |
ContractCreation.cs | 606 | Issuing work. Direct contract, reverse auction, or rescue: shared by the sidebar form and the web bridge |
ContractInbox.cs | 115 | Local inbox state. The client-side trash bin and week grouping (never sent to the server) |
ContractIntegration.cs | 508 | Stock contract bridge. Injects API missions as stock contracts in Mission Control |
ContractConstraints.cs | 454 | Mission limits. Forbidden/required parts, propellants, categories, Δv, crew count, crew professions (TraitMods) |
OrbitConstraint.cs | 202 | Orbital regime. Parses the orbit sub-object (polar, equatorial, keostationary, Molniya, …) and gates the submit button |
PartClassifier.cs | 189 | Part analysis. Derives propellants, engine categories, part categories from live modules |
EditorPartEnforcer.cs | 136 | Editor filter. Hides forbidden parts in the VAB/SPH palette during an active contract |
CraftDeltaV.cs | 55 | Δv reader. Reads stock VesselDeltaV for mission limit validation |
| File | Lines | Role |
|---|---|---|
VesselRenderer.cs | 1,258 | Blueprint rendering. 8-view capture, dual-pass alpha, layer 30 isolation, NW thumbnails, Deferred-path support |
DecalCapture.cs | 277 | ConformalDecals. Re-issues the decal draw on the isolation layer so decals appear in blueprints; renders never-drawn text decals |
CinematicCapture.cs | 283 | Hero shots. Sunlit camera pose computation, HUD toggle, ScaledSpace sync |
CheckpointDetector.cs | 347 | Milestone detection. Rendezvous, flyby, asteroid, EVA, staging, orbit, landing, splashdown |
| File | Lines | Role |
|---|---|---|
RescueImmunityGuardian.cs | 672 | Emergency freeze. Lifts stranded rescue crew out of the simulation, parks them, thaws at 10 km or on demand |
LifeSupport/LifeSupportRegistry.cs | 87 | Which LS mods are installed here, and which one this install actually runs |
LifeSupport/LifeSupportScan.cs | 118 | What a craft is provisioned for, and for how long (the marketplace/contract LS flag) |
LifeSupport/LsFreeze.cs | 95 | Tells each installed LS mod to let go of frozen crew, and hands them back with a clean slate |
LifeSupport/LsRations.cs | 188 | Stows a few days of the rescuer's resources aboard the wreck |
LifeSupport/LsReflect.cs | 244 | Defensive reflection helpers shared by every adapter |
LifeSupport/LsAdapterBase.csILifeSupportAdapter.csLsEndurance.cs | 85 / 65 / 26 | Adapter contract, shared base, endurance result type |
LifeSupport/UsiLsAdapter.csTacLsAdapter.csSnacksAdapter.csKerbalismAdapter.csDeepFreezeAdapter.cs | 75 / 86 / 54 / 150 / 45 | One adapter per optional LS mod, reflection-only |
TraitRepair.cs | 480 | Roster repair. Reversibly replaces a profession no installed mod defines; restores it when the mod returns |
LocalNotifActions.cs | 114 | Action keys carried in a local notification's data, rendered as a button by both front ends |
| File | Lines | Role |
|---|---|---|
DeviceId.cs | 172 | Stable per-install GUID (no hardware data read), capped KSP.log reader for reports |
ModVersion.cs | 93 | DLL SHA-256 hash, challenge-response attestation |
PartCatalogUploader.cs | 92 | Uploads the installed part list, FNV-1a hash gate |
PhysicsRangeManager.cs | 216 | Temporarily disables Physics Range Extender during submissions |
ClickThroughHelper.cs | 114 | Routes IMGUI windows through Click Through Blocker when available |
ToolActions.cs | 690 | Tools-tab operations: flag import, craft export, quicksend, bug report, shared by the sidebar and the web bridge |
Favorites.cs | 108 | Starred players for the quicksend picker (PluginData, not localStorage: the bridge origin changes every launch) |
CheatDetection.cs | 980 | Cheat watchdog. Taints a vessel whose state breaks physics continuity, catching teleports by effect rather than by tool |
DebugMenuCheatWarning.cs | 311 | Writes the disqualification warning into KSP's own F12 cheat screens, and hooks their three teleport buttons as a second channel |
SimulationDetection.cs | 179 | Probes RP-1/KCT and KRASH "simulation active" flags by reflection, so a simulated flight cannot be submitted as a real one |
StreamerMode.cs | 332 | Hides other players' avatars and usernames when broadcasting software is detected or the player asks. Scans nothing while switched off |
SecureFile.cs | 105 | Restrictive-permission writes for the files under PluginData that hold credentials |
TextSanitizer.cs | 94 | Strips characters the game fonts render as tofu from server and local notification text |
Web/)| File | Lines | Role |
|---|---|---|
Web/LocalServer.cs | 363 | Loopback HTTP listener: static bundle, /gk/* game bridge, /api/* proxy, /gk/events SSE |
Web/BridgeAuth.cs | 158 | Launch nonce, session cookie + CSRF token: layers 3 and 4 of the five-layer gate |
Web/ApiProxy.cs | 315 | Forwards /api/* upstream with the token attached in C#. The allow-list is the security boundary |
Web/GkRoutes.cs | 828 | Everything only this process can do: game state, craft actions, contract creation |
Web/EventStream.cs | 152 | SSE push, tee'd from the notification socket |
Web/StaticFiles.cs | 200 | Serves GameData/BoundlessMissions/WebUI/ |
Web/MainThreadQueue.cs | 180 | Marshals anything touching KSP back onto the Unity main thread |
Web/JobRegistry.cs | 96 | Long-running job results, readable across a page reload |
| File | Lines | Role |
|---|---|---|
UI/Gui/SidebarController.cs | 1,025 | Owns the Canvas, the expand animation, input locks, font recovery |
UI/Gui/UIF.cs | 1,207 | Fluent widget builder; font/material re-assertion (RefreshFont, RefreshText) |
UI/Gui/Theme.cs | 380 | Design tokens ported from the website's globals.css; the normalized font material |
UI/Gui/Sprites.cs | 749 | Procedural 9-slice rounded-rect sprites (there is no Unity Editor on this machine) |
UI/Gui/SidebarPanel.cs | 201 | Panel base: WorksOffline, show/hide, refresh contract |
UI/Gui/FloatWindow.cs | 284 | Draggable, clamped window shell (used by the submission screen) |
UI/Gui/Panels/ContractsPanel.cs | 1,637 | Contract inbox + active contract actions (incl. rescue wreck spawn) |
UI/Gui/Panels/SubmitPanel.cs | 397 | The submission screen: a pure read of SubmissionSession |
UI/Gui/Panels/ToolsPanel.cs | 441 | Export, flag import, quicksend, bug report, roster repair card |
UI/Gui/Panels/NotificationsPanel.cs | 451 | The feed, with local-notification action buttons |
UI/Gui/Panels/SettingsPanel.cs | 323 | Server choice, feature toggles, interface switch |
UI/Gui/Panels/MissionsPanel.csProfilePanel.csMarketPanel.cs | 172 / 209 / 194 | Weekly missions, profile, marketplace selling half |
UI/Gui/Panels/FinancePanel.cs | 846 | The wallet's history: summary, a 14-day diverging graph, the transaction list, lifetime per-category totals and Send coins |
GeneKermanModGeneKermanMod is the mod's entry point. It is a Unity MonoBehaviour marked
with [KSPAddon(KSPAddon.Startup.MainMenu, true)] and DontDestroyOnLoad,
making it a persistent singleton that survives scene changes.
State machine: Unlinked → Linking → Linked.
Responsibilities:
ApiClient, NotificationSocket,
CheckpointDetector, ClientState and the uGUI SidebarController. Loads
PluginData/settings.cfg and PluginData/consent.cfg. Registers the KSP
Application Launcher toolbar button.Update()): Ticks the notification socket, drains queued
WebSocket notifications, drains Web.MainThreadQueue (anything the loopback
bridge needs done on the main thread), polls the API on a timer fallback when
the socket is down, ticks the checkpoint detector during flight, and re-checks
an active suspension against the clock.GameEvents.onGameSceneLoadRequested to reset
state, re-register event hooks, and adjust UI visibility across Space Center,
Flight, Editor, and Tracking Station scenes.RunCoroutine() so non-MonoBehaviour classes
(like ApiClient) can start Unity coroutines.RaiseLocalNotification() raises a client-side
notice about something this install found (a broken roster trait, a missing
mod, a substituted part). A local notification may carry an action key in its
data dict (see LocalNotifActions).OnGUI draws whichever IMGUI gate applies (consent,
update required, suspension, data paused, device verification, link) and
suppresses the canvas beneath it.Key state:
Instance : static singleton reference
Api : the networking client
State : ClientState, the account, by reference
Socket : the WebSocket handler
Detector : the checkpoint detector
PluginDataPath : GameData/BoundlessMissions/PluginData (runtime storage root)
ClientStateClientState is the account as the client knows it: profile, weekly missions,
contracts (incoming and active), and the notification feed, plus the fetch
coroutines, the merge of server notifications with local ones, the de-dup, the
unread count, and every action (RequestMarkRead, RequestDismiss, accept,
cancel, dispute, …).
It is deliberately headless and deliberately shared by reference: the uGUI
sidebar, the browser bridge (Web/GkRoutes) and the notification socket all
read the same object. A second copy drifts the moment either side gains a
mutation, and the unread badge is the first thing to show it.
ApiClientApiClient encapsulates all HTTP communication with the server. Every API call
is a Unity coroutine using UnityWebRequest, the only HTTP client available in
KSP's Mono runtime.
Request flow:
Authorization: Bearer <token>,
X-Device-Id: <deviceId>, X-Mod-Version: <version>,
X-Mod-Hash: <sha256>.TransmissionBlocked short-circuits every call until the
player has accepted the current policy version. Nothing leaves the process
before that.UpdateRequiredWindow and blocks further API calls until the
player updates. The NotificationSocket also triggers a re-check when it
receives a "version" frame. Acknowledging the gate drops the client into
limited mode, only panels declaring WorksOffline (Tools, Settings)
render, behind a banner offering Re-check / Download latest.device_verify): The player's device hasn't
been approved yet: DeviceVerifyWindow is shown with instructions to
approve from Discord.suspended): routed to
GeneKermanMod.OnSuspended → SuspendedWindow. The session token is
deliberately not revoked, so every subsequent request keeps returning the
explanation instead of dropping the player to a link screen.Persistence (all under PluginData/):
session.token: the current 30-day signed session tokensessions.cfg: known sessionssettings.cfg: user configuration (see Configuration)Note that settings.cfg stores the server as separate serverProtocol /
serverHost / serverPort values, never as a URL: // is a comment delimiter
in ConfigNode and would truncate the value. The marketplace URL is split the
same way.
Key methods:
Get(path, callback) / Post(path, json, callback): generic REST callsUploadVessel(...): multipart form upload (vessel node + blueprint PNG +
craft file + screenshot + metadata JSON)DownloadCraft(url, callback): downloads a .craft fileGetWsTicket(callback): obtains a single-use WebSocket auth ticketChallenge-response attestation:
The server can send a challenge (nonce + offset + length). ModVersion.AttestDigest
hashes SHA256(nonce || dll_bytes[offset..offset+length]) and returns the
digest, proving the player is running an unmodified official DLL.
NotificationSocketNotificationSocket maintains a persistent WebSocket connection for
server-pushed events (contract updates, vessel deliveries, version pokes).
Design constraints:
ClientWebSocket, so the mod bundles
websocket-sharp.dll.ConcurrentQueue<Dictionary<string,object>>
and drained on the Unity main thread by GeneKermanMod.Update().Connection lifecycle (all driven by Tick() on the main thread):
Connect() enables the socket; Tick() calls OpenSocket() on the next
frame.TicketProvider (an
ApiClient.GetWsTicket coroutine) so the long-lived auth token never
appears in the WebSocket URL.wss:// with cert validation disabled (KSP ships an outdated
cert store).pendingRetry / ScheduleRetry().{"type":"ping"}) is sent via SendAsync to
keep NAT mappings open and detect silently-dropped connections.ConsumeJustConnected() returns true after a reconnect, the host runs
a catch-up notification poll to recover messages lost while disconnected.Frame types:
{"type":"version"} → sets versionPoke = true → host re-checks mod version{"notification":{...}} → queued for main-thread processingVessel transfer is the mod's most complex subsystem. There are two transfer
paths: live vessel (in-flight ProtoVessel serialization) and craft
file (.craft blueprint from the editor or on disk), each with its own
export/import chain.
VesselTransferVesselTransfer handles the core serialization of vessels and crafts using
KSP's ConfigNode system.
Export (sending a live vessel):
Vessel.BackupVessel() → ProtoVessel → protoVessel.Save(node) →
ConfigNodeGKCREW child nodes (name, trait, level,
experience, gender) so the receiver can reconstruct the exact crew.FlagTransfer.EmbedFlagsInNode() embeds custom flag images.CkanGenerator.EmbedModsInNode() embeds the mod dependency manifest.TextureTransfer.EmbedInNode() embeds the Textures Unlimited manifest.RealFuelsTransfer.EmbedInNode() embeds the RealFuels manifest.ScaleBridge.SnapshotIntoVesselNode() bakes the TweakScale-computed values.ConfigNode is serialized to text for upload.Import (receiving a vessel):
ConfigNode.Load() using a temporary file on disk
(because ConfigNode.Parse() is unreliable in KSP's Mono runtime).FlagTransfer.ExtractAndInstallFlags() installs flag images and strips
GKFLAG nodes.CkanGenerator.ExtractCheckAndStripMods() strips and processes GKMODS.PartAliases.ApplyToVesselNode() substitutes missing parts for the local
equivalents, before the two reconcile passes, because a substituted part
is a different prefab with different modules.TextureTransfer.ExtractCheckAndStripFromNode() checks or reconciles the
paint job.RealFuelsTransfer.ExtractCheckAndStripFromNode() checks or reconciles the
fuel/engine configuration.ScaleBridge.NeutralizeTweakScaleForImport() removes TweakScale MODULE
nodes from baked parts (so GeneKermanScale is the sole authority).GKCREW nodes is applied to the ProtoCrewMember roster via
ApplyTrait, which refuses to create a profession this install cannot
define; downgrades are reported once and recorded for TraitRepair.FlightGlobals.Vessels.Add() / ProtoVessel.Load().Export (sending a .craft file): every export path (contract submission
×2, quicksend ×2, marketplace listing, export-to-file ×2, and the
blueprint attached to a vessel transfer) runs the same chain, in this
order:
bake (ScaleBridge) → GKFLAG → GKTSVER → GKTU → GKRF → GKMODS → GKTHUMB
Baking is not optional on any of them. A blueprint has no import-side scale
step, so a craft that leaves unbaked cannot be repaired on arrival. Baking
fixes three things at once: the scale itself, the pos re-anchor (KSP
serialises a surface-attached part on a scaled parent with a KSP-Recall-dependent
encoding), and the root-local layout pin that GeneKermanScale re-asserts at
runtime.
FlagTransfer.EmbedFlagsInCraft() and every block after it append raw text
and never re-serialize the craft body via ConfigNode.ToString(), which would
wrap it in a spurious root {} node that KSP's craft loader rejects.
Import (receiving a .craft file): handled by CraftInstaller.Install() in
strict reverse-append order (see below).
Ownership and removal: transferred kerbals are tagged "{owner}'s {name}"
while they live in someone else's save (ApplyOwnershipTag, reversible), which
makes an untagged roster name the test for "this one is mine".
RemoveVesselFromSave leans on that: giving a craft up calls Vessel.Die(),
which kills whoever is aboard, so every removal also decides a CrewFate:
LeavesWithCraft for the issuer of a rescue, BorrowedOnly for its rescuer.
The fate is chosen where the removal is queued (only that caller knows which
side this is) and rides the persisted queue, because the removal itself can run
sessions later. PurgeBorrowedGhostCrew sweeps borrowed kerbals left dead or
missing by a craft that vanished before its removal ran, not cosmetic, since
KSP counts them against the astronaut-complex hire limit and refuses any new
applicant name that is a substring of an existing roster name.
VesselDataCollectorVesselDataCollector captures vessel telemetry for submission metadata:
VesselSnapshot: vessel name, type, situation, body, lat/lon/alt, orbital
elements (SMA, ecc, inc, Ap, Pe, period), part count, mass, cost, crew countCaptureLoadedVessels(): snapshots all vessels in physics range
(excluding debris, space objects, flags)GetNearbyVessels(): returns live Vessel references for multi-vessel
submissionFindCraftFile(): searches saves/<save>/Ships/ and root Ships/ for a
craft by nameCaptureScreenshot(): saves a PNG via ScreenCapture.CaptureScreenshot()CraftInstallerCraftInstaller.Install() is the single entry point for writing a received
craft to disk:
0x1F 0x8B)GKTHUMB → GKMODS → GKRF → GKTU → GKTSVER → GKFLAGPartAliases.ApplyToCraft(): substitute missing partsTextureTransfer.ReconcileCraftBody(): keep the recolour modules the local
prefabs accept, drop the ones they can'tRealFuelsTransfer.ReconcileCraftBody(): check on an RF install, reconcile
to local fuels on one withouttype = VAB|SPH)saves/<save>/Ships/<type>/ with collision-avoidance numbering.loadmeta sidecar if providedCkanGenerator.OnCraftInstalled() for missing-mod detectionCraftThumb.InstallThumbnail() for the craft browserCritical invariant: The
.craftbody is never round-tripped throughConfigNode. Side-channel blocks are appended as raw text at the end and stripped as raw text from the end. This preserves the byte-for-byte integrity of the craft file, which KSP's craft loader is very strict about.
The mod carries auxiliary data alongside vessel/craft transfers using a system
of side-channel blocks, structured text nodes appended to or embedded
within KSP's ConfigNode serialization format.
For .craft files (raw text), blocks are appended at the end and
stripped in reverse order on import:
<original craft body> ← already baked by ScaleBridge
GKFLAG { ... } ← appended first, stripped last
GKTSVER { ... }
GKTU { ... }
GKRF { ... }
GKMODS { ... }
GKTHUMB { ... } ← appended last, stripped first
Order is load-bearing in both directions. Each stripper cuts from its own marker
to end of file, so stripping out of order takes later blocks with it. One useful
consequence: an older client receiving a craft that carries GKRF loses
nothing: its GKTU strip cuts to end of file and removes GKRF along the way.
For vessel ConfigNodes, blocks are embedded as child nodes (GKFLAG,
GKCREW, GKMODS, GKTU, GKRF) and removed after extraction.
Three of these blocks exist for the same reason: the mod adds no parts.
TweakScale, Textures Unlimited and RealFuels all configure existing parts, so
every mod-detection path in this codebase, all of which resolve parts →
GameData folder via AvailablePart.partUrl, is blind to them. The block is
what carries the fact a part name cannot express.
FlagTransferProblem: KSP stores flags as GameData-relative paths (e.g.
MyFlags/eagle). When a craft moves to another player who doesn't have that
image, KSP shows a missing decal.
Solution: Content-addressed flag embedding.
Export:
/).Squad/Flags/default, SquadExpansion/*)..png,
.dds, .jpg, .jpeg, .truecolor, .mbm, .tga).SHA-256(imageBytes) → new URL = GeneKerman/Flags/<hex>.+→-, /→_, no padding): standard
base64's / would be parsed as a comment delimiter by ConfigNode.GKFLAG nodes with url, ext, and data values.Import:
GameData/GeneKerman/Flags/<hash>.<ext>.GameDatabase at runtime (creates a Texture2D, loads image
via LoadImage(), adds a TextureInfo) so the flag renders immediately
without a game restart.Why content addressing, and why it is also the evidence. KSP names
player-imported flags with short random ids (Squad/Flags/UtB0nwS), so two
players' identically-named flags are different pictures. A hash can only be
computed from the bytes, and a URL is only rewritten for a flag whose bytes were
read, so a GeneKerman/Flags/<hash> URL in a craft is proof the sender held
the image. A reference that doesn't resolve on arrival was lost in transit,
never missing at export.
Dangling references are reset at both ends: Unresolvable on export, and
ResetDanglingFlagsInText / InNode on import (always after the carried flags
are installed), both back to Squad/Flags/default. Left alone the problem is
self-perpetuating rather than cosmetic: a re-export finds no file, embeds
nothing, and ships the same dead URL onward, while every module resolving it
errors. And not always in its own name: ModuleConformalFlag with
useCustomFlag = false renders the mission flag, so a broken mission flag
surfaces as a ConformalDecals exception mid-OnLoad (the stock flag decals only
warn).
A conformal decal carrying its own flagUrl transfers like any other reference:
CollectFlagUrls matches any flag-keyed value that looks like a path.
The URL-safe base64 is not a style choice. A raw
//pair makesConfigNodetreat the rest of the value as a comment and silently truncate the image, which is the bug that minted the dangling references still sitting in the test saves.
ScaleBridge / GeneKermanScale / TweakScaleGuardThe problem: TweakScale rescales parts using a factor + exponent table. Different TweakScale versions/forks produce different final values from the same factor. A player without TweakScale gets stock-sized parts. This makes transferred crafts unreliable.
The solution: Snapshot absolute, already-computed values on the sender and replay them on the receiver without any TweakScale dependency.
Three classes work together:
ScaleBridge (sender-side snapshot + receiver-side neutralization)On send (vessel): SnapshotIntoVesselNode(vessel, vesselNode)
model.localScale / prefab.localScale ≠ 1:
currentScale / defaultScale via reflection)part.massgkActive=True, gkLinear, gkMass, gkFields into the part's
GeneKermanScale MODULE nodeOn send (craft): SnapshotIntoCraftBytes(craftBytes, liveParts)
Part objects by craftIDpos values from live editor transforms to fix KSP-Recall-dependent
surface-attach encodinggkPin, gkPinPos) into each part so the
receiver can re-assert the correct positionOn receive: NeutralizeTweakScaleForImport(vesselNode)
gkActive=True, removes the TweakScale MODULE nodeGeneKermanScale (receiver-side PartModule)A PartModule added to every part prefab via the ModuleManager patch
@PART[*]:FINAL in Patches/GeneKermanScale.cfg. Dormant by default
(gkActive=false, gkLinear=1).
When a transferred craft arrives with snapshot data:
OnLoad: Applies model scale and attach-node offsets early (before KSP
builds physics joints)OnStart: Re-applies everything (model, nodes, mass, module stats, drag
cubes) and starts a 12-frame ReapplyGeometry() coroutine to win over KSP's
own surface-attach re-projectionApplyPin(): In editor scenes, forces the part's transform position to
root.position + root.rotation * gkPinPos, countering KSP's re-seatCurated module stats (the StatFields dictionary):
ModuleEngines / ModuleEnginesFX : maxThrust, minThrust
ModuleRCSFX / ModuleRCS : thrusterPower
ModuleReactionWheel : PitchTorque, YawTorque, RollTorque
Resources are NOT handled: maxAmount is already persistent in KSP's
serialization and reconstructs correctly without intervention.
ScaleEditorReapply (undo/redo fix)A KSPAddon(EditorAny) that listens for onEditorUndo / onEditorRedo and
calls RequestGeometryReassert() on every GeneKermanScale instance. This is
needed because undo/redo rebuilds the part tree (triggering surface-attach
re-projection) without re-running OnStart.
TweakScaleGuard (version warning)The backstop for a bake that bailed or threw, and the version-mismatch warning.
GKTSVER { ver = <version> } block, but only to crafts
where something is actually rescaled.The trigger is "is anything actually rescaled", never "does this craft mention TweakScale". TweakScale attaches its module to every compatible part whether or not you scale it, and
ScaleBridgeonly strips the ones it snapshots, so a fully baked craft still carries TweakScale modules (roughly two thirds of them, measured across real crafts). Matching the bare module name warned every recipient of every baked craft about a mismatch that could not affect them.The check compares each module's
currentScaleagainst itsdefaultScaleusing the same epsilon asScaleBridge, the two must agree, or the guard would fire on exactly the partsScaleBridgejudged unscaled and left behind. A module whose fields are absent or unparseable is treated as scaled: silence has to be earned.
AssemblyLoader.loadedAssemblies for the
Scale assembly (exact name match, avoids companions like
TweakScaleCompanion_* or Scale_Redist). Falls back to whichever assembly
defines TweakScale.TweakScale.TextureTransferCarries a craft's Textures Unlimited paint job across a transfer, and guarantees a TU-painted craft still loads for someone who hasn't got TU.
The recolour data itself needs no channel: TU keeps it in a KSPTextureSwitch
PartModule whose persistent fields (texture set name, packed colour channels)
are already written into the .craft/VESSEL node, so it has always ridden
along. What it lacks is two things a part name cannot express.
Which mod. TU adds zero parts, so the part walk cannot see it. The fix is to
resolve each referenced texture set back to the GameData folder of the config
that defines it (GameDatabase's KSP_TEXTURE_SET entries), knowable only
on the sender's machine, and carry that in the GKTU block. That block also
feeds CkanGenerator.ResolveMods, which turns a missing recolour pack into an
installable CKAN modpack, and ToolActions, which unions
TexturePackFoldersForCraft into the marketplace mod tags and sends
CraftHasCustomTextures as a separate custom_textures flag (the website's
"Modded Textures Available" tag). The flag is separate on purpose: a texture
set the sender cannot resolve contributes no folder while the paint job is
still on the craft.
A clean load without it. On import, ReconcileCraftBody keeps every
recolour module the local prefab accepts and drops the ones it can't, so the
craft arrives either fully painted or in stock colours, never with orphan
module nodes. The per-part prefab check is what catches the case a folder check
cannot: TU installed, but not the pack that patches this part. It is
deliberately not consulted when TU is absent entirely, since it answers
"leave it alone" for a part it can't find and a craft can arrive with missing
parts.
The texture files are never embedded: a set is DDS art belonging to the pack author, far too big for a craft transfer and not ours to redistribute. This is a manifest plus a guard, not a copy.
Gated by enableTextureTransfer (default on). TU's module and field names are
matched from a defensive list rather than a single literal, so an unrecognised
variant degrades to "carried but not understood" instead of to a broken craft.
Not to be confused with TUFX, which is scene-wide post-processing and carries nothing per-craft.
RealFuelsTransferCarries a craft's RealFuels / Realism Overhaul fuel-and-engine configuration, and guarantees an RF-configured craft still loads for someone without RealFuels.
Like TU, RF adds zero parts: it configures existing ones via ModuleFuelTanks
(tank type + TANK nodes), ModuleEngineConfigs (selected config) and its
ModuleEnginesRF engine replacement, and the config state already rides in the
craft's MODULE/RESOURCE nodes, so two RSS-RO players exchange working crafts
with no help at all.
What the GKRF block adds:
env)TANK_DEFINITION
declares it (the same GameDatabase lookup TextureTransfer does for
KSP_TEXTURE_SET)CONFIG into the part config and its origin folder is unrecoverable.
Those are checked recipient-side against the local post-patch PART config.On import with RF: checks, not changes. Undefined tank types, unavailable engine configs, and an RO ↔ non-RO environment mismatch are each reported once.
On import without RF: a reconcile. The RF module nodes and every part-level
RESOURCE naming a locally-undefined propellant are dropped, so parts refill
from their local prefabs and the craft arrives in local fuels instead of
half-loaded, with the caveat stated that the design was balanced for other
physics.
The reverse hazard needs no manifest: a craft with propulsion but no RF state arriving on an RO install is warned about locally, since RO's patches re-plumb it on load. Plain-RF installs stay quiet.
The generated CKAN modpack lists RealFuels and any missing tank packs but
never RO: RO is an environment, not a dependency, so like a DLC it is named
in the warning and kept out of the .ckan. Marketplace listings get the RF
folders (including RealismOverhaul as a visible tag) unioned into their mod
tags, exactly as TU packs are.
Gated by enableFuelConfigTransfer (default on); switched off it still scans
and warns but writes nothing: the PartAliases contract.
CkanGeneratorProblem: A .craft stores only part names, never which mod each part
came from. A recipient who's missing a mod just sees "this craft has missing
parts" with no way to know what to install.
Solution: Map parts to mods at export time and generate a CKAN metapackage on import.
A GameData folder is not a mod. The whole file is built around not confusing
the two (see A note on mod detection). CKAN's
registry.json is indexed by install path, and a part resolves through the
longest path prefix exactly one module owns; a prefix two modules share is left
out of the index so the walk drops past it rather than guessing.
Getting this wrong broke both directions at once. On export the arbitrary winner
was usually the companion module, which, having no parts, is the one useless
thing to hand CKAN. On import, a recipient holding only the companion read as
already having the mod, so no .ckan and no warning were produced at all.
Export:
AvailablePart.partUrl.CKAN/registry.json (cached for the session). A
sender without CKAN has nothing to resolve against and still reports the
bare folder.ModuleInventoryPart STOREDPART nodes, KIS
ModuleKISInventory ITEM nodes), because parts inside containers would
otherwise be missed.GKMODS block with MOD { folder, path, ckan, name } entries.
path is what carries the answer and what the recipient-side check tests; it
is absent from blocks written by older clients, so every read falls back to
folder.Import:
GameData/ directories to find missing mods..ckan metapackage file in
<KSP>/GeneKerman_MissingMods/<context>.ckan.<craft>.gkmods sidecar alongside the installed craft so the editor
can re-check later.Editor re-check: EditorCkanWatcher is a KSPAddon(EditorAny) that
listens for onEditorLoad. When a craft with a .gkmods sidecar is loaded, it
re-runs missing-mod detection and regenerates the CKAN metapackage if needed.
Also used for the marketplace. The same resolution names a part's mod on a
listing (MarketplaceModName → "DeepFreeze", not "REPOSoftTech"), but only when
the resolved root actually contains the part, so a plugin-only sibling rooted at
SomeMod/Plugins cannot lend its subfolder name to a part living under
SomeMod/Parts. ResolveMods is also the hook TextureTransfer and
RealFuelsTransfer use to add dependencies the part walk can never reach.
PartAliasesCkanGenerator answers "which mod is missing?". PartAliases answers the
narrower and more common question: "this exact part isn't here, but the same
thing is, under another name." It runs on both the .craft and VESSEL-node
import paths, and stays quiet when the recipient has the mod but not that part,
the gap CkanGenerator cannot see.
The motivating case is Making History's InflatableAirlock versus ReStock+'s
restock-airlock-1, the same object under two names, since ReStock retextures
the DLC part with the very asset ReStock+ builds its DLC-free stand-in from.
That is also how the table was derived, mechanically rather than by eye: two
parts are listed as the same thing only when ReStock's DLC patch and the ReStock+
stand-in resolve to the same ReStock/Assets/... model, which proves identical
geometry and attach nodes.
Shared art does not prove shared balance: ReStock+ reuses the Wolfhound's and
Skiff's bells for much smaller engines, so those live in a separate
LookAlikes list that is only ever reported, never substituted.
Substitution runs in both directions, because ReStock+ hides its stand-ins
(TechHidden + category = none) when the DLC is present, and a career save
treats a hidden part as unpurchased and blocks launch. So "usable" here means
loaded and not hidden.
Gated by enablePartSubstitution (default on, since it only engages on a craft
that would otherwise refuse to load). Switched off it still scans and reports,
just as advice rather than as changes made. Every swap plus anything still
missing is reported in one notification.
This file is the source of truth for the pairs. The bot's copy at
data/part_aliases.py, in the server repo, is generated from it by that repo'spython tools/gen_part_aliases.pyand must be regenerated when the table changes.
CraftThumbProblem: A freshly-installed .craft has no entry in KSP's thumbs/
folder, so the craft browser shows a green placeholder.
Solution: Render a thumbnail on the sender's side and carry it with the craft.
Export: EmbedThumbForCurrentCraft() calls
VesselRenderer.CaptureNWThumbnail() to render a northwest-perspective view,
then appends it as a GKTHUMB { data = <base64> } block.
Import: CraftInstaller strips the block, decodes the PNG, and writes it
to thumbs/<save>_<VAB|SPH>_<craftname>.png, the exact path KSP's craft
browser looks up.
VesselRendererVesselRenderer produces clean blueprint images of vessels, the primary visual
submitted with missions. It shoots 8 views of the craft (6 orthographic + 2
perspective) and composites them onto a blueprint sheet; the same machinery
renders the NW craft-browser thumbnail embedded in shared .craft files. A
capture that yields no vessel pixels falls back to a plain screenshot rather than
submit a blank image.
How it works:
Layer isolation (layer 30): All vessel parts are temporarily moved to Unity layer 30 (an unused layer). This isolates them from the rest of the scene (terrain, skybox, other vessels).
Temporary camera: A new Camera is created, set to orthographic
projection, culling everything except layer 30, aimed at the vessel's center
of mass.
Dual-pass rendering for clean alpha:
camera.backgroundColor = Color.black →
render → ReadPixels() → blackTexcamera.backgroundColor = Color.white →
render → ReadPixels() → whiteTexalpha = 1 - (white.r - black.r) (since rendered = vessel * a + bg * (1-a))
Final pixel = (black.r / alpha, black.g / alpha, black.b / alpha, alpha)Restore: Parts are moved back to their original layer, the temporary camera is destroyed.
NW Thumbnails: CaptureNWThumbnail() renders a smaller
northwest-perspective view (the angle KSP uses for craft browser thumbnails)
for the CraftThumb system.
Deferred (blackrack) replaces every part and suit shader game-wide and prepares only its own cameras. A third-party camera rendering those shaders on the forward path therefore draws nothing in the flight scene: the clears run, zero fragments land. (The editor happens to survive, which is what made this look scene-specific rather than shader-specific.)
When Deferred is installed, the capture camera renders on the deferred path
instead. That needs two workarounds, both handled inside VesselRenderer:
FAKE_ORTHO_FOV),
whose foreshortening error hides inside the framing PADDING.Without Deferred, the stock forward + MSAA path is untouched.
DecalCaptureLayer isolation only moves things that have a Renderer, and a conformal
decal has none. ModuleConformalDecal (and its ModuleConformalFlag /
ModuleConformalText subclasses) hooks Camera.onPreCull and Graphics.DrawMeshes
the target part's mesh with its projection material on a hardcoded layer 0.
Every decal (image, flag and text alike) was therefore culled out of the
blueprint and out of the shared-craft thumbnail, while KSP's own thumbnail camera
showed them.
DecalCapture re-issues that same draw on the isolation layer, for the capture
camera only, reading the module's already-computed mesh, material and property
block rather than re-deriving a projection. The mod's own layer-0 draw still
happens and is still culled, so nothing is drawn twice.
It also generates the texture of any text decal that has never rendered: a
text decal's lettering is a runtime-rendered texture, and the module's fields
carry the string while nothing is drawn until UpdateText has run once.
All reflection, like the LifeSupport adapters, no ConformalDecals reference in the build, and a no-op without the mod.
CinematicCaptureCinematicCapture produces "hero shots": real in-game screenshots at flight
milestones with the vessel, skybox, and objects of interest in frame.
Camera pose computation (ComputePose):
The camera is placed on the sunlit side of the vessel (determined by
finding the direction to Sun.Instance.sun) so the dominant light source
illuminates the faces pointing at the lens.
Three framing modes:
Execution (coroutine):
FlightCamera controller to prevent it from overwriting the
transform.ScaledSpace camera to the same scaled-space pose (prevents
duplicate planet rendering).UIMasterController.HideUI() + GameEvents.onHideUI.yield return WaitForEndOfFrame → ScreenCapture.CaptureScreenshot(path).ContractIntegrationGKContractScenario is a ScenarioModule registered for Space Center, Flight,
and Tracking Station. It bridges API contracts to KSP's stock contract system.
InjectContract(): Tracks a mapping from API contract ID → stock contract
GUID. The stock GKMissionContract appears in the Mission Control UI with
the mission description, payment, difficulty, and due date.CompleteContract(): When the API confirms completion, marks all
GKMissionParameters as complete.CancelContract(): Cancels the stock contract when the API contract is
cancelled/expired.HasImportedVessel() / MarkVesselImported()
prevent double-importing the same contract's vessel.Persistence uses OnLoad / OnSave with CONTRACT_MAPPINGS and
IMPORTED_VESSELS ConfigNodes, defensively guarded to never let an exception
escape into KSP's ScenarioRunner.
ContractConstraints / PartClassifierContractConstraints parses the constraints object from the API and enforces
part-usage rules:
| Constraint | Description |
|---|---|
forbidden_parts / required_parts | Part mentions (title substring match) |
forbidden_part_names / required_part_names | Exact internal part names (resolved by the bot) |
forbidden_propellants / required_propellants | Propellant resource names (e.g. SolidFuel) |
forbidden_engine_categories / required_engine_categories | Engine type tokens (solid, ion, nuclear, chemical, monoprop, rcs, electric) |
forbidden_part_categories / required_part_categories | Part category tokens (heatshield, parachute, solarpanel, wheel, rtg, ladder, engine, rcs, reactionwheel) |
max_parts / min_parts | Total part count limits |
max_dv / min_dv | Vacuum delta-v limits (m/s), with 0.5% tolerance |
min_crew / max_crew | Crew count limits |
crew_traits | Required or forbidden crew professions, matched on the exact ProtoCrewMember.trait string |
Two enforcement modes:
IsForbidden(AvailablePart): Per-part check, used by EditorPartEnforcer
to hide forbidden parts in the VAB/SPH editor.CheckCraft(IEnumerable<Part>, deltaVVac): Whole-craft validation at
submit time. Returns a list of human-readable violation strings.Crew professions are matched by the exact ProtoCrewMember.trait string on
both ends, which is what lets a contract written on a modded install still mean
something on one without it. Which mod defines a modded profession is the one
thing that string cannot express and no part walk can recover, so it is written
down twice and kept in sync by comment: ContractConstraints.TraitMods here and
data/mission_constraints.py::_TRAIT_MODS on the bot (the same convention as
PartClassifier.GetEngineCategories ↔ ENGINE_CATEGORIES).
Both tables are closed: an unlisted profession yields no mod name rather than a guessed one. And only a floor names its mod: a ceiling ("no Kolonists") is satisfied by not having the mod, so naming it would read as advice to install something in order to obey a ban.
PartClassifier derives the semantic facts from live Part objects:
ModuleEngines / ModuleEnginesFX for propellants and throttle-lock
(solid rocket detection)ModuleAblator → heatshield,
ModuleParachute → parachute)EditorPartEnforcerA KSPAddon(EditorAny) that registers an EditorPartListFilter to hide parts
from the VAB/SPH palette during an active contract:
partUrl folder isn't in the allowed
mod list are hidden. Supports exclusion paths (-Squad/Expansions to block
DLC).ContractConstraints.IsForbidden()
are hidden.CraftDeltaVCraftDeltaV.TotalVacuum() reads the stock VesselDeltaV.TotalDeltaVVac:
-1 when unavailable (Δv readout disabled, calc not ready), and
callers skip the check rather than failingOrbitConstraintA contract's orbital-regime requirement, parsed from the orbit sub-object the
bot attaches to constraints when the mission text names a specific orbit
(polar, equatorial, keostationary, Molniya, …).
It drives the submit-button gate: a craft whose reported orbital elements don't
match is blocked before upload, and the bot re-checks authoritatively on
/submit. Schema and tolerances mirror data/orbit_constraints.py and the
ORBIT_* values in settings.py, both in the server repo.
Unlike part limits there is no editor enforcement, an orbit is a flight state, not a part choice.
SubmissionSessionSubmissionSession is the whole submission flow with the drawing taken out: the
classification rules a mission is submitted under, the scene/vessel validation
that enforces them client-side, the render capture and its freshness check, and
the coroutine that packs and uploads everything.
Mission types (AI-classified by the bot, cached server-side):
| Type | Must submit from | Sends |
|---|---|---|
craft_build | VAB / SPH | craft file + blueprint render / screenshot |
active_vessel | Flight | craft + .loadmeta + telemetry + renders |
flag_design | : | Discord only (there is no in-game flag upload) |
CollectUsedModFolders builds the dependency list that rides with the
submission. It deliberately omits TweakScale: a baked craft does not need it.
The screen itself is UI/Gui/Panels/SubmitPanel.cs, mounted in a draggable
FloatWindow rather than as a sidebar tab, submitting is read against the
scene behind it (the craft on the build stage, the navball in flight), and a
centred panel that owns the middle of the screen is the wrong shape for that.
The window pauses Physics Range Extender while it is up, which is why
WindowPanel.OnWindowClosed must never be skipped: the X, Escape and teardown
all close it behind the panel's back.
LifeSupport/All of this is reflection-only: the build references no life-support mod, and every call is a safe no-op when the target isn't installed.
| Piece | Role |
|---|---|
LsAdapterBase + ILifeSupportAdapter | The contract every adapter implements |
UsiLsAdapter, TacLsAdapter, SnacksAdapter, KerbalismAdapter, DeepFreezeAdapter | One adapter per optional mod |
LsReflect | Defensive reflection helpers shared by all of them |
LifeSupportRegistry | Which LS mods are installed here, and which one this install runs |
LifeSupportScan | Which mod a craft is provisioned for and for how long: the flag shown on marketplace listings and contract embeds |
"Provisioned for" means which installed consumption mod's resources the craft
actually carries (Supplies → USI, Food/Water/Oxygen → TAC or Kerbalism,
Snacks → Snacks). A craft with no LS resources is tagged none. Endurance is
reported per kerbal; the display side derives the range for
1..CrewCapacity.
Rates are declared once per adapter (DailyNeedPerKerbal) and feed both the
endurance display and the ration kit. Kerbalism's are read from its live
Profile.rules rather than guessed.
RescueImmunityGuardianThis is what makes a rescue work between two players on different life-support mods. It is three things, and all three have to hold:
RescueImmunityGuardian lifts the stranded crew out of the simulation
entirely, each kerbal is removed from the wreck (remembering their part and
seat) and parked at rosterStatus = Dead so KSP's respawn timer cannot revive
them behind our back. Nothing consumes a kerbal that isn't aboard a vessel,
so this holds uniformly across USI-LS, TAC-LS, Snacks and Kerbalism with no
per-mod hacks. It is also the only part that works under Kerbalism's
background simulation.LsFreeze tells each installed mod to let go of them and, on thaw, hands
them back with a clean slate. USI, TAC and Snacks reconstruct hunger from a
stored "last fed" timestamp and would otherwise kill a kerbal the instant it
is thawed after 200 days frozen.LsRations stows a few days of the rescuer's resources aboard, since a
wreck built for TAC carries nothing a USI save recognises.
(emergencyRationDays, default 3.)Crew thaw automatically at 10 km, outside load range, so the wreck is still unloaded and KSP seats them on load, or from a button.
A thaw is two releases, not one, because the freeze imposes two states: the
LS mods have to let go (LsFreeze.Thaw) and so does the roster
(ReleaseParked). Every path that drops a freeze record must therefore thaw
first, including the path where the wreck is already gone, since Kerbalism's
disabled flag is saved and a kerbal frozen but never thawed would be exempt
from life support forever. Any path that drops a record without seating the crew
must release them too: ours back to Available, borrowed ones out of the roster.
Left parked they are KIA for the rest of the save, which the Astronaut Complex
shows as kerbals simply missing from the Available tab.
Gated by enableEmergencyFreeze (default on).
TraitRepairRepairs a save whose roster holds a kerbal with a profession no installed mod
defines: the state VesselTransfer.ApplyTrait refuses to create, arrived at
by uninstalling a mod between sessions.
It is not cosmetic. A trait string is just a name; KSP resolves it to an
EXPERIENCE_TRAIT config on demand, and when nothing matches, pcm.trait keeps
the unresolvable name while pcm.experienceTrait stays null. Every stock screen
built out of CrewListItem (Astronaut Complex, crew assignment) reads
experienceTrait.Title and NullRefs part-way through drawing the list, taking
out the rest of the list, the other tabs, and any chance of telling which kerbal
caused it.
Three things make the repair safe:
BrokenCrew, formatted by VesselTransfer.FindUnresolvableTraitCrew)
stays read-only, because the trait string is the only record of the profession.TraitRepair.Repair runs only from a button, and copies the original into
PluginData/trait_repairs.cfg before overwriting it.RestoreRecovered hands the profession back by itself once the defining mod is
installed again, which makes the repair a loan, not a deletion.Records are keyed by save folder as well as kerbal name, and are dropped only
once the profession is safely back on the kerbal (or the player has moved that
kerbal on themselves). The import path (RememberDowngrades) and the repair path
deliberately share one record file, since they are the same loss.
Two tables must not be confused: ContractConstraints.TraitMods is a fact
(which mod owns a profession), while TraitRepair.StockEquivalent is a
usability guess (which stock job is closest), the same separation
PartAliases draws between substitutions and LookAlikes. Deciding what to
write asks CanDefine (strict); deciding what is broken asks IsDefined
(lenient, so an unready GameDatabase never reads as a roster full of broken
kerbals).
The button is reachable two ways, via LocalNotifActions: as a button on the
local notification (rendered and dispatched by NotificationsPanel without
either front end knowing what the action does), and as a card in the sidebar's
Tools panel, above the link gate and only while something is broken, so the fix
survives dismissing the notification.
CheatDetectionA teleported vessel really is at the target, and its numbers are perfectly consistent, so a server-side telemetry check cannot see it. Only the client is present when it happens, so detection lives here.
CheatWatchdog taints a vessel whose state breaks physics continuity. On rails
the orbital elements are mathematically constant; off rails the position must
extrapolate from last tick's position and velocity (in body-relative vectors, so
floating-origin and Krakensbane shifts cancel); and a change of main body must
happen at the SOI boundary. That catches F12 Set Position, Set Orbit and
HyperEdit by effect, without knowing which tool did it. Two channels
supplement it, because some cheats look physical: the stock CheatOptions
toggles are read directly, and VesselMover's "currently moving" state is probed
by defensive reflection.
Every threshold prefers false negatives: grace ticks around every legitimate perturbation (scene load, dock/undock, staging, pack/unpack), a jump floor no thrust crosses in one tick, and the on-rails element rule switched off entirely under Principia and PersistentThrust, whose whole point is moving on-rails orbits.
Grace forgives derivatives only, never state. Stock Set Orbit and Set Position
teleport by way of the very pack/unpack events grace absorbs, so
CheckStateContinuity judges state on every tick including grace ones. The
invariant is position-via-the-orbit: propagate last tick's conic to now and
compare against where the current conic says the vessel is. That was chosen over
per-element checks after both of the latter's blind spots bit in testing.
Taint is per vessel (persistentId), persisted so a quickload to before the
cheat legitimately clears it, and it spreads via docking, undocking and EVA.
DebugMenuCheatWarning is the companion surface: it writes a disqualification
warning into the F12 screens' own error slot and hooks all three teleport
buttons as a second, direct channel.
Submissions always attach a cheat_report for exactly the submitted vessels. An
explicit clean report is distinct from an old client that never checked, and the
server treats only tainted as a verdict: absent reports pass, and
tools_installed (HyperEdit merely present) is reviewer context, never grounds,
because presence is not usage.
SimulationDetectionRP-1/KCT's "Simulate" and KRASH are the RSS/RO test-flight loop. The physics
inside a simulation is perfectly continuous, so the watchdog cannot see it and
only the simulation mod can say. Each mod's own "simulation active" flag is
probed by defensive reflection, the watchdog taints any vessel flown while one
runs, and SubmissionSession refuses a flight submit outright before upload.
Ending a simulation reverts the save, which rolls the taint store back with it,
so a simulation leaves no mark on the real career. Editor (craft_build) submits
are exempt, a blueprint having no flight to have simulated.
StreamerModeHides other players' Discord avatars and Boundless usernames in the player
pickers, either because the player asked (hidePlayerDetails) or because
broadcasting software is running (enableStreamerMode).
Detection copies Discord's approach, which is process-name matching against a
short list, because no OS reports that a window is being captured. Three scans,
tried in order and each only when the last found nothing: the managed process
list; a /proc scan, which is what makes it work under Proton, since scan 1
sees only the Wine prefix while a native Linux OBS lives outside it; and KSP's
own loaded modules, where OBS Game Capture's injected graphics-hook64.dll is
the one signal that means "capturing this game" rather than "running".
Two rules hold the file together. Nothing is scanned unless the switch is on, because reading the list of programs somebody runs is itself an intrusion. And the scan runs off the main thread, because enumerating processes blocks.
Unlike CheatDetection it prefers false positives: hiding an avatar that
needed no hiding costs nothing, while missing one puts a face on a stream.
Hiding means the avatar is never fetched, not merely not drawn. Nothing here is
ever sent to the server.
SurfacePlacementA landed vessel's alt is an altitude above sea level, so it only means
anything against the terrain that produced it. Hand a craft parked on stock Duna
to someone running Parallax and the same number is metres in the air or metres
inside the hill, with nothing in the craft file to notice it by: both saves agree
on every value and disagree about where the ground is.
So the sender's ground level under the craft is carried in a GKLAND block, and
the correction is relative: alt = terrain_local + (alt_sender - terrain_sender), the only arithmetic that survives two installs disagreeing about
the landscape.
The last few metres are stock's job, and the real work is not letting KSP skip
it. Vessel.Load clamps a landed craft up to the local PQS height, and
GoOffRails then calls CheckGroundCollision, which raycasts the actual collider.
But GoOffRails skips that whenever the vessel's stored PQSMin/PQSMax match
the local PQS levels, which between two players on default terrain detail they
do, precisely the case that needs it most. ForceGroundReseat therefore imports
the vessel the way KSP spawns one, so the seating runs regardless. That flag
alone fixes a craft at any wrong altitude, which is what makes the whole thing
degrade gracefully for a client that never wrote GKLAND.
GKLAND also carries the body name, because a VESSEL node names its world by
an index into FlightGlobals.Bodies that a planet pack reorders, so a Minmus
lander can otherwise arrive on Ike.
RescueWaypointsA surface rescue's landing site is the whole job, and until this the rescuer was handed a coordinate and nothing else while KSP already owned the instrument for it. A stock FinePrint waypoint draws on the map, in the tracking station and, once activated, on the navball, which is what a landing is actually flown against.
Four things shape it. Markers are derived, never stored, recomputed from the
contract list the client already holds, so a contract ending anywhere takes its
marker with it and nothing goes in the save. They live and die with the map
camera, since FinePrint.WaypointManager is a component on the map camera's
GameObject; entries are therefore invalidated rather than dropped, so the navball
can still be cleared for a rescue that ended while there was no map at all.
Absent lat/lon is not 0°, 0°: a rescue with the landing-site switch off means
"anywhere on the body", so the test is presence, never value. And only active,
incoming contracts are marked. Navigation is offered, not forced, because
switching the player's navball target out from under them is not ours to do.
CheckpointDetectorTicked each frame during flight. Detects milestones worth photographing:
Polled detectors (throttled to every 2 seconds):
SpaceObject (asteroid/comet) within 2,200m. Distinguishes
comets via the ModuleComet PartModule.Event-driven detectors (via GameEvents):
onCrewOnEva: a kerbal steps outsideonStageActivate: stage separation while in space (pad
staging is skipped)onVesselSituationChange → ORBITINGonVesselSituationChange → LANDEDonVesselSituationChange → SPLASHEDDebouncing:
"kind:target", e.g. "flyby:Mun")Suspended flag paused by the host during captureWhen a checkpoint fires, it invokes a callback with a Checkpoint struct
containing the kind, title, message, label, and optional target vessel/body.
The host (GeneKermanMod) shows a CheckpointPrompt and, if accepted, starts
the CinematicCapture coroutine.
ConsentKSP's add-on rules (8.1) require an unambiguous in-game opt-in before any
personally-identifiable information is gathered or sent. That consent is stored
in its own file, PluginData/consent.cfg (node GeneKermanConsent), separate
from settings.cfg, so the agreement is an explicit, standalone record.
ApiClient.TransmissionBlocked
short-circuits every request, and the link/login menu is gated behind it.Consent.cs re-reads the file when it changes on disk (mtime-checked), so a
manual edit takes effect live./api/v1/version/check
returns policy_version (from config/policy in Firestore, set with the
/policyversion admin command). When the server requires a newer version than
the one recorded, the mod blocks transmission and forces re-consent, a
fleet-wide re-consent with no mod rebuild.The opt-in is drawn by UI/ConsentWindow.cs in IMGUI, deliberately: porting it
to the canvas would put the gate on the surface the gate exists to disable.
DeviceIdA random GUID generated once and persisted to PluginData/device.id. It is:
X-Device-IdThe server blocks unrecognized device IDs until the user approves them from
Discord. GetKspLog() is only used when the player files a moderation report;
no hardware identifier is read at any point.
ModVersionCurrent: Human-readable version string (e.g. "1.0.0")Sha256: SHA-256 hash of the running GeneKerman.dll on disk, computed
once and cached. Sent to the server for version gating.AttestDigest(nonce, offset, length): challenge-response,
SHA256(UTF8(nonce) || dll[offset..offset+length]). The server recomputes
the same over the published DLL; a mismatch means the client's DLL has been
modified.The server compares ModVersion.Sha256 against the published latest and answers
/api/v1/version/check. A refused client raises UI/UpdateRequiredWindow, which
blocks every server-backed feature; "Continue anyway" drops into limited mode,
where only panels declaring WorksOffline (Tools, Settings) draw. That last part
is the point: without it a client rejected by the official server would have no
way to switch to a server that would accept it.
Being out of date is not the same as being refused. CKAN is the recommended update route and it indexes and upgrades on its own schedule, so a build that was the published latest until recently is accepted and merely told it is behind. The client reads that from three fields:
up_to_date answers "may I proceed", not "am I newest", and is true for a
graced build. It has to be this field, because every client already shipped
treats false as "raise the blocking window" and knows nothing about grace.update_available is the literal question, and drives the advisory notice.grace_until is when this build stops being accepted.NoticeUpdateAvailable raises that as one non-blocking notification per session,
carrying the server's own sentence when it sent one, so the update wording can
be corrected without shipping a client. A deadline is only ever appended when it
parsed, because a wrong date here is a wrong promise about when the mod stops
working.
OnVersionGate raises the same blocking window from any 426 on a gated
request, so a modified DLL that skipped the startup check is still stopped on its
first call.
A ban names one craft file that must not circulate, which is the case between
"this listing is bad" and "this player is bad". Every upload is fingerprinted
three ways server-side (exact bytes, design including part positions rounded
to the centimetre, and parts names alone) and a ban names which it enforces.
The mod needed no change for this. All three refusals ride the success:false
plus message path its panels already render, and quicksend only removes the
sender's vessel on ok, so a refusal can never destroy a ship. Worth knowing
while reading this file: a .craft is plain text, so anyone willing to open one
in an editor gets past any hash of it, and nothing downstream may treat "got past
the gate" as "safe".
A suspension is a timed block on the API surface (the KSP client and the
website), issued from the owner console. It is deliberately not a Discord ban
(the server repo's cogs/moderation.py owns those) and not a wipe: balance, XP, contracts and
listings are untouched and waiting. There is no permanent option.
Refusal is 403 {"code": "suspended", reason, until}, structured like the device
gate so the client can draw it: ApiClient routes it to
GeneKermanMod.OnSuspended → UI/SuspendedWindow.cs: reason, live countdown,
a "check again" button, and the reassurance that nothing was deleted. The
sidebar stops rendering behind it.
Sessions are deliberately not revoked. A revoked token would drop the mod to its link screen, whose only offer (link again) would work and change nothing; a live token means every request comes back carrying the explanation.
Expiry is resolved on read on both sides: the server checks until > now with
no sweeper, and the mod's Update frees itself on the clock and simply earns a
fresh 403 if it was wrong.
Unlike the update gate, SuspendedWindow has no limited mode behind it:
nothing here is fixable from the client side.
PartCatalogUploaderUploads the player's installed part list (internal_name + display_title for
every loaded part) to the server so the bot can resolve fuzzy part mentions in
mission constraints (e.g. "the Thud engine" → liquidEngine2-2).
The quicksend recipient list used to be everyone with a corp in your guild, which made the counterparty of a hand-over anybody at all in a large Discord. Friendship is now mutual and explicit (request, then accept), keyed on account ids so a Boundless account with no Discord is a first-class friend, and deliberately independent of any guild.
PlayerPicker grew a Source for exactly the reason it must not matter:
quicksend lists friends, contract creation still lists the roster, because a
contract is an offer of work and anyone may be offered one. The gate is the
server, not the picker, so a client drawing the wrong list cannot become a send
to a stranger.
A blueprint send is a copy. A live-vessel send is a hand-over: once the server confirms, this client queues the vessel and its crew out of the save through the same removal machinery as a rescue, and a decline gives it back.
Three fields make that safe across versions and rollbacks, and all three must stay:
vessel_pid, the pid in the sender's save, captured with the snapshot.
It is the marker that the sender actually removed the ship, so the reject
handler only returns entries carrying it.vessel_returnable in the send response is the mirror image: the client
only removes the vessel on the server's promise that a decline can return it,
so a new mod against an old server degrades to send-a-copy rather than deleting
a ship nothing would give back.MaybeHandleGiftAccepted uses to
re-assert a removal a quickload rolled back. Quicksends have no contract for
ReconcileRescueVessels to re-derive that intent from, so the echo is the
backstop.On the return import, ClientState checks the pid before spawning: original
still in the save means cancel the queued removal and keep it, never spawn a
duplicate.
VesselTransfer.ApplyIncomingOwnershipTag refuses an incoming crew name that
claims to be ours, because the crew node is written entirely by the sender.
"{me}'s Jeb" arriving from somebody else is a claim, not a fact, and that
refusal is the impersonation defence.
The honest return leg has exactly the forgery's shape. A rescue escapes it only because the server holds independent evidence. A quicksend had none, so lending a crewed ship to a friend and getting it back returned the kerbals double-tagged and borrowed, with the originals gone from the roster, and borrowed crew are eligible for the ghost purge, so a round trip could eventually delete the lender's own crew.
The server-side crew ledger is the missing record. On a live-vessel send back to
the same person it offers exactly the names that went out as homebound, which
the client passes to the one code path that may strip a tag. The refusal itself
is untouched: attested returns strip, everything else keeps it.
The wallet was a single number, which answers "how much have I got" and nothing
else. FinancePanel is the history: the summary, a 14-day graph and the list in
one column, with "Where it goes" (lifetime per-category, click-through to filter
the list) and "Send coins" taking the detail slot.
The graph is a diverging bar chart, income above the axis and spending below, which puts position on the in/out distinction so the two series never depend on a colour pair alone. The tab re-fetches on every open rather than once per KSP session, because a contract issued after the first open would otherwise never appear. Totals come from the server as their own field and the panel never adds anything up, because the stored list is a ring buffer and a summary computed by summing visible rows would silently shrink over time.
Fine debt is shown here and in the Profile panel, with the rate it is being collected at, because rewards that silently halve arrive as a bug report rather than as an appeal.
All integrations are reflection-based with no compile-time dependency. If
the target mod isn't installed, every call is a safe no-op. The only hard
requirement is ModuleManager, which applies the GeneKermanScale patch.
| Mod | What we do about it | Where |
|---|---|---|
| ModuleManager | Required. Patches GeneKermanScale onto every part prefab | Patches/GeneKermanScale.cfg |
| TweakScale (+ forks) | Bake absolute values on send so the craft needs no TweakScale on arrival; warn on a genuine version mismatch | ScaleBridge, GeneKermanScale, TweakScaleGuard |
| KSP-Recall | Its surface-attach pos encoding is why exports re-anchor pos from the live transform | ScaleBridge |
| Textures Unlimited | Carry the paint job's manifest; reconcile recolour modules per prefab on import | TextureTransfer |
| RealFuels / Realism Overhaul | Carry tank types and engine configs; reconcile to local fuels for a recipient without RF | RealFuelsTransfer |
| ReStock / ReStock+ | Substitute equivalent parts in both directions (ReStock+ hides its stand-ins when the DLC is present) | PartAliases |
| ConformalDecals | Re-issue the decal draw on the isolation layer so decals appear in blueprints; render never-drawn text decals | DecalCapture |
| Deferred (blackrack) | Render the capture camera on the deferred path, with a supersample for MSAA and narrow-FOV stand-ins for ortho | VesselRenderer |
| USI-LS / TAC-LS / Snacks / Kerbalism / DeepFreeze | Endurance scan, freeze/thaw handshake, ration kit | LifeSupport/ |
| Physics Range Extender | Temporarily disabled during a submission, and restored only if we disabled it | PhysicsRangeManager |
| Click Through Blocker | IMGUI windows drawn through CTB's GUILayoutWindow so clicks don't reach the game behind them | ClickThroughHelper |
| CKAN | read (indexed by , not folder) to map parts → mod identifiers |
PhysicsRangeManagerProblem: PRE inflates the physics bubble so many distant vessels stay loaded. During a multi-vessel submission, this causes unstable spam-loading.
Solution: Before capture, TryDisable() probes for PRE's static enable
toggle (searching member names: ModEnabled, Enabled, Active,
IsEnabled, enabled), turns it off, resets all loaded vessels' ranges to
stock defaults, captures, then Reenable() restores PRE, but only if the
mod was the one that disabled it.
ClickThroughHelperWhen installed, IMGUI windows are drawn through CTB's GUILayoutWindow instead
of the stock GUILayout.Window, preventing clicks on the mod's UI from also
reaching the game underneath. Resolved via reflection on
ClickThroughFix.ClickThruBlocker.GUILayoutWindow. The uGUI sidebar does not
need this: it holds an InputLockManager lock instead.
Every part-walk detection path in this codebase resolves a part to its GameData
folder via AvailablePart.partUrl. That means it is structurally blind to
any mod that adds no parts (TweakScale, Textures Unlimited, RealFuels), which
is exactly why each of those has a side-channel block of its own.
And a GameData folder is not a mod. DeepFreeze installs
REPOSoftTech/DeepFreeze next to its companion REPOSoftTech/BackgroundResources,
and the -Core split many mods ship (Firespitter/FirespitterCore,
NearFutureElectrical/NearFutureElectrical-Core) puts a parts mod and a
plugin-only one in one folder. So CKAN's registry is indexed by install path
and a part resolves through the longest path prefix exactly one module owns; a
prefix two modules share is left out of the index so the walk drops past it
rather than guessing.
The mod draws in two toolkits, and which one a screen uses is a decision, not an accident.
UI/Gui/): the primary interfaceA retained-mode Canvas UI in the GeneKerman.UI.Gui namespace. The toolbar
button opens it; the classic IMGUI main window it replaced is gone, and so
is UI/CreateContractWindow.cs (superseded outright by ContractForm, which
adds auctions, orbit/lat-lon/Δv margins, restriction modes and a permanence
gate the old window never had).
| Piece | Role |
|---|---|
Theme.cs | Design tokens, ported from the website's globals.css. The mod's only shared palette: the IMGUI files still use inline colors |
Sprites.cs | Procedural 9-slice rounded-rect sprites (there is no Unity Editor on this machine) |
UIF.cs | The fluent widget builder |
SidebarController.cs | Owns the Canvas, the expand animation, the input locks |
FloatWindow.cs | Draggable, clamped window shell |
ImageViewer.cs | Full-screen zoom/pan lightbox for submission images (borrows textures, never owns them) |
PlayerPicker.cs / BodyPicker.cs / DatePicker.cs | Shared pickers: players (quicksend + contracts), celestial bodies (rescues), an inline month grid |
Panels/ | The screens: missions, contract inbox, profile, feed, market (selling half), tools, settings |
The panel is centred and opens by expanding sideways out of the middle of the
screen. There is no pull-out tab and no near edge, so there is no VAB/SPH edge
mirroring to do. AnimateExpand drives openAmount and ApplyExpand is the
single writer of the transform: it scales panelRect.localScale.x through
EaseOutExpo from a centre pivot rather than animating the width, because a
per-frame width change re-runs the whole layout, and a layout squeezed near zero
makes every Ellipsis label render nothing, so the panel would flash empty on
the way out. The resting width still animates (400 ↔ 880 for master-detail),
because that one genuinely is a change of layout size.
Five rules that are load-bearing:
OnGUI, so it needs explicit
GameEvents.onHideUI handling or it appears in every screenshot.InputLockManager lock, or clicks reach the game behind it.
PointerOverSidebar returns false outright while closed, a scaled-to-nothing
panel still has a rect, and would otherwise hold a lock over a sliver of
screen showing nothing.KEYBOARDINPUT plus the
quick-save pair) held screen-wide, since a keystroke has no cursor position.Destroy(): a leaked control lock outlives
the mod.onGameSceneLoadRequested fires before the teardown and so cannot detect
it, SidebarController.UpdateAssets polls for it and UIF.RefreshFont()
re-points the live labels (Theme.Alive tests the material, not the
reference).TMP_FontAsset ships one default material that every label not asking
for its own shares: colour mask, stencil, z-test and clip rect all live
on it, so anything in the game (flight is where it happens) can make every
label invisible. Theme.FontMaterial is a private normalized clone taken in
the main menu, UIF.Label assigns it, and UIF.RefreshText() re-asserts it
and rebuilds meshes synchronously, SetAllDirty only queues, and a
queued-but-unserviced label draws nothing, silently.Limited mode. The acknowledged-update gate used to belong to the classic
window. The canvas now renders while UpdateRequired && UpdateAcknowledged and
narrows itself to the panels declaring WorksOffline (Tools, Settings) behind a
banner carrying Re-check / Download latest. Tools drops its two server-backed
cards there (quicksend, bug report) and keeps export, flag import and roster
repair, which are local. The empty else if (UpdateRequired) arm in
GeneKermanMod.OnGUI must stay: what it still does is stop the block below
it drawing the link window and the device prompt under the gate.
Exactly the set that draws when the canvas may not, porting any of them would put a gate on the surface the gate exists to disable.
| Window | Purpose |
|---|---|
ConsentWindow | First-run privacy/terms opt-in (KSP add-on rule 8.1) |
UpdateRequiredWindow | Mandatory update gate |
SuspendedWindow | Service-suspension notice: reason, countdown, "check again" |
DataPausedWindow | Data-sharing paused notice |
DeviceVerifyWindow | Device binding approval |
LinkWindow | Discord account linking (the toolbar opens the sidebar only once linked) |
CheckpointPrompt | "Capture this moment?": time-critical, drawn over the game |
WebUiWindow | Recovery path for a browser that never opened |
GKSkin defines the custom GUISkin these use, and all of them are drawn
through ClickThroughHelper.Window().
Panels/SubmitPanel.cs in a FloatWindow, because submitting
is read against the scene behind it. Living on the canvas gives it the render
gate (F2 and every capture hide the whole canvas, so submission no longer needs
a window of its own to be hidden for a screenshot), the input lock, the font
recovery and the teardown for free.Panels/ContractsPanel.cs, carrying the classic window's
mail furniture: week groups, a local bin and multi-select (ContractInbox.cs
holds both, shared so trashing in one front end hides it in the other),
mark-read/dismiss routed through ClientState's Request* wrappers so the
unread badge cannot drift from the list, and, in the editor, on an active
contract: the contract's part limits plus the switch that arms
EditorPartEnforcer.ContractForm, behind an explicit "this is
permanent" switch (sending destroys the issuer's vessel); spawning the wreck is
in ContractsPanel's active-contract actions. Both call ContractCreation /
RequestSpawnRescueWreck rather than reimplementing, since the per-save dedup,
the orbit-epoch freeze and the emergency-freeze registration hang off those.Panels/MarketPanel.cs is the selling half only.
Browsing and buying stay on the website; what a browser cannot do is read the
ship in the VAB and render it.ToolActions
with the others but, alone among them, is not exposed on the /gk/actions/*
bridge and has no card in the web screen: the attachment that makes a KSP bug
diagnosable (KSP.log) can only be read from inside the running game.Web/An optional second front end: the same account, drawn as a React page in the
player's own browser. Off by default (enableWebUi = false), switched on in the
sidebar's Settings panel. An existing install is never moved to a different UI
by a mod update.
Why a local server at all. KSP runs Unity 2019.4, which has no webview, and shipping an embedded browser would mean 50–300 MB of per-platform native binaries against a mod that is under a megabyte. So the UI runs in the player's browser, and this server is what makes that safe and same-origin:
GET / → the built React bundle in GameData/BoundlessMissions/WebUI/
* /api/v1/... → proxied upstream with the session token attached in C#
* /gk/... → game state and actions only this process can perform
GET /gk/events → SSE push, tee'd from the notification socket
Because the page, its data and its game bridge share one origin, there is no CORS, no mixed content, and the session token never enters JavaScript.
The five-layer gate. The bridge holds a 30-day session token and will attach it to anything it proxies, so "only our page may call this" has to be airtight. No layer is sufficient alone:
127.0.0.1 only: nothing on the LAN can reach it.HttpOnly; SameSite=Strict session cookie plus a CSRF token in a custom
header.Host match (DNS-rebinding defence) plus Origin and Sec-Fetch-Site.Layers 1, 2 and 5 live in LocalServer; BridgeAuth owns 3 and 4.
Known residual risk, documented rather than pretended away:
Application.OpenURLshells out toxdg-openon Linux, so the launch URL, nonce included, is briefly visible in/procandps aux. The 15 s TTL plus single use plus the browser consuming it in ~200 ms makes the window narrow, and a hostile local user on the same account could already just readPluginData/session.token.
ApiProxy is a confused deputy by construction, so its allow-list is the
security boundary: an endpoint not on the list cannot be reached with the
mod's token, whatever the page asks for.
Threading. The accept loop and every handler run off the main thread.
Anything that touches KSP goes through MainThreadQueue, drained by
GeneKermanMod.Update(). Long-running operations get a JobRegistry entry so a
page that reloads mid-install still learns the outcome.
Because the bridge binds a fresh ephemeral port every session,
http://127.0.0.1:<port> is a different origin on every launch, so
localStorage is empty each time and anything that must persist (starred
players, the inbox bin) lives in PluginData instead. See Favorites.cs and
ContractInbox.cs.
If the browser never opens, UI/WebUiWindow.cs is the recovery path: it shows
the origin so the player can open it by hand.
build.shcd "KSP Mod Side"
./build.sh
# 1. Build with dotnet (Release config, .NET 4.7.2 target)
# dotnet build -c Release -p:GKChannel="$CHANNEL"
# CHANNEL=production strips the debug self-test panel (GK_DEBUG_PANEL)
# 2. Print the DLL's SHA-256: register it with the update gate via
# /admin publishversion, or the admin console's Mod Version tab
# 3. Build the browser UI (repo-root WebUI/, via Vite → GameData/…/WebUI/)
# Skipped, not fatal, when node_modules is absent: but then WebUI/ is
# whatever was last built, which is what the manifest check below catches.
# 4. Prepare GameData/BoundlessMissions/
# - Copy GeneKerman.dll → Plugins/
# - Copy websocket-sharp.dll → Plugins/
# - Copy toolbar icon → Textures/
# - Copy Iconpack-1 UI icons → Textures/
# - Create default settings.cfg if absent
# 5. Deploy to every KSP install in KSP_PATHS[]
# - Preserve each install's existing settings.cfg (backup → copy → restore)
# - Copy the whole GameData/BoundlessMissions/ into each KSP's GameData/
# (WebUI/ is removed first: its assets are content-hashed, so a plain cp
# would leave every previous build's files behind)
The Vite build stamps WebUI/manifest.json with ModVersion.Current, and the
mod refuses to start the bridge if that does not match the running DLL, a
stale bundle talking to a newer /gk surface is exactly the failure this
prevents.
Deploy targets (KSP_PATHS in build.sh).
To build without deploying:
cd "KSP Mod Side/GeneKerman"
dotnet build -c Release
Release packaging (the CKAN-shaped zip in dist/) is covered in
PACKAGING.md.
GeneKerman.csproj)bin/GeneKerman.dll (class library, no entry point)KSPPath defaults to the KR-KSP dev instance (FK-KSP was the reference until it was deleted on 2026-08-22). ManagedPath is probed, not assumed (KSP_x64_Data/Managed on KR-KSP/KR2-KSP, KSP_Data/Managed on RSS-RO) with a fallback to any sibling instance that has assemblies and one explicit error if none do;
ManagedPath is $(KSPPath)/KSP_Data/Managed (not KSP_x64_Data/Managed
on these installs). Override with -p:KSPPath="..." to compile against
another install.Assembly-CSharp.dll / Assembly-CSharp-firstpass.dll (KSP's game API)UnityEngine.dll plus the modules actually used: CoreModule,
IMGUIModule, UIModule, UnityEngine.UI, UnityWebRequestModule,
ImageConversionModule, ScreenCaptureModule, TextRenderingModule,
InputLegacyModule, AnimationModule, PhysicsModulewebsocket-sharp.dll (bundled in lib/, the only Private=true reference)ToolbarControl and ClickThroughBlocker: referenced conditionally,
only if present in the target install's GameDataGK_DEBUG_PANEL is defined only off the production
channel, so a shipped DLL contains none of DebugTestPanel.cs.| Dependency | Source | Purpose |
|---|---|---|
Assembly-CSharp.dll | KSP install | KSP game API (vessels, parts, contracts, UI, …) |
UnityEngine*.dll | KSP install | Unity engine (rendering, coroutines, IMGUI, uGUI, textures) |
websocket-sharp.dll | Bundled (lib/) | WebSocket client (KSP's Mono lacks ClientWebSocket) |
| ModuleManager | Required (separate install) | Patches GeneKermanScale onto every part prefab |
Soft dependencies, all optional, all reflection-based, all no-ops when absent. See Third-Party Mod Compatibility for what each one actually does:
Click Through Blocker · ToolbarControl · TweakScale (+ forks) · KSP-Recall · Textures Unlimited · RealFuels / Realism Overhaul · ReStock / ReStock+ · ConformalDecals · Deferred · USI-LS · TAC-LS · Snacks · Kerbalism · DeepFreeze · Physics Range Extender · CKAN
The stock expansions (Making History, Breaking Ground) are treated as dependencies rather than as stock: they are bought separately, and owning one says nothing about the other.
GameData/BoundlessMissions/PluginData/settings.cfg (KSP ConfigNode format;
node name is GeneKerman). Everything here is editable from the sidebar's
Settings panel, hand-editing is supported but not required.
GeneKerman
{
useOfficialServer = true
serverProtocol = http
serverHost = localhost
serverPort = 5022
marketplaceProtocol = https
marketplaceAddress = boundlessmissions.com
enableNotifications = true
enableCheckpointPhotos = true
enableDataGathering = true
enableWebUi = false
enableEmergencyFreeze = true
emergencyRationDays = 3
enablePartSubstitution = true
enableTextureTransfer = true
enableFuelConfigTransfer = true
}
| Key | Default | Description |
|---|---|---|
useOfficialServer | true | Use the official backend. When true the custom host/port below are kept but ignored, so toggling back restores them |
serverProtocol / serverHost / serverPort | http / localhost / 5022 | The custom backend, stored in three fields: // is a comment delimiter in ConfigNode and would truncate a URL |
marketplaceProtocol / marketplaceAddress | https / the official site | The website the Market panel opens. Split for the same reason. The old single marketplaceUrl key is migrated on load and never written back |
enableNotifications | true | Notification push and polling |
enableCheckpointPhotos | true | Offer the cinematic capture prompt at detected milestones |
enableDataGathering | true | Send telemetry with submissions. Off shows the "data sharing paused" notice |
enableWebUi | false | Open the browser UI instead of the sidebar. Absent means classic: an existing install is never moved to a different UI by an update |
enableEmergencyFreeze | true | Freeze stranded rescue crew so they survive any life-support mod |
emergencyRationDays | 3 | Days of the rescuer's resources stowed aboard a rescue wreck. Clamped at 0: a hand-edited negative would size the kit backwards |
enablePartSubstitution | true | Swap missing parts for local equivalents. Off still scans and reports |
enableTextureTransfer | true | Carry Textures Unlimited paint jobs. Off still scans and warns |
enableFuelConfigTransfer | true | Carry RealFuels tank/engine configs. Off still scans and warns |
Consent is deliberately not in this file: it lives in
PluginData/consent.cfg as a standalone record. See
Consent Gate.
Runtime data (created automatically):
| Path | Contents |
|---|---|
PluginData/settings.cfg | The above |
PluginData/consent.cfg | Accepted policy version and timestamp |
PluginData/device.id | Stable per-install device identifier |
PluginData/session.token | Current signed session token |
PluginData/sessions.cfg | Known sessions |
PluginData/catalog.hash | FNV-1a hash of the last-uploaded part catalog |
PluginData/favorites.cfg | Starred players for the quicksend picker |
PluginData/trashed_contracts.txt | The inbox's local bin (never sent to the server) |
PluginData/trait_repairs.cfg | Original professions loaned out by TraitRepair |
PluginData/renders/ | Saved blueprint PNGs |
PluginData/screenshots/ | Saved screenshots |
GameData/GeneKerman/Flags/ | Content-addressed flag images that arrived with crafts |
<KSP>/GeneKerman_MissingMods/*.ckan | Generated CKAN metapackages |
RealFuelsTransfer.cs | 1,135 | GKRF. Carries RealFuels/RO tank types and engine configs; checks on an RF install, reconciles to local fuels on one without |
ReforgedTransfer.cs | 591 | Reforged Materials Redux. The other recolour mod. No side-channel block: its paint already rides in the craft's own MODULE nodes, so this is a strict "is it actually painted" test plus an import-side reconcile |
SurfacePlacement.cs | 576 | GKLAND. Puts a transferred landed craft on the recipient's ground, correcting altitude relative to the sender's terrain and forcing KSP's own ground reseat |
QuicksendLedger.cs | 255 | Local record of what this client sent to whom, so a returning hand-over can be recognised |
CkanGenerator.cs | 1,078 | GKMODS. Maps parts → mods via CKAN's install paths, reports missing mods, writes a .ckan metapackage |
PartAliases.cs | 410 | Part substitution. Swaps a missing part for the equivalent this install has (source of truth for the server repo's data/part_aliases.py) |
CraftThumb.cs | 209 | GKTHUMB. Embeds/extracts the NW-view thumbnail PNG for KSP's craft browser |
VesselDataCollector.cs | 394 | Telemetry. Vessel snapshots (orbit, mass, cost, crew), craft file lookup, screenshots |
ControlDiag.cs | 264 | Diagnostics for stuck input locks and control-lock leaks |
DebugTestPanel.cs | 378 | In-game security self-test panel. Compiled out of production builds (#if GK_DEBUG_PANEL) |
Web/DebugBridge.cs, Web/DebugRoutes.cs | 1,000+ | The agent test bridge. Compiled out of production builds (#if GK_DEBUG_PANEL); see tools/README-bridge.md |
Web/BodyReader.cs | 79 | Request-body reader for the bridge listeners |
UI/Gui/Panels/MoreTimePanel.cs |
| 286 |
| The "ask for more time" form, mounted as a window because it is read against the deadline it moves |
UI/Gui/ContractForm.cs | 610 | Contract creation: rewards, constraints, mod-list mode, auctions, rescues |
UI/Gui/PlayerPicker.csBodyPicker.csDatePicker.cs | 469 / 175 / 285 | Shared search/favourites, celestial body, and inline month-grid pickers |
UI/Gui/ImageViewer.cs | 409 | Full-screen zoom/pan lightbox for submission images |
UI/Gui/ToastHost.cs | 253 | Toast notifications on the canvas |
UI/Gui/ScrollForwarder.csScrollMemory.csFmt.cs | 58 / 158 / 105 | Nested-scroll forwarding, scroll position memory, formatting helpers |
UI/ConsentWindow.cs | 169 | First-run privacy/terms opt-in (IMGUI: it gates the canvas) |
UI/LinkWindow.cs | 283 | Discord account linking flow |
UI/SuspendedWindow.cs | 197 | Service-suspension notice: reason, live countdown, "check again" |
UI/UpdateRequiredWindow.cs | 128 | Mandatory update gate |
UI/DeviceVerifyWindow.cs | 79 | Device binding approval |
UI/DataPausedWindow.cs | 106 | Data-sharing paused notice |
UI/CheckpointPrompt.cs | 147 | "Capture this moment?" prompt over the flight scene |
UI/WebUiWindow.cs | 119 | Recovery path for a browser that never opened |
UI/GKSkin.cs | 144 | Custom IMGUI GUISkin for the windows above |
registry.jsonCkanGenerator |
| Making History / Breaking Ground | Treated as dependencies, not stock: keyed by two-segment path (SquadExpansion/MakingHistory), reported when missing but never written into a .ckan (CKAN can detect a DLC and never install one) | CkanGenerator |