@spacemolt/lib 14.1.0 → 14.2.0

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
@@ -138,7 +138,7 @@ for await (const hit of account.events('battle_damage')) { /* ... */ }
138
138
 
139
139
  ### Subscriptions
140
140
 
141
- Subscribe to a station's order book or to player presence at your location; the
141
+ Subscribe to a station's order book or to presence at your location; the
142
142
  baseline snapshot seeds a local cache that the push stream keeps current:
143
143
 
144
144
  ```ts
@@ -146,9 +146,19 @@ await account.subscribeMarket(); // current docked station
146
146
  account.market(baseId); // cached order book, kept live by `market_update`
147
147
 
148
148
  await account.subscribeObservation();
149
- account.observation(); // cached presence, kept live by `observation_update`
149
+ const view = account.observation(); // kept live by `observation_update`
150
+ view?.nearby; // players at your POI, keyed by player_id
151
+ view?.system; // players system-wide
152
+ view?.pirates; // pirate NPCs, keyed by pirate_id
153
+ view?.empireNpcs; // empire NPCs, keyed by npc_id
154
+ view?.creatures; // wildlife, keyed by creature_id
155
+ view?.prizes; // intact captured ships, keyed by prize_id
150
156
  ```
151
157
 
158
+ The watch is not players only. It covers five of `get_nearby`'s six classes —
159
+ arena NPCs are the one it does not carry — and every class except wildlife is
160
+ also mirrored into `account.state.location`.
161
+
152
162
  ## Multi-account
153
163
 
154
164
  `SpacemoltClient` manages many accounts, staggers connects to respect login
package/dist/account.d.ts CHANGED
@@ -343,14 +343,17 @@ export declare class Account {
343
343
  /** The cached order book for a base, if subscribed. */
344
344
  market(baseId: string): MarketBook | undefined;
345
345
  /**
346
- * Subscribe to a change-feed of player presence at your current POI/system.
346
+ * Subscribe to a change-feed of presence at your current POI/system: players
347
+ * (POI and system-wide), plus the pirates, empire NPCs, wildlife and intact
348
+ * prizes at the POI — not players only. That is five of `get_nearby`'s six
349
+ * classes; arena NPCs are not in the feed, so an arena match still needs
350
+ * `get_nearby` to see its opponents.
347
351
  * Returns the baseline and seeds the observation cache; `observation_update`
348
352
  * pushes are merged automatically. Read with `account.observation()`. Also
349
- * bridges POI-scoped presence into the base state cache's
350
- * `location.nearby_players`/`nearby_player_count` (see
351
- * `bridgeObservationToLocation`), so `account.state.location` reflects live
352
- * data too once subscribed, instead of only refreshing on the next
353
- * `get_status`/mutation delta.
353
+ * bridges the POI-scoped classes into their matching `location` fields in the
354
+ * base state cache (see `bridgeObservationToLocation`), so
355
+ * `account.state.location` reflects live data too once subscribed, instead of
356
+ * only refreshing on the next `get_status`/mutation delta.
354
357
  *
355
358
  * Scoped to your current POI the same way `subscribeMarket` is scoped to
356
359
  * your current dock — see the docked_at note there; the same silent
@@ -362,14 +365,30 @@ export declare class Account {
362
365
  /** Unsubscribe from the observation watch and clear its cache. */
363
366
  unsubscribeObservation(): Promise<void>;
364
367
  /**
365
- * Mirrors the observation watch's POI-scoped player presence into
366
- * `location.nearby_players`/`nearby_player_count` in the base state cache,
367
- * so code reading `account.state.location` (rather than
368
- * `account.observation()`) sees live presence data once subscribed. NPC and
369
- * pirate presence (`nearby_empire_npcs`/`nearby_pirates`, their counts, and
370
- * `offline_collapsed`) have no equivalent in the observation feed — it only
371
- * ever carries player presence — so those fields are left untouched by this
372
- * bridge and still only refresh on the next `get_status`/mutation delta.
368
+ * Mirrors the observation watch's POI-scoped presence into the matching
369
+ * `location` fields in the base state cache, so code reading
370
+ * `account.state.location` (rather than `account.observation()`) sees live
371
+ * data once subscribed: players, pirates, empire NPCs, intact prizes, their
372
+ * counts, and the cloaked-signature hint. Wildlife has no `location` field —
373
+ * read creatures from `account.observation()`.
374
+ *
375
+ * Three ways the bridged values differ from what `get_status` would return
376
+ * for the same POI, none of them fixable from here:
377
+ * - The arrays are uncapped. `get_status` truncates them (50 players, 20
378
+ * each of the rest) while reporting the pre-cap total in the count, so a
379
+ * bridged array can be longer than the server would ever send. The counts
380
+ * mean the same thing either way.
381
+ * - `offline_collapsed` is a `get_status` response cap the feed does not
382
+ * report, so it is left untouched at whatever the server last said. Above
383
+ * the crowding threshold `get_status` drops offline players from
384
+ * `nearby_players` and counts them there instead; the feed carries them in
385
+ * full, so once bridged the two no longer sum — read the array, not the sum.
386
+ * - `ship_name` on players and empire NPCs is the ship's display name here
387
+ * (its custom name, else the class name) where `get_status` sends the
388
+ * custom name alone. Present does not imply custom-named while subscribed.
389
+ *
390
+ * `unknown_signature` is written even when false, where `get_status` omits
391
+ * the key; the bridge has to write it to clear a signature that has gone.
373
392
  * No-ops if `location` hasn't been seeded yet or nothing is subscribed.
374
393
  */
375
394
  private bridgeObservationToLocation;
package/dist/index.js CHANGED
@@ -3832,6 +3832,15 @@ function indexBy(items, key) {
3832
3832
  }
3833
3833
  return map;
3834
3834
  }
3835
+ function merge(map, changed, departed, key) {
3836
+ for (const item of changed ?? []) {
3837
+ const k = key(item);
3838
+ if (k)
3839
+ map.set(k, item);
3840
+ }
3841
+ for (const id of departed ?? [])
3842
+ map.delete(id);
3843
+ }
3835
3844
 
3836
3845
  class ObservationCache {
3837
3846
  view = null;
@@ -3842,6 +3851,10 @@ class ObservationCache {
3842
3851
  tick: 0,
3843
3852
  nearby: indexBy(snapshot.nearby, (p) => p.player_id),
3844
3853
  system: indexBy(snapshot.system_agents, (p) => p.player_id),
3854
+ pirates: indexBy(snapshot.pirates, (p) => p.pirate_id),
3855
+ empireNpcs: indexBy(snapshot.empire_npcs, (n) => n.npc_id),
3856
+ creatures: indexBy(snapshot.creatures, (c) => c.creature_id),
3857
+ prizes: indexBy(snapshot.prizes, (p) => p.prize_id),
3845
3858
  cloaked: indexBy(snapshot.cloaked_contacts, (c) => c.target_id),
3846
3859
  unknownSignature: snapshot.unknown_signature ?? false,
3847
3860
  activeScan: snapshot.active_scan ?? false
@@ -3856,6 +3869,10 @@ class ObservationCache {
3856
3869
  tick: update.tick,
3857
3870
  nearby: new Map,
3858
3871
  system: new Map,
3872
+ pirates: new Map,
3873
+ empireNpcs: new Map,
3874
+ creatures: new Map,
3875
+ prizes: new Map,
3859
3876
  cloaked: new Map,
3860
3877
  unknownSignature: false,
3861
3878
  activeScan: false
@@ -3863,21 +3880,13 @@ class ObservationCache {
3863
3880
  }
3864
3881
  const v = this.view;
3865
3882
  v.tick = update.tick;
3866
- for (const p of update.nearby_changed ?? [])
3867
- if (p.player_id)
3868
- v.nearby.set(p.player_id, p);
3869
- for (const id of update.nearby_departed ?? [])
3870
- v.nearby.delete(id);
3871
- for (const p of update.system_changed ?? [])
3872
- if (p.player_id)
3873
- v.system.set(p.player_id, p);
3874
- for (const id of update.system_departed ?? [])
3875
- v.system.delete(id);
3876
- for (const c of update.cloaked_resolved ?? [])
3877
- if (c.target_id)
3878
- v.cloaked.set(c.target_id, c);
3879
- for (const id of update.cloaked_lost ?? [])
3880
- v.cloaked.delete(id);
3883
+ merge(v.nearby, update.nearby_changed, update.nearby_departed, (p) => p.player_id);
3884
+ merge(v.system, update.system_changed, update.system_departed, (p) => p.player_id);
3885
+ merge(v.pirates, update.pirates_changed, update.pirates_departed, (p) => p.pirate_id);
3886
+ merge(v.empireNpcs, update.empire_npcs_changed, update.empire_npcs_departed, (n) => n.npc_id);
3887
+ merge(v.creatures, update.creatures_changed, update.creatures_departed, (c) => c.creature_id);
3888
+ merge(v.prizes, update.prizes_changed, update.prizes_departed, (p) => p.prize_id);
3889
+ merge(v.cloaked, update.cloaked_resolved, update.cloaked_lost, (c) => c.target_id);
3881
3890
  if (update.unknown_signature !== undefined)
3882
3891
  v.unknownSignature = update.unknown_signature;
3883
3892
  if (update.active_scan !== undefined)
@@ -4579,9 +4588,39 @@ class Account {
4579
4588
  ...p.ship_name !== undefined && { ship_name: p.ship_name },
4580
4589
  ...p.username !== undefined && { username: p.username }
4581
4590
  }));
4591
+ const nearbyPirates = [...view.pirates.values()].map((p) => ({
4592
+ pirate_id: p.pirate_id,
4593
+ name: p.name,
4594
+ tier: p.tier,
4595
+ is_boss: p.is_boss,
4596
+ status: p.status,
4597
+ hull: p.hull ?? 0,
4598
+ max_hull: p.max_hull ?? 0,
4599
+ shield: p.shield ?? 0,
4600
+ max_shield: p.max_shield ?? 0,
4601
+ ...p.faction !== undefined && { faction: p.faction },
4602
+ ...p.faction_name !== undefined && { faction_name: p.faction_name }
4603
+ }));
4604
+ const nearbyEmpireNpcs = [...view.empireNpcs.values()].map((n) => ({
4605
+ npc_id: n.npc_id,
4606
+ name: n.name,
4607
+ role: n.role,
4608
+ empire: n.empire,
4609
+ in_combat: n.in_combat,
4610
+ ...n.fleet_name !== undefined && { fleet_name: n.fleet_name },
4611
+ ...n.ship_class !== undefined && { ship_class: n.ship_class },
4612
+ ...n.ship_name !== undefined && { ship_name: n.ship_name }
4613
+ }));
4582
4614
  const changed = this.cache.patchSection("location", {
4583
4615
  nearby_players: nearbyPlayers,
4584
- nearby_player_count: view.nearby.size
4616
+ nearby_player_count: view.nearby.size,
4617
+ nearby_pirates: nearbyPirates,
4618
+ nearby_pirate_count: view.pirates.size,
4619
+ nearby_empire_npcs: nearbyEmpireNpcs,
4620
+ nearby_empire_npc_count: view.empireNpcs.size,
4621
+ nearby_prizes: [...view.prizes.values()],
4622
+ nearby_prize_count: view.prizes.size,
4623
+ unknown_signature: view.unknownSignature
4585
4624
  });
4586
4625
  if (changed.length)
4587
4626
  this.stateRevision++;
@@ -2,11 +2,18 @@
2
2
  * Local cache of the subscribed observation watch (current POI + system).
3
3
  *
4
4
  * Seeded from the `subscribe_observation` baseline and kept current by merging
5
- * `observation_update` pushes: `*_changed` arrays upsert players (keyed by
6
- * `player_id`), `*_departed` arrays (player_id strings) remove them, and the
5
+ * `observation_update` pushes: `*_changed` arrays upsert entities (keyed by
6
+ * their own id), `*_departed` arrays (id strings) remove them, and the
7
7
  * cloaked-contact / signature hints are tracked alongside.
8
+ *
9
+ * Not a players-only feed: the baseline and every update also carry the
10
+ * pirate, empire-NPC, wildlife and intact-prize presence at the watched POI,
11
+ * each with the same changed/departed delta shape as players. It covers five
12
+ * of `get_nearby`'s six classes — arena NPCs are the one it does not carry, so
13
+ * a player in an arena match still has to poll `get_nearby` for their
14
+ * opponents. `offline_collapsed` has no equivalent here either.
8
15
  */
9
- import type { NotificationObservationUpdate, SubscribeObservationResponse } from '../generated/openapi/types.gen.js';
16
+ import type { CreatureInfo, EmpireNpcInfo, NotificationObservationUpdate, PirateInfo, PrizeInfo, SubscribeObservationResponse } from '../generated/openapi/types.gen.js';
10
17
  export type ObservedPlayer = NonNullable<NotificationObservationUpdate['nearby_changed']>[number];
11
18
  export type CloakedContact = NonNullable<NotificationObservationUpdate['cloaked_resolved']>[number];
12
19
  export interface ObservationView {
@@ -18,6 +25,14 @@ export interface ObservationView {
18
25
  nearby: Map<string, ObservedPlayer>;
19
26
  /** Uncloaked players system-wide, keyed by player_id. */
20
27
  system: Map<string, ObservedPlayer>;
28
+ /** Pirate NPCs at the watched POI, keyed by pirate_id. */
29
+ pirates: Map<string, PirateInfo>;
30
+ /** Empire NPCs at the watched POI, keyed by npc_id. */
31
+ empireNpcs: Map<string, EmpireNpcInfo>;
32
+ /** Wildlife at the watched POI, keyed by creature_id. */
33
+ creatures: Map<string, CreatureInfo>;
34
+ /** Intact captured ships at the watched POI, keyed by prize_id. */
35
+ prizes: Map<string, PrizeInfo>;
21
36
  /** Cloaked contacts resolved by an active sensor sweep, keyed by target_id. */
22
37
  cloaked: Map<string, CloakedContact>;
23
38
  /** A faint cloaked-ship signature is present at the watched POI. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spacemolt/lib",
3
- "version": "14.1.0",
3
+ "version": "14.2.0",
4
4
  "description": "The definitive TypeScript library for SpaceMolt. WebSocket-v2-first, multi-account, with local state caches. Internals are regenerated from the server's OpenAPI spec.",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",