@shipload/sdk 1.0.0-next.73 → 1.0.0-next.75

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 (42) hide show
  1. package/lib/shipload.d.ts +514 -127
  2. package/lib/shipload.js +5952 -4948
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +5936 -4942
  5. package/lib/shipload.m.js.map +1 -1
  6. package/lib/testing.d.ts +174 -47
  7. package/lib/testing.js +539 -147
  8. package/lib/testing.js.map +1 -1
  9. package/lib/testing.m.js +539 -147
  10. package/lib/testing.m.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/contracts/server.ts +457 -134
  13. package/src/data/component-process.test.ts +119 -0
  14. package/src/data/component-process.ts +33 -0
  15. package/src/data/entities.json +2 -2
  16. package/src/data/metadata.ts +18 -5
  17. package/src/index-module.ts +29 -2
  18. package/src/influence/civic-shuttle.test.ts +58 -0
  19. package/src/influence/civic-shuttle.ts +52 -6
  20. package/src/influence/duration.ts +0 -65
  21. package/src/influence/index.ts +4 -5
  22. package/src/influence/influence.test.ts +51 -136
  23. package/src/influence/valuation.ts +75 -0
  24. package/src/managers/actions.ts +37 -10
  25. package/src/managers/balances.test.ts +33 -0
  26. package/src/managers/balances.ts +15 -0
  27. package/src/managers/context.ts +50 -11
  28. package/src/managers/index.ts +14 -0
  29. package/src/managers/jobs.test.ts +82 -1
  30. package/src/managers/jobs.ts +41 -2
  31. package/src/managers/nft.test.ts +173 -1
  32. package/src/managers/nft.ts +35 -2
  33. package/src/managers/shuttle.test.ts +562 -0
  34. package/src/managers/shuttle.ts +397 -0
  35. package/src/resolution/resolve-item.ts +4 -0
  36. package/src/scheduling/availability.ts +10 -0
  37. package/src/scheduling/energy.ts +55 -1
  38. package/src/scheduling/jobs.test.ts +171 -1
  39. package/src/scheduling/jobs.ts +110 -13
  40. package/src/scheduling/projection.ts +2 -1
  41. package/src/scheduling/task-effects.ts +38 -0
  42. package/src/shipload.ts +23 -4
@@ -0,0 +1,397 @@
1
+ import {
2
+ Name,
3
+ UInt16,
4
+ UInt32,
5
+ UInt64,
6
+ type NameType,
7
+ type UInt16Type,
8
+ type UInt32Type,
9
+ type UInt64Type,
10
+ } from '@wharfkit/antelope'
11
+ import {BaseManager} from './base'
12
+ import {ServerContract} from '../contracts'
13
+ import {getItem} from '../data/catalog'
14
+ import {ENTITY_DEPOT} from '../data/kind-registry'
15
+ import {isCivicEntity} from '../influence/civic'
16
+ import {projectEntity} from '../scheduling/projection'
17
+
18
+ export type ShuttleMode = 'internal' | 'own' | 'ship' | 'bays'
19
+
20
+ export type ShuttleReasonCode =
21
+ | 'workshop-capped'
22
+ | 'no-generator'
23
+ | 'not-equipped'
24
+ | 'job-cap'
25
+ | 'target-busy'
26
+ | 'cargo-wont-fit'
27
+ | 'depot-full'
28
+ | 'not-stored'
29
+ | 'no-storage'
30
+ | 'bays-booked'
31
+ | 'player-cap'
32
+ | 'queue-full'
33
+ | 'inputs-unavailable'
34
+ | 'ship-energy'
35
+ | 'dropoff-collision'
36
+ | 'ship-capped'
37
+ | 'departs'
38
+ | 'cargo-not-aboard'
39
+ | 'no-capacity'
40
+ | 'schedule-full'
41
+ | 'plot-recipe'
42
+ | 'not-ready'
43
+ | 'unknown'
44
+
45
+ export interface ShuttleReason {
46
+ code: ShuttleReasonCode
47
+ reason: string
48
+ at?: Date
49
+ until?: Date
50
+ have?: number
51
+ need?: number
52
+ cap?: number
53
+ taskType?: number
54
+ }
55
+
56
+ export interface ShuttleOption {
57
+ shuttledBy?: string
58
+ mode: ShuttleMode
59
+ hostId: string
60
+ laneKey: number
61
+ duration: number
62
+ resolved: boolean
63
+ start?: Date
64
+ finish?: Date
65
+ bays: number
66
+ window?: {startsAt: Date; completesAt: Date}
67
+ energyCost?: number
68
+ blocked?: ShuttleReason
69
+ }
70
+
71
+ export interface ShuttleOptions {
72
+ blocked?: ShuttleReason
73
+ options: ShuttleOption[]
74
+ auto?: ShuttleOption
75
+ }
76
+
77
+ // contract strings verbatim from errors.hpp and placement.hpp
78
+ const REASON_CODES: Record<string, ShuttleReasonCode> = {
79
+ 'workshop has a pending plan-capper; cannot accept new jobs': 'workshop-capped',
80
+ 'ship needs an energy source to book a craft job': 'no-generator',
81
+ 'entity cannot recharge': 'no-generator',
82
+ 'workshop has no fabricator installed': 'not-equipped',
83
+ 'dock has no assembly arm installed': 'not-equipped',
84
+ 'too many bookings waiting on materials at this building': 'job-cap',
85
+ 'upgrade target is busy': 'target-busy',
86
+ 'Entity must be idle to modify modules.': 'target-busy',
87
+ 'target cargo would not fit the upgraded capacity': 'cargo-wont-fit',
88
+ 'player storage allowance at this depot is exceeded': 'depot-full',
89
+ 'player has no such item stored at this depot': 'not-stored',
90
+ 'player has fewer of that item stored at this depot': 'not-stored',
91
+ 'entity has no storage': 'no-storage',
92
+ 'target entity has no storage': 'no-storage',
93
+ "the building's shuttle bays are fully booked": 'bays-booked',
94
+ 'player already has the most transfers this building takes': 'player-cap',
95
+ 'fabricator queue is full': 'queue-full',
96
+ 'Cannot unload cargo that is not loaded.': 'inputs-unavailable',
97
+ 'Insufficient inputs for recipe.': 'inputs-unavailable',
98
+ 'Cargo debit exceeds available quantity.': 'inputs-unavailable',
99
+ 'Craft requires more energy than entity has.': 'ship-energy',
100
+ 'identical Workshop delivery already scheduled at this time; finish that delivery or change the shipment':
101
+ 'dropoff-collision',
102
+ 'cannot append: schedule is capped by a pending plan-capper': 'ship-capped',
103
+ 'ship departs before the shuttle completes': 'departs',
104
+ 'giver has insufficient giveable cargo': 'cargo-not-aboard',
105
+ 'Entity cargo capacity would be exceeded.': 'no-capacity',
106
+ 'lane queue is full': 'schedule-full',
107
+ 'No recipe found for plot target.': 'plot-recipe',
108
+ 'Item is not part of the plot target recipe.': 'plot-recipe',
109
+ 'Deposit would exceed plot recipe requirement.': 'plot-recipe',
110
+ 'craft job is not finished yet': 'not-ready',
111
+ }
112
+
113
+ export const BOOKING_LEVEL_CODES: ReadonlySet<ShuttleReasonCode> = new Set([
114
+ 'workshop-capped',
115
+ 'no-generator',
116
+ 'not-equipped',
117
+ 'job-cap',
118
+ 'target-busy',
119
+ 'cargo-wont-fit',
120
+ 'depot-full',
121
+ 'not-stored',
122
+ 'no-storage',
123
+ 'not-ready',
124
+ ])
125
+
126
+ // never-shuttle rejections from resolve_civic_carrier; the query never omits, so the wrapper drops them here
127
+ export const OMITTED_REASONS: ReadonlySet<string> = new Set([
128
+ 'that ship has no shuttle bay',
129
+ 'that ship belongs to another player',
130
+ 'that ship is not here',
131
+ "only a Depot's public shuttle bays can take this",
132
+ "choose the building's own shuttle instead",
133
+ 'counterpart has a pending plan-capper; cannot place a hold on it',
134
+ 'entity not found',
135
+ ])
136
+
137
+ export function shuttleReasonCode(reason: string): ShuttleReasonCode {
138
+ return REASON_CODES[reason] ?? 'unknown'
139
+ }
140
+
141
+ const MODES: ShuttleMode[] = ['internal', 'own', 'ship', 'bays']
142
+
143
+ const modeRank = (o: ShuttleOption) => (o.mode === 'internal' || o.mode === 'own' ? 0 : 1)
144
+
145
+ const finishRank = (o: ShuttleOption) => o.finish?.getTime() ?? Number.POSITIVE_INFINITY
146
+
147
+ export function rankShuttleOptions(a: ShuttleOption, b: ShuttleOption): number {
148
+ const blocked = (o: ShuttleOption) => (o.blocked ? 1 : 0)
149
+ if (blocked(a) !== blocked(b)) return blocked(a) - blocked(b)
150
+ if (finishRank(a) !== finishRank(b)) return finishRank(a) < finishRank(b) ? -1 : 1
151
+ if (modeRank(a) !== modeRank(b)) return modeRank(a) - modeRank(b)
152
+ return Number(BigInt(a.hostId) - BigInt(b.hostId))
153
+ }
154
+
155
+ type RawOptions = ServerContract.Types.shuttle_options
156
+
157
+ function toDate(tp: {toDate(): Date} | undefined): Date | undefined {
158
+ return tp ? tp.toDate() : undefined
159
+ }
160
+
161
+ function wireTime(tp: {toMilliseconds(): number; toDate(): Date}): Date | undefined {
162
+ return tp.toMilliseconds() === 0 ? undefined : tp.toDate()
163
+ }
164
+
165
+ function toNum(v: {toNumber(): number} | undefined): number | undefined {
166
+ return v === undefined ? undefined : v.toNumber()
167
+ }
168
+
169
+ function mapReason(r: ServerContract.Types.shuttle_rejection): ShuttleReason {
170
+ return {
171
+ code: shuttleReasonCode(r.reason),
172
+ reason: r.reason,
173
+ at: toDate(r.at),
174
+ until: toDate(r.until),
175
+ have: toNum(r.have),
176
+ need: toNum(r.need),
177
+ cap: toNum(r.cap),
178
+ taskType: toNum(r.task_type),
179
+ }
180
+ }
181
+
182
+ function mapOptions(raw: RawOptions): ShuttleOptions {
183
+ const options: ShuttleOption[] = []
184
+ for (const o of raw.options) {
185
+ const rejection = o.rejection ? mapReason(o.rejection) : undefined
186
+ if (rejection && OMITTED_REASONS.has(rejection.reason)) continue
187
+ const window =
188
+ o.window_starts_at && o.window_completes_at
189
+ ? {
190
+ startsAt: toDate(o.window_starts_at)!,
191
+ completesAt: toDate(o.window_completes_at)!,
192
+ }
193
+ : undefined
194
+ const start = wireTime(o.start)
195
+ const finish = wireTime(o.finish)
196
+ options.push({
197
+ shuttledBy: o.shuttled_by ? o.shuttled_by.toString() : undefined,
198
+ mode: MODES[o.mode.toNumber()] ?? 'internal',
199
+ hostId: o.host_id.toString(),
200
+ laneKey: o.lane_key.toNumber(),
201
+ duration: o.duration.toNumber(),
202
+ resolved: o.resolved,
203
+ start,
204
+ finish,
205
+ bays: o.bays.toNumber(),
206
+ window,
207
+ energyCost: toNum(o.energy_cost),
208
+ blocked: rejection,
209
+ })
210
+ }
211
+ options.sort(rankShuttleOptions)
212
+ const first = options[0]?.blocked
213
+ const blocked =
214
+ first &&
215
+ BOOKING_LEVEL_CODES.has(first.code) &&
216
+ options.every((o) => o.blocked?.code === first.code)
217
+ ? first
218
+ : undefined
219
+ const auto = blocked ? undefined : options.find((o) => !o.blocked)
220
+ return {blocked, options, auto}
221
+ }
222
+
223
+ export type CandidateEntity = Pick<
224
+ ServerContract.Types.entity_row,
225
+ 'id' | 'owner' | 'kind' | 'coordinates' | 'modules' | 'item_id' | 'lanes'
226
+ >
227
+
228
+ const SHUTTLE_CANDIDATE_CAP = 8
229
+
230
+ function siteOf(entity: CandidateEntity) {
231
+ return projectEntity({...entity, cargo: [], cargomass: UInt32.from(0)}).location
232
+ }
233
+
234
+ function hasLoaderModule(entity: CandidateEntity): boolean {
235
+ return entity.modules.some(
236
+ (m) => m.installed && getItem(m.installed.item_id).moduleType === 'loader'
237
+ )
238
+ }
239
+
240
+ export function shuttleCandidates(
241
+ req: {building: CandidateEntity; shipId: UInt64Type; player: NameType},
242
+ entities: readonly CandidateEntity[],
243
+ civicOwner: NameType
244
+ ): string[] {
245
+ const {building, shipId, player} = req
246
+ const buildingId = UInt64.from(building.id)
247
+ const bookingShipId = UInt64.from(shipId)
248
+ const playerName = Name.from(player)
249
+ const site = siteOf(building)
250
+
251
+ const depots: UInt64[] = []
252
+ const ships: UInt64[] = []
253
+ const seen = new Set<string>()
254
+
255
+ for (const entity of entities) {
256
+ const id = UInt64.from(entity.id)
257
+ if (id.equals(buildingId) || id.equals(bookingShipId)) continue
258
+ if (seen.has(id.toString())) continue
259
+ if (!siteOf(entity).equals(site)) continue
260
+
261
+ if (isCivicEntity(entity, civicOwner)) {
262
+ if (Name.from(entity.kind).equals(ENTITY_DEPOT)) {
263
+ depots.push(id)
264
+ seen.add(id.toString())
265
+ }
266
+ continue
267
+ }
268
+
269
+ if (!Name.from(entity.owner).equals(playerName)) continue
270
+ if (!hasLoaderModule(entity)) continue
271
+ ships.push(id)
272
+ seen.add(id.toString())
273
+ }
274
+
275
+ const byId = (a: UInt64, b: UInt64) => Number(BigInt(a.toString()) - BigInt(b.toString()))
276
+ depots.sort(byId)
277
+ ships.sort(byId)
278
+
279
+ return [...depots, ...ships].slice(0, SHUTTLE_CANDIDATE_CAP).map((id) => id.toString())
280
+ }
281
+
282
+ function chainAssertion(e: unknown): string | undefined {
283
+ const detailsMessage = (e as {details?: {message?: string}[]}).details?.[0]?.message
284
+ const message = detailsMessage ?? (e instanceof Error ? e.message : String(e))
285
+ const match = message.match(/assertion failure with message:\s*(.*)/i)
286
+ return match ? match[1].trim() : undefined
287
+ }
288
+
289
+ export class ShuttleManager extends BaseManager {
290
+ private async query(
291
+ name:
292
+ | 'getcraftopts'
293
+ | 'getbuildopts'
294
+ | 'getclaimopts'
295
+ | 'getcbldopts'
296
+ | 'getstoreopts'
297
+ | 'gettakeopts',
298
+ data: unknown
299
+ ): Promise<ShuttleOptions> {
300
+ let raw: RawOptions
301
+ try {
302
+ raw = (await this.server.readonly(name as never, data as never)) as RawOptions
303
+ } catch (e) {
304
+ const reason = chainAssertion(e)
305
+ if (reason === undefined) throw e
306
+ return {
307
+ blocked: {code: shuttleReasonCode(reason), reason},
308
+ options: [],
309
+ auto: undefined,
310
+ }
311
+ }
312
+ return mapOptions(ServerContract.Types.shuttle_options.from(raw))
313
+ }
314
+
315
+ craft(req: {
316
+ shipId: UInt64Type
317
+ workshopId: UInt64Type
318
+ recipeId: UInt16Type
319
+ quantity: UInt32Type
320
+ inputs: ServerContract.ActionParams.Type.cargo_item[]
321
+ candidates: UInt64Type[]
322
+ recharge?: boolean
323
+ }): Promise<ShuttleOptions> {
324
+ return this.query('getcraftopts', {
325
+ ship_id: UInt64.from(req.shipId),
326
+ workshop_id: UInt64.from(req.workshopId),
327
+ recipe_id: UInt16.from(req.recipeId),
328
+ quantity: UInt32.from(req.quantity),
329
+ inputs: req.inputs,
330
+ candidates: req.candidates.map((c) => UInt64.from(c)),
331
+ recharge: req.recharge ?? false,
332
+ })
333
+ }
334
+
335
+ build(req: {
336
+ targetId: UInt64Type
337
+ dockId: UInt64Type
338
+ targetItemId: UInt16Type
339
+ inputs: ServerContract.ActionParams.Type.cargo_item[]
340
+ candidates: UInt64Type[]
341
+ }): Promise<ShuttleOptions> {
342
+ return this.query('getbuildopts', {
343
+ target_id: UInt64.from(req.targetId),
344
+ dock_id: UInt64.from(req.dockId),
345
+ target_item_id: UInt16.from(req.targetItemId),
346
+ inputs: req.inputs,
347
+ candidates: req.candidates.map((c) => UInt64.from(c)),
348
+ })
349
+ }
350
+
351
+ claim(req: {
352
+ jobId: UInt64Type
353
+ shipId: UInt64Type
354
+ candidates: UInt64Type[]
355
+ }): Promise<ShuttleOptions> {
356
+ return this.query('getclaimopts', {
357
+ job_id: UInt64.from(req.jobId),
358
+ ship_id: UInt64.from(req.shipId),
359
+ candidates: req.candidates.map((c) => UInt64.from(c)),
360
+ })
361
+ }
362
+
363
+ cancelBuild(req: {jobId: UInt64Type; candidates: UInt64Type[]}): Promise<ShuttleOptions> {
364
+ return this.query('getcbldopts', {
365
+ job_id: UInt64.from(req.jobId),
366
+ candidates: req.candidates.map((c) => UInt64.from(c)),
367
+ })
368
+ }
369
+
370
+ store(req: {
371
+ shipId: UInt64Type
372
+ depotId: UInt64Type
373
+ items: ServerContract.ActionParams.Type.cargo_item[]
374
+ candidates: UInt64Type[]
375
+ }): Promise<ShuttleOptions> {
376
+ return this.query('getstoreopts', {
377
+ ship_id: UInt64.from(req.shipId),
378
+ depot_id: UInt64.from(req.depotId),
379
+ items: req.items,
380
+ candidates: req.candidates.map((c) => UInt64.from(c)),
381
+ })
382
+ }
383
+
384
+ take(req: {
385
+ shipId: UInt64Type
386
+ depotId: UInt64Type
387
+ items: ServerContract.ActionParams.Type.cargo_item[]
388
+ candidates: UInt64Type[]
389
+ }): Promise<ShuttleOptions> {
390
+ return this.query('gettakeopts', {
391
+ ship_id: UInt64.from(req.shipId),
392
+ depot_id: UInt64.from(req.depotId),
393
+ items: req.items,
394
+ candidates: req.candidates.map((c) => UInt64.from(c)),
395
+ })
396
+ }
397
+ }
@@ -5,6 +5,8 @@ import {getItem, getModules} from '../data/catalog'
5
5
  import {ENTITY_SHIP, getPackedEntityType} from '../data/kind-registry'
6
6
  import {getEntityLayout} from '../data/recipes-runtime'
7
7
  import {entityMetadata, getItemFamily} from '../data/metadata'
8
+ import type {ComponentProcess} from '../data/metadata'
9
+ import {getComponentProcess} from '../data/component-process'
8
10
  import {
9
11
  getModuleCapabilityType,
10
12
  isModuleItem,
@@ -75,6 +77,7 @@ export interface ResolvedItem {
75
77
  tier: number
76
78
  mass: number
77
79
  itemType: ResolvedItemType
80
+ process?: ComponentProcess
78
81
  stats?: ResolvedItemStat[]
79
82
  attributes?: ResolvedAttributeGroup[]
80
83
  moduleSlots?: ResolvedModuleSlot[]
@@ -149,6 +152,7 @@ function resolveComponent(id: number, stats?: UInt64Type): ResolvedItem {
149
152
  tier: item.tier,
150
153
  mass: item.mass,
151
154
  itemType: 'component',
155
+ process: getComponentProcess(id),
152
156
  stats: resolvedStats,
153
157
  }
154
158
  }
@@ -106,7 +106,17 @@ export function incomingSources(
106
106
  return out
107
107
  }
108
108
 
109
+ // Mirrors for_each_task_cargo's hosted_civic_leg guard: a hosted Drop-off/Pickup moves E's cargo, not the host's.
110
+ export function isHostedCivicLeg(task: Task): boolean {
111
+ const type = task.type.toNumber()
112
+ return (
113
+ (type === TaskType.CIVIC_DEPOSIT || type === TaskType.CIVIC_WITHDRAW) &&
114
+ task.couplings.length > 0
115
+ )
116
+ }
117
+
109
118
  export function taskCargoEffect(task: Task): CargoEffect {
119
+ if (isHostedCivicLeg(task)) return {added: [], removed: []}
110
120
  switch (taskCargoRule(task.type.toNumber())) {
111
121
  case 'all-in':
112
122
  case 'undeploy':
@@ -1,6 +1,10 @@
1
+ import type {ServerContract} from '../contracts'
1
2
  import {TaskType} from '../types'
2
3
  import {createProjectedEntity, type Projectable} from './projection'
3
- import {orderedTasks} from './schedule'
4
+ import {orderedTasks, unappliedTasks} from './schedule'
5
+ import {taskEnergyDrawNeedsCoordinates, taskEnergyEffect} from './task-effects'
6
+
7
+ type Task = ServerContract.Types.task
4
8
 
5
9
  export function energyAtTime(entity: Projectable, now: Date): number {
6
10
  const projected = createProjectedEntity(entity)
@@ -45,3 +49,53 @@ export function energyAtTime(entity: Projectable, now: Date): number {
45
49
 
46
50
  return clamp(running)
47
51
  }
52
+
53
+ // Mirrors calc_task_effect's energy branch (projection.cpp): the step this task's type applies to running energy.
54
+ function applyTaskEnergyEffect(
55
+ taskType: number,
56
+ hasCoordinates: boolean,
57
+ capacity: number | undefined,
58
+ cost: number | undefined,
59
+ energy: number
60
+ ): number {
61
+ const effect = taskEnergyEffect(taskType)
62
+ if (effect === 'fills') return capacity !== undefined ? capacity : energy
63
+ if (effect === 'zeroes') return cost === undefined ? 0 : energy > cost ? energy - cost : 0
64
+ if (cost === undefined) return energy
65
+ if (effect === 'draws' && (!taskEnergyDrawNeedsCoordinates(taskType) || hasCoordinates)) {
66
+ return energy > cost ? energy - cost : 0
67
+ }
68
+ return energy
69
+ }
70
+
71
+ function taskCost(task: Task): number | undefined {
72
+ return task.energy_cost !== undefined ? Number(task.energy_cost) : undefined
73
+ }
74
+
75
+ // Walks tasks in schedule order, gating on funding at each draw; false on the first shortfall.
76
+ function tasksFundedFrom(
77
+ tasks: readonly {task: Task}[],
78
+ capacity: number | undefined,
79
+ baseEnergy: number
80
+ ): boolean {
81
+ let energy = baseEnergy
82
+ for (const {task} of tasks) {
83
+ const cost = taskCost(task)
84
+ if (cost !== undefined && energy < cost) return false
85
+ energy = applyTaskEnergyEffect(
86
+ task.type.toNumber(),
87
+ task.coordinates !== undefined,
88
+ capacity,
89
+ cost,
90
+ energy
91
+ )
92
+ }
93
+ return true
94
+ }
95
+
96
+ // Mirrors walk_pending_energy(require_funded=true) / energy_draws_funded in projection.cpp.
97
+ export function energyDrawsFunded(entity: Projectable, baseEnergy: number): boolean {
98
+ const projected = createProjectedEntity(entity)
99
+ const capacity = projected.generator ? Number(projected.generator.capacity) : undefined
100
+ return tasksFundedFrom(unappliedTasks(entity), capacity, baseEnergy)
101
+ }