@shipload/sdk 0.7.1 → 1.0.0-next.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.
Files changed (94) hide show
  1. package/lib/shipload.d.ts +2730 -287
  2. package/lib/shipload.js +10862 -2229
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +10434 -2171
  5. package/lib/shipload.m.js.map +1 -1
  6. package/package.json +11 -20
  7. package/src/capabilities/crafting.ts +22 -0
  8. package/src/capabilities/gathering.ts +36 -0
  9. package/src/capabilities/guards.ts +38 -0
  10. package/src/capabilities/hauling.ts +22 -0
  11. package/src/capabilities/index.ts +8 -0
  12. package/src/capabilities/loading.ts +8 -0
  13. package/src/capabilities/modules.ts +86 -0
  14. package/src/capabilities/movement.ts +29 -0
  15. package/src/capabilities/storage.ts +159 -0
  16. package/src/contracts/server.ts +1389 -285
  17. package/src/data/capabilities.ts +408 -0
  18. package/src/data/catalog.ts +135 -0
  19. package/src/data/categories.ts +55 -0
  20. package/src/data/colors.ts +84 -0
  21. package/src/data/entities.json +50 -0
  22. package/src/data/item-ids.ts +75 -0
  23. package/src/data/items.json +252 -0
  24. package/src/data/locations.ts +53 -0
  25. package/src/data/metadata.ts +208 -0
  26. package/src/data/nebula-adjectives.json +211 -0
  27. package/src/data/nebula-nouns.json +151 -0
  28. package/src/data/recipes-runtime.ts +65 -0
  29. package/src/data/recipes.json +878 -0
  30. package/src/data/syllables.json +1790 -0
  31. package/src/data/tiers.ts +45 -0
  32. package/src/derivation/crafting.ts +350 -0
  33. package/src/derivation/index.ts +32 -0
  34. package/src/derivation/location-size.ts +15 -0
  35. package/src/derivation/resources.ts +112 -0
  36. package/src/derivation/stats.ts +146 -0
  37. package/src/derivation/strata.ts +43 -0
  38. package/src/derivation/stratum.ts +134 -0
  39. package/src/derivation/tiers.ts +54 -0
  40. package/src/entities/cargo-utils.ts +84 -0
  41. package/src/entities/container.ts +108 -0
  42. package/src/entities/entity-inventory.ts +39 -0
  43. package/src/entities/gamestate.ts +152 -0
  44. package/src/entities/inventory-accessor.ts +42 -0
  45. package/src/entities/location.ts +60 -0
  46. package/src/entities/makers.ts +196 -0
  47. package/src/entities/player.ts +15 -0
  48. package/src/entities/ship-deploy.ts +258 -0
  49. package/src/entities/ship.ts +204 -0
  50. package/src/entities/warehouse.ts +119 -0
  51. package/src/errors.ts +100 -9
  52. package/src/format.ts +12 -0
  53. package/src/index-module.ts +317 -7
  54. package/src/managers/actions.ts +250 -0
  55. package/src/managers/base.ts +25 -0
  56. package/src/managers/context.ts +114 -0
  57. package/src/managers/entities.ts +103 -0
  58. package/src/managers/epochs.ts +47 -0
  59. package/src/managers/index.ts +9 -0
  60. package/src/managers/locations.ts +68 -0
  61. package/src/managers/players.ts +13 -0
  62. package/src/nft/description.ts +176 -0
  63. package/src/nft/deserializers.ts +83 -0
  64. package/src/nft/index.ts +2 -0
  65. package/src/resolution/describe-module.ts +166 -0
  66. package/src/resolution/display-name.ts +39 -0
  67. package/src/resolution/resolve-item.ts +358 -0
  68. package/src/scheduling/accessor.ts +82 -0
  69. package/src/{epoch.ts → scheduling/epoch.ts} +1 -1
  70. package/src/scheduling/projection.ts +463 -0
  71. package/src/scheduling/schedule.ts +179 -0
  72. package/src/shipload.ts +47 -160
  73. package/src/subscriptions/connection.ts +154 -0
  74. package/src/subscriptions/debug.ts +17 -0
  75. package/src/subscriptions/index.ts +5 -0
  76. package/src/subscriptions/manager.ts +240 -0
  77. package/src/subscriptions/mappers.ts +28 -0
  78. package/src/subscriptions/types.ts +143 -0
  79. package/src/travel/travel.ts +500 -0
  80. package/src/types/capabilities.ts +76 -0
  81. package/src/types/entity-traits.ts +69 -0
  82. package/src/types/entity.ts +39 -0
  83. package/src/types/index.ts +3 -0
  84. package/src/types.ts +140 -35
  85. package/src/{hash.ts → utils/hash.ts} +2 -2
  86. package/src/utils/system.ts +168 -0
  87. package/src/goods.ts +0 -124
  88. package/src/market.ts +0 -214
  89. package/src/rolls.ts +0 -8
  90. package/src/ship.ts +0 -36
  91. package/src/state.ts +0 -0
  92. package/src/syllables.ts +0 -1184
  93. package/src/system.ts +0 -37
  94. package/src/travel.ts +0 -259
package/src/shipload.ts CHANGED
@@ -1,30 +1,22 @@
1
- import {
2
- APIClient,
3
- Bytes,
4
- Checksum256,
5
- Name,
6
- NameType,
7
- Serializer,
8
- UInt16Type,
9
- UInt64,
10
- UInt64Type,
11
- } from '@wharfkit/antelope'
12
- import {Coordinates, Distance, GoodPrice} from './types'
13
- import {marketprice, marketprices} from './market'
1
+ import {APIClient} from '@wharfkit/antelope'
14
2
  import {PlatformContract, ServerContract} from './contracts'
15
- import {ERROR_SYSTEM_NOT_INITIALIZED} from './errors'
16
- import {ChainDefinition} from '@wharfkit/session'
17
- import ContractKit, {Contract} from '@wharfkit/contract'
18
- import {findNearbyPlanets, travelplan} from './travel'
19
-
20
- import {Ship} from './ship'
21
- import {EpochInfo, getCurrentEpoch, getEpochInfo} from './epoch'
22
- import {hasSystem} from './system'
3
+ import type {ChainDefinition} from '@wharfkit/common'
4
+ import ContractKit, {type Contract} from '@wharfkit/contract'
5
+
6
+ import {GameContext} from './managers/context'
7
+ import type {EntitiesManager} from './managers/entities'
8
+ import type {PlayersManager} from './managers/players'
9
+ import type {LocationsManager} from './managers/locations'
10
+ import type {EpochsManager} from './managers/epochs'
11
+ import type {ActionsManager} from './managers/actions'
12
+ import type {SubscriptionsManager} from './subscriptions/manager'
13
+ import type {GameState} from './entities/gamestate'
23
14
 
24
15
  interface ShiploadOptions {
25
16
  platformContractName?: string
26
17
  serverContractName?: string
27
18
  client?: APIClient
19
+ subscriptionsUrl?: string
28
20
  }
29
21
 
30
22
  interface ShiploadConstructorOptions extends ShiploadOptions {
@@ -33,22 +25,25 @@ interface ShiploadConstructorOptions extends ShiploadOptions {
33
25
  }
34
26
 
35
27
  export class Shipload {
36
- public client: APIClient
37
- public server: Contract
38
- public platform: Contract
39
- public game: PlatformContract.Types.game_row | undefined
28
+ private readonly _context: GameContext
40
29
 
41
30
  constructor(chain: ChainDefinition, constructorOptions?: ShiploadConstructorOptions) {
42
31
  const {client, platformContract, serverContract} = constructorOptions || {}
43
- this.client = client || new APIClient({url: chain.url})
32
+ const apiClient = client || new APIClient({url: chain.url})
44
33
 
45
- this.platform = platformContract
34
+ const platform = platformContract
46
35
  ? platformContract
47
- : new PlatformContract.Contract({client: this.client})
36
+ : new PlatformContract.Contract({client: apiClient})
48
37
 
49
- this.server = serverContract
38
+ const server = serverContract
50
39
  ? serverContract
51
- : new ServerContract.Contract({client: this.client})
40
+ : new ServerContract.Contract({client: apiClient})
41
+
42
+ this._context = new GameContext(apiClient, server, platform)
43
+
44
+ if (constructorOptions?.subscriptionsUrl) {
45
+ this._context.setSubscriptionsUrl(constructorOptions.subscriptionsUrl)
46
+ }
52
47
  }
53
48
 
54
49
  static async load(
@@ -80,155 +75,47 @@ export class Shipload {
80
75
  })
81
76
  }
82
77
 
83
- async getGame(reload = false): Promise<PlatformContract.Types.game_row> {
84
- if (!reload && this.game) {
85
- return this.game
86
- }
87
- const game = await this.platform.table('games').get()
88
- if (!game) {
89
- throw new Error(ERROR_SYSTEM_NOT_INITIALIZED)
90
- }
91
- this.game = game
92
- return game
78
+ get client(): APIClient {
79
+ return this._context.client
93
80
  }
94
81
 
95
- async getState(): Promise<ServerContract.Types.state_row> {
96
- const state = await this.server.table('state').get()
97
- if (!state) {
98
- throw new Error(ERROR_SYSTEM_NOT_INITIALIZED)
99
- }
100
- return state
82
+ get server(): Contract {
83
+ return this._context.server
101
84
  }
102
85
 
103
- async getShip(ship_id: UInt64Type): Promise<Ship> {
104
- const ship = await this.server.table('ship').get(UInt64.from(ship_id))
105
- if (!ship) {
106
- throw new Error('No ship found')
107
- }
108
- return new Ship(ship)
86
+ get platform(): Contract {
87
+ return this._context.platform
109
88
  }
110
89
 
111
- async getShips(player: NameType | ServerContract.Types.player_row): Promise<Ship[]> {
112
- let account: Name
113
- if (player instanceof ServerContract.Types.player_row) {
114
- account = player.owner
115
- } else {
116
- account = Name.from(player)
117
- }
118
- const from = Serializer.decode({
119
- data:
120
- Serializer.encode({object: UInt64.from(UInt64.min)}).hexString +
121
- Serializer.encode({object: Name.from(account)}).hexString,
122
- type: 'uint128',
123
- })
124
- const to = Serializer.decode({
125
- data:
126
- Serializer.encode({object: UInt64.from(UInt64.max)}).hexString +
127
- Serializer.encode({object: Name.from(account)}).hexString,
128
- type: 'uint128',
129
- })
130
- const ships = await this.server
131
- .table('ship')
132
- .query({
133
- key_type: 'i128',
134
- index_position: 'secondary',
135
- from,
136
- to,
137
- })
138
- .all()
139
- return ships.map((ship) => new Ship(ship))
90
+ get entities(): EntitiesManager {
91
+ return this._context.entities
140
92
  }
141
93
 
142
- async marketprice(
143
- location: ServerContract.ActionParams.Type.coordinates,
144
- good_id: number
145
- ): Promise<GoodPrice> {
146
- const game = await this.getGame()
147
- const state = await this.getState()
148
- return marketprice(location, good_id, game.config.seed, state)
94
+ get players(): PlayersManager {
95
+ return this._context.players
149
96
  }
150
97
 
151
- async marketprices(
152
- location: ServerContract.ActionParams.Type.coordinates
153
- ): Promise<GoodPrice[]> {
154
- const game = await this.getGame()
155
- const state = await this.getState()
156
- return marketprices(location, game.config.seed, state)
98
+ get locations(): LocationsManager {
99
+ return this._context.locations
157
100
  }
158
101
 
159
- async hasSystem(location: ServerContract.ActionParams.Type.coordinates): Promise<boolean> {
160
- const game = await this.getGame()
161
- return hasSystem(game.config.seed, location)
102
+ get epochs(): EpochsManager {
103
+ return this._context.epochs
162
104
  }
163
105
 
164
- async findNearbyPlanets(
165
- origin: ServerContract.ActionParams.Type.coordinates,
166
- maxDistance: UInt16Type = 20
167
- ): Promise<Distance[]> {
168
- const game = await this.getGame()
169
- return findNearbyPlanets(game.config.seed, origin, maxDistance)
106
+ get actions(): ActionsManager {
107
+ return this._context.actions
170
108
  }
171
109
 
172
- async travelplan(
173
- ship: ServerContract.Types.ship_row,
174
- origin: ServerContract.ActionParams.Type.coordinates,
175
- destination: ServerContract.ActionParams.Type.coordinates,
176
- recharge = false
177
- ): Promise<ServerContract.Types.travel_plan> {
178
- const game = await this.getGame()
179
- const cargos = await this.server.table('cargo').all({
180
- from: ship.id,
181
- to: ship.id,
182
- index_position: 'secondary',
183
- })
184
- return travelplan(game, ship, cargos, origin, destination, recharge)
185
- }
186
-
187
- async getCargo(
188
- ship: UInt64Type | ServerContract.Types.ship_row
189
- ): Promise<ServerContract.Types.cargo_row[]> {
190
- let shipId: UInt64
191
- if (ship instanceof ServerContract.Types.ship_row) {
192
- shipId = UInt64.from(ship.id)
193
- } else {
194
- shipId = UInt64.from(ship)
195
- }
196
-
197
- const cargoItems = await this.server
198
- .table('cargo')
199
- .query({
200
- key_type: 'i64',
201
- index_position: 'secondary',
202
- from: shipId,
203
- to: shipId,
204
- })
205
- .all()
206
-
207
- return cargoItems
110
+ get subscriptions(): SubscriptionsManager {
111
+ return this._context.subscriptions
208
112
  }
209
113
 
210
- async getCurrentEpochHeight(): Promise<UInt64> {
211
- const game = await this.getGame()
212
- return getCurrentEpoch(game)
213
- }
214
-
215
- async getCurrentEpoch(): Promise<EpochInfo> {
216
- const game = await this.getGame()
217
- const epoch = await this.getCurrentEpochHeight()
218
- return getEpochInfo(game, epoch)
219
- }
220
-
221
- async getEpoch(height: UInt64Type): Promise<EpochInfo> {
222
- const game = await this.getGame()
223
- return getEpochInfo(game, UInt64.from(height))
114
+ async getGame(reload = false): Promise<PlatformContract.Types.game_row> {
115
+ return this._context.getGame(reload)
224
116
  }
225
117
 
226
- async getLocation(location: Coordinates) {
227
- const hash = Checksum256.hash(Bytes.from(`${location.x}-${location.y}`, 'utf8'))
228
- return this.server.table('location').all({
229
- index_position: 'secondary',
230
- from: hash,
231
- to: hash,
232
- })
118
+ async getState(reload = false): Promise<GameState> {
119
+ return this._context.getState(reload)
233
120
  }
234
121
  }
@@ -0,0 +1,154 @@
1
+ import type {ClientMessage, ServerMessage} from './types'
2
+ import {debug} from './debug'
3
+
4
+ export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting'
5
+
6
+ export interface WebSocketConnectionOptions {
7
+ url: string
8
+ onMessage: (message: ServerMessage) => void
9
+ onStateChange?: (state: ConnectionState) => void
10
+ }
11
+
12
+ export class WebSocketConnection {
13
+ private ws: WebSocket | null = null
14
+ private url: string
15
+ private onMessage: (message: ServerMessage) => void
16
+ private onStateChange?: (state: ConnectionState) => void
17
+ private reconnectAttempts = 0
18
+ private reconnectTimeout: ReturnType<typeof setTimeout> | null = null
19
+ private _state: ConnectionState = 'disconnected'
20
+ private shouldReconnect = true
21
+ private sendQueue: string[] = []
22
+
23
+ private static readonly MIN_RECONNECT_DELAY = 1000
24
+ private static readonly MAX_RECONNECT_DELAY = 30000
25
+ private static readonly RECONNECT_MULTIPLIER = 2
26
+
27
+ constructor(options: WebSocketConnectionOptions) {
28
+ this.url = options.url
29
+ this.onMessage = options.onMessage
30
+ this.onStateChange = options.onStateChange
31
+ }
32
+
33
+ get state(): ConnectionState {
34
+ return this._state
35
+ }
36
+
37
+ private setState(state: ConnectionState) {
38
+ if (this._state !== state) {
39
+ this._state = state
40
+ this.onStateChange?.(state)
41
+ }
42
+ }
43
+
44
+ connect() {
45
+ if (this.ws) {
46
+ return
47
+ }
48
+
49
+ this.shouldReconnect = true
50
+ this.setState('connecting')
51
+ debug('Connecting to', this.url)
52
+
53
+ try {
54
+ this.ws = new WebSocket(this.url)
55
+
56
+ this.ws.onopen = () => {
57
+ debug('Connected')
58
+ this.reconnectAttempts = 0
59
+ this.setState('connected')
60
+ while (
61
+ this.sendQueue.length > 0 &&
62
+ this.ws &&
63
+ this.ws.readyState === WebSocket.OPEN
64
+ ) {
65
+ this.ws.send(this.sendQueue.shift()!)
66
+ }
67
+ }
68
+
69
+ this.ws.onmessage = (event) => {
70
+ try {
71
+ const message = JSON.parse(event.data) as ServerMessage
72
+ this.onMessage(message)
73
+ } catch (e) {
74
+ // eslint-disable-next-line no-console
75
+ console.error('[WS] Failed to parse message:', e)
76
+ }
77
+ }
78
+
79
+ this.ws.onclose = () => {
80
+ this.ws = null
81
+ this.sendQueue.length = 0
82
+
83
+ if (this.shouldReconnect) {
84
+ this.setState('reconnecting')
85
+ this.scheduleReconnect()
86
+ } else {
87
+ this.setState('disconnected')
88
+ }
89
+ }
90
+ } catch (e) {
91
+ // eslint-disable-next-line no-console
92
+ console.error('[WS] Failed to create connection:', e)
93
+ this.ws = null
94
+ if (this.shouldReconnect) {
95
+ this.setState('reconnecting')
96
+ this.scheduleReconnect()
97
+ }
98
+ }
99
+ }
100
+
101
+ private scheduleReconnect() {
102
+ if (this.reconnectTimeout) {
103
+ return
104
+ }
105
+
106
+ const delay = Math.min(
107
+ WebSocketConnection.MIN_RECONNECT_DELAY *
108
+ WebSocketConnection.RECONNECT_MULTIPLIER ** this.reconnectAttempts,
109
+ WebSocketConnection.MAX_RECONNECT_DELAY
110
+ )
111
+
112
+ debug(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1})`)
113
+
114
+ this.reconnectTimeout = setTimeout(() => {
115
+ this.reconnectTimeout = null
116
+ this.reconnectAttempts++
117
+ this.connect()
118
+ }, delay)
119
+ }
120
+
121
+ disconnect() {
122
+ this.shouldReconnect = false
123
+
124
+ if (this.reconnectTimeout) {
125
+ clearTimeout(this.reconnectTimeout)
126
+ this.reconnectTimeout = null
127
+ }
128
+
129
+ if (this.ws) {
130
+ this.ws.close()
131
+ this.ws = null
132
+ }
133
+
134
+ this.sendQueue.length = 0
135
+ this.setState('disconnected')
136
+ }
137
+
138
+ close() {
139
+ this.disconnect()
140
+ }
141
+
142
+ send(message: ClientMessage) {
143
+ const data = JSON.stringify(message)
144
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
145
+ this.ws.send(data)
146
+ return
147
+ }
148
+ this.sendQueue.push(data)
149
+ }
150
+
151
+ get isConnected(): boolean {
152
+ return this._state === 'connected'
153
+ }
154
+ }
@@ -0,0 +1,17 @@
1
+ /* eslint-disable no-console */
2
+
3
+ let enabled = false
4
+
5
+ export function setSubscriptionsDebug(on: boolean): void {
6
+ enabled = on
7
+ }
8
+
9
+ export function isSubscriptionsDebugEnabled(): boolean {
10
+ return enabled
11
+ }
12
+
13
+ export function debug(...args: unknown[]): void {
14
+ if (enabled) {
15
+ console.log('[WS]', ...args)
16
+ }
17
+ }
@@ -0,0 +1,5 @@
1
+ export * from './connection'
2
+ export * from './types'
3
+ export * from './manager'
4
+ export * from './mappers'
5
+ export {setSubscriptionsDebug, isSubscriptionsDebugEnabled} from './debug'
@@ -0,0 +1,240 @@
1
+ import {WebSocketConnection} from './connection'
2
+ import type {
3
+ BoundingBox,
4
+ BoundsDeltaMessage,
5
+ ClientMessage,
6
+ ServerMessage,
7
+ SnapshotMessage,
8
+ SubscribeEntityMessage,
9
+ SubscribeMessage,
10
+ UnsubscribeEntityMessage,
11
+ UpdateBoundsMessage,
12
+ UpdateMessage,
13
+ WireEntity,
14
+ } from './types'
15
+ import {mapEntity, parseWireEntity} from './mappers'
16
+ import type {Ship} from '../entities/ship'
17
+ import type {Warehouse} from '../entities/warehouse'
18
+ import type {Container} from '../entities/container'
19
+
20
+ export type SubscriptionEntityType = 'ship' | 'warehouse' | 'container'
21
+ export type EntityInstance = Ship | Warehouse | Container
22
+
23
+ export interface SubscriptionsOptions {
24
+ url: string
25
+ }
26
+
27
+ export interface BoundsSubscriptionHandle {
28
+ readonly subId: string
29
+ unsubscribe(): void
30
+ updateBounds(bounds: BoundingBox): void
31
+ current: Map<number, EntityInstance>
32
+ }
33
+
34
+ export interface EntitySubscriptionHandle {
35
+ readonly subId: string
36
+ readonly entityType: SubscriptionEntityType
37
+ readonly entityId: string
38
+ unsubscribe(): void
39
+ current: EntityInstance | null
40
+ }
41
+
42
+ export class SubscriptionsManager {
43
+ private readonly conn: WebSocketConnection
44
+ private readonly entitySubs = new Map<
45
+ string,
46
+ {
47
+ type: SubscriptionEntityType
48
+ id: string
49
+ onUpdate: (e: EntityInstance) => void
50
+ handle: EntitySubscriptionHandle
51
+ }
52
+ >()
53
+ private readonly boundsSubs = new Map<
54
+ string,
55
+ {
56
+ onSnapshot?: (entities: EntityInstance[]) => void
57
+ onUpdate?: (entity: EntityInstance) => void
58
+ onBoundsDelta?: (entered: EntityInstance[], exited: number[]) => void
59
+ handle: BoundsSubscriptionHandle
60
+ }
61
+ >()
62
+ private subCounter = 0
63
+
64
+ constructor(opts: SubscriptionsOptions) {
65
+ this.conn = new WebSocketConnection({
66
+ url: opts.url,
67
+ onMessage: (m) => this.onMessage(m),
68
+ })
69
+ this.conn.connect()
70
+ }
71
+
72
+ close() {
73
+ this.conn.close()
74
+ }
75
+
76
+ private generateSubID(prefix: string): string {
77
+ this.subCounter += 1
78
+ return `${prefix}-${this.subCounter}-${Math.random().toString(36).slice(2, 8)}`
79
+ }
80
+
81
+ private sendMessage(msg: ClientMessage) {
82
+ this.conn.send(msg)
83
+ }
84
+
85
+ subscribeEntity(
86
+ type: SubscriptionEntityType,
87
+ id: string,
88
+ onUpdate: (e: EntityInstance) => void
89
+ ): EntitySubscriptionHandle {
90
+ const subId = this.generateSubID('ent')
91
+ const msg: SubscribeEntityMessage = {
92
+ type: 'subscribe_entity',
93
+ sub_id: subId,
94
+ entity_type: type,
95
+ entity_id: id,
96
+ }
97
+ const handle: EntitySubscriptionHandle = {
98
+ subId,
99
+ entityType: type,
100
+ entityId: id,
101
+ unsubscribe: () => this.unsubscribeEntity(subId),
102
+ current: null,
103
+ }
104
+ this.entitySubs.set(subId, {type, id, onUpdate, handle})
105
+ this.sendMessage(msg)
106
+ return handle
107
+ }
108
+
109
+ private unsubscribeEntity(subId: string) {
110
+ const entry = this.entitySubs.get(subId)
111
+ if (!entry) return
112
+ this.entitySubs.delete(subId)
113
+ const msg: UnsubscribeEntityMessage = {type: 'unsubscribe_entity', sub_id: subId}
114
+ this.sendMessage(msg)
115
+ }
116
+
117
+ subscribeBounds(
118
+ bounds: BoundingBox,
119
+ handlers: {
120
+ onSnapshot?: (entities: EntityInstance[]) => void
121
+ onUpdate?: (entity: EntityInstance) => void
122
+ onBoundsDelta?: (entered: EntityInstance[], exited: number[]) => void
123
+ owner?: string
124
+ prioritizeOwner?: string
125
+ }
126
+ ): BoundsSubscriptionHandle {
127
+ const subId = this.generateSubID('bnd')
128
+ const msg: SubscribeMessage = {
129
+ type: 'subscribe',
130
+ sub_id: subId,
131
+ bounds,
132
+ owner: handlers.owner,
133
+ prioritize_owner: handlers.prioritizeOwner,
134
+ }
135
+ const handle: BoundsSubscriptionHandle = {
136
+ subId,
137
+ unsubscribe: () => this.unsubscribeBounds(subId),
138
+ updateBounds: (b) => this.updateBounds(subId, b),
139
+ current: new Map(),
140
+ }
141
+ this.boundsSubs.set(subId, {
142
+ onSnapshot: handlers.onSnapshot,
143
+ onUpdate: handlers.onUpdate,
144
+ onBoundsDelta: handlers.onBoundsDelta,
145
+ handle,
146
+ })
147
+ this.sendMessage(msg)
148
+ return handle
149
+ }
150
+
151
+ private unsubscribeBounds(subId: string) {
152
+ this.boundsSubs.delete(subId)
153
+ this.sendMessage({type: 'unsubscribe', sub_id: subId})
154
+ }
155
+
156
+ private updateBounds(subId: string, bounds: BoundingBox) {
157
+ const msg: UpdateBoundsMessage = {type: 'update_bounds', sub_id: subId, bounds}
158
+ this.sendMessage(msg)
159
+ }
160
+
161
+ private onMessage(msg: ServerMessage) {
162
+ switch (msg.type) {
163
+ case 'snapshot':
164
+ this.handleSnapshot(msg)
165
+ break
166
+ case 'update':
167
+ this.handleUpdate(msg)
168
+ break
169
+ case 'bounds_delta':
170
+ this.handleBoundsDelta(msg)
171
+ break
172
+ case 'error':
173
+ this.handleError(msg)
174
+ break
175
+ }
176
+ }
177
+
178
+ private parseEntity(raw: WireEntity): EntityInstance {
179
+ const ei = parseWireEntity(raw)
180
+ return mapEntity(ei)
181
+ }
182
+
183
+ private handleSnapshot(msg: SnapshotMessage) {
184
+ const entSub = this.entitySubs.get(msg.sub_id)
185
+ if (entSub) {
186
+ if (msg.entities.length > 0) {
187
+ const ent = this.parseEntity(msg.entities[0])
188
+ entSub.handle.current = ent
189
+ entSub.onUpdate(ent)
190
+ }
191
+ return
192
+ }
193
+ const boundsSub = this.boundsSubs.get(msg.sub_id)
194
+ if (boundsSub) {
195
+ const ents = msg.entities.map((e) => this.parseEntity(e))
196
+ boundsSub.handle.current.clear()
197
+ for (const e of ents) boundsSub.handle.current.set(Number(e.id), e)
198
+ boundsSub.onSnapshot?.(ents)
199
+ }
200
+ }
201
+
202
+ private handleUpdate(msg: UpdateMessage) {
203
+ const ent = this.parseEntity(msg.entity)
204
+ for (const subId of msg.sub_ids) {
205
+ const entSub = this.entitySubs.get(subId)
206
+ if (entSub) {
207
+ entSub.handle.current = ent
208
+ entSub.onUpdate(ent)
209
+ continue
210
+ }
211
+ const boundsSub = this.boundsSubs.get(subId)
212
+ if (boundsSub) {
213
+ boundsSub.handle.current.set(msg.entity_id, ent)
214
+ boundsSub.onUpdate?.(ent)
215
+ }
216
+ }
217
+ }
218
+
219
+ private handleBoundsDelta(msg: BoundsDeltaMessage) {
220
+ const sub = this.boundsSubs.get(msg.sub_id)
221
+ if (!sub) return
222
+ const entered = msg.entered.map((e) => this.parseEntity(e))
223
+ for (const e of entered) sub.handle.current.set(Number(e.id), e)
224
+ for (const id of msg.exited) sub.handle.current.delete(id)
225
+ sub.onBoundsDelta?.(entered, msg.exited)
226
+ }
227
+
228
+ private handleError(msg: {sub_id?: string; error: string}) {
229
+ if (!msg.sub_id) return
230
+ const entSub = this.entitySubs.get(msg.sub_id)
231
+ if (entSub) {
232
+ this.entitySubs.delete(msg.sub_id)
233
+ return
234
+ }
235
+ const boundsSub = this.boundsSubs.get(msg.sub_id)
236
+ if (boundsSub) {
237
+ this.boundsSubs.delete(msg.sub_id)
238
+ }
239
+ }
240
+ }