@ksp-gonogo/kerbcast 1.10.2 → 1.11.0-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -59,6 +59,10 @@ client.on("adaptive-shed", (event) => {
59
59
  client.disconnect();
60
60
  ```
61
61
 
62
+ Commands can be sent as soon as `connect` resolves. Any sent before the
63
+ control channel has opened wait for it, and reject if it has not opened
64
+ within 10 s or the client disconnects first.
65
+
62
66
  ### Pre-handshake discovery
63
67
 
64
68
  ```ts
@@ -106,8 +110,9 @@ controls each part actually supports.
106
110
  skip, producing the macroblocking and stuttering aesthetic of
107
111
  in-game CommNet signal loss. Also a real CPU optimisation when
108
112
  every consumer wants degraded video.
109
- - `kind` (`"part"` | `"kerbal"`) and, for a kerbal face camera,
110
- `crewLocation` (`"seat"` | `"eva"`). Distinguish crew face cameras
113
+ - `kind` (`"part"` | `"kerbal"` | `"command"`) and, for a kerbal face
114
+ camera, `crewLocation` (`"seat"` | `"eva"`). Distinguish crew face
115
+ cameras and command cameras (see [Command cameras](#command-cameras))
111
116
  from part cameras.
112
117
  - `trackMode` (`"none"` | `"activeVessel"` | `"target"`). Server-authoritative
113
118
  auto-track state on a pan+zoom camera, so every consumer reflects the same
@@ -123,6 +128,254 @@ controls each part actually supports.
123
128
  the active vessel or its target (`TrackMode`), or `"none"` to stop. The
124
129
  chosen mode is published back on `CameraState.trackMode`.
125
130
 
131
+ ## Command cameras
132
+
133
+ A command camera is a camera that stands on the ground (or rides a craft)
134
+ and watches a craft from there, the way a tracking station would. The
135
+ game's settings give each scene one, the Launchpad camera, and a client can
136
+ create more at any vantage. Each appears in `client.cameras` with
137
+ `kind: "command"` and a `commandCentre` describing where it stands and what
138
+ it watches.
139
+
140
+ A created camera lasts for the rest of the game scene (a flight, or the
141
+ Space Center while the game's command camera setting is on); leaving the
142
+ scene removes it. Every connected client sees it, and any of them can aim
143
+ or destroy it.
144
+
145
+ The vantage is one of:
146
+
147
+ - `launchSite`: beside a launch site. `site` names it (`"LaunchPad"`,
148
+ `"Runway"`, a Making History or Kerbal Konstructs site); omit it for the
149
+ active craft's launch site. `distance` (default 500 m), `bearing` from the
150
+ site (default 180, due south) and `height` (default 5 m)
151
+ - `station`: at a CommNet ground station, by its node name (`"KSC"`), with
152
+ `height` above it (default 30 m)
153
+ - `surface`: at any `latitude` and `longitude` on a `body`, `height` metres
154
+ above the ground there (above the sea over water; default 5 m)
155
+ - `vessel`: riding the craft with that `persistentId`. Keep that craft
156
+ beyond physics range of the active one, or the lens sits inside its parts.
157
+ If the craft docks into another (or is grabbed by a claw), the camera rides
158
+ the combined craft. If the craft is destroyed or recovered, the camera is
159
+ removed and its feed ends
160
+
161
+ `createCommandCamera` resolves with the new camera's `CameraState` once the
162
+ game has built it and reported what it is watching, normally within a
163
+ second or two, so `commandCentre.subjectPersistentId` and
164
+ `commandCentre.targets` are already filled in. If that report has not come
165
+ 5 s after the camera appeared, it resolves with the state as it stands. The
166
+ request carries a token (random unless you pass `token`) that the camera
167
+ reports back as `commandCentre.token`; that is how the promise finds it,
168
+ and how any other code can. A vantage the game can not find (an unknown
169
+ body, site, station or craft) builds nothing, and the promise rejects after
170
+ `timeoutMs` (default 35 s, since the game waits up to 30 s for a site to be
171
+ placed).
172
+
173
+ Lens reach depends on the vantage: a launch site camera has the tracking
174
+ camera's lens at every Tracking Station level, a station's lens follows the
175
+ Tracking Station's level in career, and surface and vessel vantages zoom
176
+ fully.
177
+
178
+ ### Aiming
179
+
180
+ `client.setCommandAim(flightId, aim)` points any command camera, and `null`
181
+ returns it to its default: the active craft in flight, the configured craft
182
+ at the Space Center. `createCommandCamera` takes the same `aim` as an option.
183
+ The aim is one of:
184
+
185
+ - `vessel`: follows the craft with that `persistentId` (any entry of
186
+ `commandCentre.targets`, whatever its eligibility)
187
+ - `orbit`: follows a conic (`body`, `inclination`, `eccentricity`,
188
+ `semiMajorAxis`, `lan`, `argumentOfPeriapsis` in degrees and metres,
189
+ `meanAnomalyAtEpoch` in radians, `epoch`)
190
+ - `positionAt`: follows a position you propagate yourself: `position` and
191
+ `velocity` as `[x, y, z]` relative to `body` in its non-rotating frame at
192
+ `ut`, carried on linearly
193
+ - `surface`: holds a point on a `body` (`latitude`, `longitude`, `altitude`
194
+ in metres above sea level), turning with the planet
195
+ - `direction`: holds a direction from the camera's local horizon,
196
+ `azimuth` degrees clockwise from north and `elevation` degrees above the
197
+ horizon
198
+
199
+ The subject is the craft the camera draws (where it truly is, so an aim off
200
+ the craft shows it off centre), brackets, sizes its zoom by and reads out. A
201
+ `vessel` aim's craft is its subject; `orbit` and `positionAt` take an
202
+ optional `subjectPersistentId`; otherwise the default craft stays the
203
+ subject. A `direction` aim zooms for the subject's range, so the subject is
204
+ framed as it passes through. Every kind takes `framingMarginM`, metres of
205
+ view kept around the subject's own size. `commandCentre.aimKind` (absent for
206
+ the default) and `commandCentre.subjectPersistentId` report what the game
207
+ applied.
208
+
209
+ ### A launchpad camera
210
+
211
+ ```ts
212
+ import { CommandVantageKind, KerbcastClient } from "@ksp-gonogo/kerbcast";
213
+
214
+ const client = new KerbcastClient({ host: "192.168.1.74", port: 8088 });
215
+ await client.connect([], { slots: 2 });
216
+
217
+ // 500 m south of the active craft's launch site, 5 m up.
218
+ const pad = await client.createCommandCamera(
219
+ { kind: CommandVantageKind.LaunchSite },
220
+ { name: "Pad south" },
221
+ );
222
+
223
+ const video = document.querySelector("video")!;
224
+ client.camera(pad.flightId).on("stream", (stream) => {
225
+ video.srcObject = stream;
226
+ });
227
+ await client.subscribe(pad.flightId);
228
+
229
+ // Later: the feed ends for every client.
230
+ await client.destroyCommandCamera(pad.flightId);
231
+ ```
232
+
233
+ `{ kind: CommandVantageKind.LaunchSite, site: "Runway", distance: 800,
234
+ bearing: 90, height: 20 }` stands one 800 m east of the runway, 20 m up.
235
+
236
+ ### Between two vantages
237
+
238
+ Here a camera stands on a rise west of the KSC and watches the launch pad,
239
+ then a particular craft, then the sky above the pad, then whichever craft
240
+ is active. The coordinates are illustrative.
241
+
242
+ ```ts
243
+ import {
244
+ CommandAimKind,
245
+ CommandVantageKind,
246
+ type KerbcastClient,
247
+ } from "@ksp-gonogo/kerbcast";
248
+
249
+ declare const client: KerbcastClient; // connected as above
250
+
251
+ const hill = await client.createCommandCamera(
252
+ {
253
+ kind: CommandVantageKind.Surface,
254
+ body: "Kerbin",
255
+ latitude: -0.08,
256
+ longitude: -74.75,
257
+ height: 10,
258
+ },
259
+ {
260
+ name: "West ridge",
261
+ // The pad: altitude is metres above sea level, and the margin keeps
262
+ // 60 m of view around the subject.
263
+ aim: {
264
+ kind: CommandAimKind.Surface,
265
+ body: "Kerbin",
266
+ latitude: -0.0972,
267
+ longitude: -74.5577,
268
+ altitude: 70,
269
+ framingMarginM: 60,
270
+ },
271
+ },
272
+ );
273
+
274
+ // Follow a particular craft, by Vessel.persistentId.
275
+ await client.setCommandAim(hill.flightId, {
276
+ kind: CommandAimKind.Vessel,
277
+ persistentId: 4000000001,
278
+ });
279
+
280
+ // Hold still, looking east and 30 degrees up, for a launch to climb into.
281
+ await client.setCommandAim(hill.flightId, {
282
+ kind: CommandAimKind.Direction,
283
+ azimuth: 90,
284
+ elevation: 30,
285
+ });
286
+
287
+ // Back to the default: the active craft.
288
+ await client.setCommandAim(hill.flightId, null);
289
+ ```
290
+
291
+ A station watching a craft is the same with
292
+ `{ kind: CommandVantageKind.Station, station: "KSC" }` and a `vessel` aim.
293
+
294
+ ### The command lock
295
+
296
+ A mod in game can take a lock on every command camera's aim
297
+ (`KerbcastControl.SetCommandAuthority(owner)`). While it holds the lock only
298
+ its own aims are accepted: `setCommandAim` rejects with a
299
+ `CommandLockedError` (its `owner` names the holder) whenever the camera's
300
+ `commandTrack.authority` reports one, and a call that races the lock being
301
+ taken comes back as an `error` event with code `ErrorCode.CommandLocked`.
302
+ `createCommandCamera` still creates the camera, at its default aim. When the
303
+ lock is released every command camera returns to its default; aims set
304
+ before or during the lock are not restored.
305
+
306
+ The lock holds until its holder releases it (or the scene changes); it never
307
+ times out. A holder that stops calling for 5 s blanks every command feed
308
+ ("gonogo not responding") but keeps the lock, so aims from anyone else are
309
+ still refused. This is deliberate: a holder that delays commands (gonogo
310
+ applies signal delay) can not be bypassed while it is unresponsive.
311
+
312
+ ### In game (C#)
313
+
314
+ Another KSP mod can do the same through `Kerbcast.KerbcastControl`, with
315
+ no sidecar round trip:
316
+
317
+ ```csharp
318
+ using Kerbcast;
319
+
320
+ // Returns the flightId the feed will have, or 0 when refused.
321
+ uint pad = KerbcastControl.CreateCommandCamera(CommandVantage.LaunchSite());
322
+
323
+ uint ridge = KerbcastControl.CreateCommandCamera(
324
+ CommandVantage.Surface("Kerbin", -0.08, -74.75, height: 10), "West ridge",
325
+ CommandAim.Surface("Kerbin", -0.0972, -74.5577, 70, framingMarginM: 60));
326
+ KerbcastControl.SetCommandAim(ridge, CommandAim.Vessel(vessel.persistentId));
327
+ KerbcastControl.SetCommandAim(ridge, CommandAim.Direction(azimuth: 90, elevation: 30));
328
+ KerbcastControl.SetCommandAim(ridge, null); // back to the default
329
+
330
+ KerbcastControl.DestroyCommandCamera(ridge);
331
+ ```
332
+
333
+ Holding the lock:
334
+
335
+ ```csharp
336
+ // Re-take it on every scene load; a different owner is refused.
337
+ KerbcastControl.SetCommandAuthority("gonogo");
338
+
339
+ // Only calls passing the owner are accepted now, and each is a heartbeat.
340
+ KerbcastControl.SetCommandAim(pad, CommandAim.PositionAt("Kerbin", ut, pos, vel,
341
+ subject: craft.persistentId, framingMarginM: uncertaintyM), "gonogo");
342
+ KerbcastControl.SetCommandAim(pad, CommandAim.Vessel(other)); // false: locked
343
+
344
+ // Every command camera returns to its default.
345
+ KerbcastControl.ClearCommandAuthority("gonogo");
346
+ ```
347
+
348
+ - `CommandVantage.LaunchSite(site, distance, bearing, height)`,
349
+ `CommandVantage.Station(nodeName, height)`,
350
+ `CommandVantage.Surface(body, latitude, longitude, height)` and
351
+ `CommandVantage.Vessel(persistentId)` mirror the vantage kinds above, with
352
+ the same defaults
353
+ - `CommandAim.Vessel(persistentId, framingMarginM = 0)`,
354
+ `Orbit(body, inc, e, sma, lan, argPe, meanAnomalyAtEpoch, epoch, subject = 0, framingMarginM = 0)`,
355
+ `PositionAt(body, ut, pos, vel, subject = 0, framingMarginM = 0)`,
356
+ `Surface(body, lat, lon, alt, framingMarginM = 0)` and
357
+ `Direction(azimuth, elevation, framingMarginM = 0)` mirror the aim kinds
358
+ - `CreateCommandCamera(vantage, name = null, aim = null)` returns the
359
+ flightId at once; the feed appears when the vantage resolves, and
360
+ `SetCommandAim` works on it before then too. Under a held lock the aim is
361
+ not applied
362
+ - `SetCommandAim(flightId, aim, owner = null)` returns false when the lock
363
+ refuses it, the feed is not a command feed, or the aim is unusable (a
364
+ craft that no longer exists, such as one docked into another, is
365
+ unusable)
366
+ - The lock holds until `ClearCommandAuthority` or a scene change, and fails
367
+ closed: a holder that makes no call (`SetCommandAuthority`, `SetCommandAim`
368
+ with its owner, `SetCommandBlank`) for 5 s of real time blanks every
369
+ command feed "gonogo not responding" and keeps the lock, so everyone else
370
+ stays refused. `SetCommandBlank(flightId, reason)` blanks one feed
371
+ - `DestroyCommandCamera(flightId)` returns false for the settings camera or
372
+ an unknown id
373
+ - `CommandFeeds()` lists every built command camera, with its `Aim`,
374
+ `SubjectPersistentId`, and `Created` set on the ones made at runtime;
375
+ `CommandTargets(flightId)` lists the craft it could follow (a craft docked
376
+ into another drops out; the combined craft keeps the survivor's
377
+ `persistentId`)
378
+
126
379
  ## Pan/zoom input helper
127
380
 
128
381
  Driving pan and zoom from a gamepad stick, nudge buttons, or an FoV