@shipload/sdk 1.0.0-next.26 → 1.0.0-next.28

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 (40) hide show
  1. package/lib/shipload.d.ts +201 -86
  2. package/lib/shipload.js +843 -425
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +825 -423
  5. package/lib/shipload.m.js.map +1 -1
  6. package/lib/testing.d.ts +13 -8
  7. package/lib/testing.js +41 -26
  8. package/lib/testing.js.map +1 -1
  9. package/lib/testing.m.js +41 -26
  10. package/lib/testing.m.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/capabilities/craftable.ts +51 -0
  13. package/src/contracts/server.ts +39 -18
  14. package/src/data/capabilities.ts +5 -0
  15. package/src/data/colors.ts +1 -1
  16. package/src/data/item-ids.ts +1 -1
  17. package/src/data/metadata.ts +3 -3
  18. package/src/data/recipes.json +10 -10
  19. package/src/derivation/capabilities.ts +11 -11
  20. package/src/derivation/index.ts +9 -0
  21. package/src/derivation/stars.test.ts +51 -0
  22. package/src/derivation/stars.ts +15 -0
  23. package/src/derivation/stats.ts +5 -4
  24. package/src/entities/entity.ts +1 -1
  25. package/src/entities/makers.ts +15 -6
  26. package/src/index-module.ts +33 -4
  27. package/src/managers/actions.ts +10 -1
  28. package/src/managers/construction.ts +67 -65
  29. package/src/nft/buildImmutableData.ts +13 -11
  30. package/src/nft/description.ts +2 -2
  31. package/src/resolution/resolve-item.ts +2 -2
  32. package/src/scheduling/accessor.ts +65 -23
  33. package/src/scheduling/availability.ts +108 -0
  34. package/src/scheduling/energy.ts +18 -11
  35. package/src/scheduling/lane-core.ts +130 -0
  36. package/src/scheduling/lanes.ts +60 -0
  37. package/src/scheduling/projection.ts +30 -54
  38. package/src/scheduling/schedule.ts +236 -121
  39. package/src/travel/travel.ts +21 -16
  40. package/src/types/capabilities.ts +1 -0
@@ -0,0 +1,108 @@
1
+ import type {ServerContract} from '../contracts'
2
+ import {TaskType} from '../types'
3
+ import * as schedule from './schedule'
4
+
5
+ type Task = ServerContract.Types.task
6
+ type CargoItem = ServerContract.Types.cargo_item
7
+
8
+ export interface CargoEffect {
9
+ added: CargoItem[]
10
+ removed: CargoItem[]
11
+ }
12
+
13
+ export interface AvailabilityInput extends schedule.ScheduleData {
14
+ cargo: CargoItem[]
15
+ }
16
+
17
+ export function taskCargoEffect(task: Task): CargoEffect {
18
+ switch (task.type.toNumber()) {
19
+ case TaskType.LOAD:
20
+ case TaskType.UNWRAP:
21
+ case TaskType.UNDEPLOY:
22
+ return {added: task.cargo, removed: []}
23
+ case TaskType.UNLOAD:
24
+ return {added: [], removed: task.cargo}
25
+ case TaskType.GATHER:
26
+ return task.entitytarget ? {added: [], removed: []} : {added: task.cargo, removed: []}
27
+ case TaskType.CRAFT:
28
+ if (task.cargo.length === 0) return {added: [], removed: []}
29
+ return {added: [task.cargo[task.cargo.length - 1]], removed: task.cargo.slice(0, -1)}
30
+ case TaskType.DEPLOY:
31
+ return task.cargo.length > 0
32
+ ? {added: [], removed: [task.cargo[0]]}
33
+ : {added: [], removed: []}
34
+ default:
35
+ return {added: [], removed: []}
36
+ }
37
+ }
38
+
39
+ function cargoKey(item: CargoItem): string {
40
+ const base = `${item.item_id.toNumber()}:${item.stats.toString()}`
41
+ const modules = item.modules ?? []
42
+ const entityId = item.entity_id?.toString()
43
+ const normalizedEntityId = entityId && entityId !== '0' ? entityId : ''
44
+ if (modules.length === 0 && normalizedEntityId === '') return base
45
+ return `${base}:modules=${JSON.stringify(modules)}:entity=${normalizedEntityId}`
46
+ }
47
+
48
+ function cargoQuantity(item: CargoItem): bigint {
49
+ return BigInt(item.quantity.toString())
50
+ }
51
+
52
+ export function projectedCargoAvailableAt(
53
+ entity: AvailabilityInput,
54
+ at: Date
55
+ ): Map<string, bigint> {
56
+ const avail = new Map<string, bigint>()
57
+
58
+ for (const item of entity.cargo) {
59
+ const key = cargoKey(item)
60
+ avail.set(key, (avail.get(key) ?? 0n) + cargoQuantity(item))
61
+ }
62
+
63
+ // Every scheduled task reserves inputs against the unsettled cargo base, even already-elapsed ones.
64
+ const tasks = schedule.orderedTasks(entity)
65
+
66
+ for (const ordered of tasks) {
67
+ if (ordered.completesAt.getTime() >= at.getTime()) continue
68
+
69
+ for (const item of taskCargoEffect(ordered.task).added) {
70
+ const key = cargoKey(item)
71
+ avail.set(key, (avail.get(key) ?? 0n) + cargoQuantity(item))
72
+ }
73
+ }
74
+
75
+ for (const ordered of tasks) {
76
+ for (const item of taskCargoEffect(ordered.task).removed) {
77
+ const key = cargoKey(item)
78
+ const current = avail.get(key) ?? 0n
79
+ const quantity = cargoQuantity(item)
80
+ avail.set(key, current > quantity ? current - quantity : 0n)
81
+ }
82
+ }
83
+
84
+ return avail
85
+ }
86
+
87
+ // Latest completion among scheduled tasks producing any of the given inputs (a craft starts no earlier).
88
+ export function cargoReadyAt(entity: AvailabilityInput, inputItemIds: readonly number[]): Date {
89
+ let readyMs = 0
90
+ for (const ordered of schedule.orderedTasks(entity)) {
91
+ for (const item of taskCargoEffect(ordered.task).added) {
92
+ if (inputItemIds.includes(item.item_id.toNumber())) {
93
+ readyMs = Math.max(readyMs, ordered.completesAt.getTime())
94
+ break
95
+ }
96
+ }
97
+ }
98
+ return new Date(readyMs)
99
+ }
100
+
101
+ export function availableForItem(avail: Map<string, bigint>, itemId: number): bigint {
102
+ const prefix = `${itemId}:`
103
+ let total = 0n
104
+ for (const [key, quantity] of avail) {
105
+ if (key.startsWith(prefix)) total += quantity
106
+ }
107
+ return total
108
+ }
@@ -1,6 +1,6 @@
1
1
  import {TaskType} from '../types'
2
2
  import {createProjectedEntity, type Projectable} from './projection'
3
- import {currentTaskIndex, currentTaskProgressFloat, isTaskComplete} from './schedule'
3
+ import {orderedTasks} from './schedule'
4
4
 
5
5
  export function energyAtTime(entity: Projectable, now: Date): number {
6
6
  const projected = createProjectedEntity(entity)
@@ -13,24 +13,31 @@ export function energyAtTime(entity: Projectable, now: Date): number {
13
13
 
14
14
  let running = Number(projected.energy)
15
15
 
16
- const tasks = entity.schedule?.tasks
17
- if (!tasks || tasks.length === 0) return clamp(running)
16
+ const ordered = orderedTasks(entity)
17
+ if (ordered.length === 0) return clamp(running)
18
18
 
19
- const activeIndex = currentTaskIndex(entity, now)
20
- const activeProgress = currentTaskProgressFloat(entity, now)
19
+ const nowMs = now.getTime()
21
20
 
22
- for (let i = 0; i < tasks.length; i++) {
23
- const complete = isTaskComplete(entity, i, now)
24
- if (!complete && i !== activeIndex) break
21
+ for (const {task, startsAt} of ordered) {
22
+ const duration = task.duration.toNumber()
23
+ const isReserved = task.type.toNumber() === TaskType.RESERVED
24
+ const elapsed = Math.min(
25
+ Math.max(0, Math.floor((nowMs - startsAt.getTime()) / 1000)),
26
+ duration
27
+ )
28
+ const complete = !isReserved && elapsed >= duration
29
+ const inProgress = !complete && elapsed > 0 && elapsed < duration
25
30
 
26
- const fraction = complete ? 1 : activeProgress
31
+ if (!complete && !inProgress) continue
27
32
 
28
- if (tasks[i].type.toNumber() === TaskType.RECHARGE) {
33
+ const fraction = complete ? 1 : duration === 0 ? 1 : elapsed / duration
34
+
35
+ if (task.type.toNumber() === TaskType.RECHARGE) {
29
36
  if (capacity !== undefined) {
30
37
  running = complete ? capacity : running + (capacity - running) * fraction
31
38
  }
32
39
  } else {
33
- const cost = Number(tasks[i].energy_cost ?? 0)
40
+ const cost = Number(task.energy_cost ?? 0)
34
41
  running -= cost * fraction
35
42
  }
36
43
 
@@ -0,0 +1,130 @@
1
+ import type {ServerContract} from '../contracts'
2
+ import {TaskType} from '../types'
3
+
4
+ type Schedule = ServerContract.Types.schedule
5
+ type Task = ServerContract.Types.task
6
+
7
+ export function laneDuration(schedule: Schedule): number {
8
+ return schedule.tasks.reduce((sum, task) => sum + task.duration.toNumber(), 0)
9
+ }
10
+
11
+ export function laneRawElapsed(schedule: Schedule, now: Date): number {
12
+ const started = schedule.started.toDate()
13
+ return Math.floor((now.getTime() - started.getTime()) / 1000)
14
+ }
15
+
16
+ export function laneElapsed(schedule: Schedule, now: Date): number {
17
+ return Math.max(0, laneRawElapsed(schedule, now))
18
+ }
19
+
20
+ export function laneStartsIn(schedule: Schedule, now: Date): number {
21
+ return Math.max(0, -laneRawElapsed(schedule, now))
22
+ }
23
+
24
+ export function laneRemaining(schedule: Schedule, now: Date): number {
25
+ return Math.max(0, laneDuration(schedule) - laneRawElapsed(schedule, now))
26
+ }
27
+
28
+ export function laneComplete(schedule: Schedule, now: Date): boolean {
29
+ if (schedule.tasks.length === 0) return false
30
+ if (schedule.tasks.some((t) => t.type.toNumber() === TaskType.RESERVED)) return false
31
+ return laneRemaining(schedule, now) === 0
32
+ }
33
+
34
+ export function laneProgress(schedule: Schedule, now: Date): number {
35
+ const duration = laneDuration(schedule)
36
+ if (duration === 0) return schedule.tasks.length > 0 ? 1 : 0
37
+ return Math.min(1, laneElapsed(schedule, now) / duration)
38
+ }
39
+
40
+ export function currentTaskIndexForLane(schedule: Schedule, now: Date): number {
41
+ if (schedule.tasks.length === 0) return -1
42
+ if (laneRawElapsed(schedule, now) < 0) return -1
43
+ const elapsed = laneElapsed(schedule, now)
44
+ let timeAccum = 0
45
+ for (let i = 0; i < schedule.tasks.length; i++) {
46
+ const taskDuration = schedule.tasks[i].duration.toNumber()
47
+ if (elapsed < timeAccum + taskDuration) return i
48
+ timeAccum += taskDuration
49
+ }
50
+ return -1
51
+ }
52
+
53
+ export function currentTask(schedule: Schedule, now: Date): Task | undefined {
54
+ const index = currentTaskIndexForLane(schedule, now)
55
+ if (index < 0) return undefined
56
+ return schedule.tasks[index]
57
+ }
58
+
59
+ export function currentTaskType(schedule: Schedule, now: Date): TaskType | undefined {
60
+ const task = currentTask(schedule, now)
61
+ return task ? (task.type.toNumber() as TaskType) : undefined
62
+ }
63
+
64
+ export function laneTaskStartTime(schedule: Schedule, index: number): number {
65
+ if (index < 0 || index >= schedule.tasks.length) return 0
66
+ let timeAccum = 0
67
+ for (let i = 0; i < index; i++) {
68
+ timeAccum += schedule.tasks[i].duration.toNumber()
69
+ }
70
+ return timeAccum
71
+ }
72
+
73
+ export function laneTaskElapsed(schedule: Schedule, index: number, now: Date): number {
74
+ if (index < 0 || index >= schedule.tasks.length) return 0
75
+ const elapsed = laneElapsed(schedule, now)
76
+ const taskStart = laneTaskStartTime(schedule, index)
77
+ const taskDuration = schedule.tasks[index].duration.toNumber()
78
+ if (elapsed <= taskStart) return 0
79
+ return Math.min(elapsed - taskStart, taskDuration)
80
+ }
81
+
82
+ export function laneTaskRemaining(schedule: Schedule, index: number, now: Date): number {
83
+ if (index < 0 || index >= schedule.tasks.length) return 0
84
+ const taskDuration = schedule.tasks[index].duration.toNumber()
85
+ return Math.max(0, taskDuration - laneTaskElapsed(schedule, index, now))
86
+ }
87
+
88
+ export function laneTaskComplete(schedule: Schedule, index: number, now: Date): boolean {
89
+ if (index < 0 || index >= schedule.tasks.length) return false
90
+ if (schedule.tasks[index].type.toNumber() === TaskType.RESERVED) return false
91
+ const taskDuration = schedule.tasks[index].duration.toNumber()
92
+ return laneTaskElapsed(schedule, index, now) >= taskDuration
93
+ }
94
+
95
+ export function laneTaskInProgress(schedule: Schedule, index: number, now: Date): boolean {
96
+ if (index < 0 || index >= schedule.tasks.length) return false
97
+ const taskElapsed = laneTaskElapsed(schedule, index, now)
98
+ const taskDuration = schedule.tasks[index].duration.toNumber()
99
+ return taskElapsed > 0 && taskElapsed < taskDuration
100
+ }
101
+
102
+ export function laneCompletesAt(schedule: Schedule, index: number): Date {
103
+ const startedMs = schedule.started.toDate().getTime()
104
+ const endSec =
105
+ laneTaskStartTime(schedule, index) + (schedule.tasks[index]?.duration.toNumber() ?? 0)
106
+ return new Date(startedMs + endSec * 1000)
107
+ }
108
+
109
+ export function currentTaskProgress(schedule: Schedule, now: Date): number {
110
+ const index = currentTaskIndexForLane(schedule, now)
111
+ if (index < 0) return 0
112
+ const elapsed = laneTaskElapsed(schedule, index, now)
113
+ const duration = schedule.tasks[index].duration.toNumber()
114
+ if (duration === 0) return 1
115
+ return Math.min(1, elapsed / duration)
116
+ }
117
+
118
+ export function currentTaskProgressFloatForLane(schedule: Schedule, now: Date): number {
119
+ if (schedule.tasks.length === 0) return 0
120
+ const index = currentTaskIndexForLane(schedule, now)
121
+ if (index < 0) return 0
122
+ const task = schedule.tasks[index]
123
+ const durationMs = task.duration.toNumber() * 1000
124
+ if (durationMs === 0) return 1
125
+ const startedMs = schedule.started.toDate().getTime()
126
+ const taskStartMs = startedMs + laneTaskStartTime(schedule, index) * 1000
127
+ const elapsedMs = now.getTime() - taskStartMs
128
+ if (elapsedMs <= 0) return 0
129
+ return Math.min(1, elapsedMs / durationMs)
130
+ }
@@ -0,0 +1,60 @@
1
+ import type {ServerContract} from '../contracts'
2
+ import {getItem} from '../data/catalog'
3
+ import type {ModuleType} from '../types'
4
+ import {getLane, type ScheduleData} from './schedule'
5
+
6
+ type ModuleEntry = ServerContract.Types.module_entry
7
+ type Lane = ServerContract.Types.lane
8
+ type Schedule = ServerContract.Types.schedule
9
+
10
+ export function laneKeyForModule(slotIndex: number): number {
11
+ return slotIndex + 1
12
+ }
13
+
14
+ function laneIsFree(lanes: Lane[], laneKey: number): boolean {
15
+ const lane = lanes.find((entry) => entry.lane_key.toNumber() === laneKey)
16
+ return lane ? lane.schedule.tasks.length === 0 : true
17
+ }
18
+
19
+ export function workerLaneKey(
20
+ modules: ModuleEntry[],
21
+ moduleSubtype: ModuleType,
22
+ lanes: Lane[]
23
+ ): number {
24
+ const occupiedMatchingLaneKeys: number[] = []
25
+
26
+ for (let slotIndex = 0; slotIndex < modules.length; slotIndex++) {
27
+ const installed = modules[slotIndex].installed
28
+ if (!installed) continue
29
+ if (getItem(installed.item_id).moduleType !== moduleSubtype) continue
30
+
31
+ const laneKey = laneKeyForModule(slotIndex)
32
+ if (laneIsFree(lanes, laneKey)) return laneKey
33
+ occupiedMatchingLaneKeys.push(laneKey)
34
+ }
35
+
36
+ if (occupiedMatchingLaneKeys.length > 0) {
37
+ return Math.min(...occupiedMatchingLaneKeys)
38
+ }
39
+
40
+ throw new Error(`No installed ${moduleSubtype} worker module`)
41
+ }
42
+
43
+ export function rawScheduleEnd(schedule: Schedule): Date {
44
+ const durationSec = schedule.tasks.reduce((sum, task) => sum + task.duration.toNumber(), 0)
45
+ return new Date(schedule.started.toDate().getTime() + durationSec * 1000)
46
+ }
47
+
48
+ export function candidateLaneCompletesAt(
49
+ entity: ScheduleData,
50
+ laneKey: number,
51
+ durationSec: number,
52
+ now: Date
53
+ ): Date {
54
+ const lane = getLane(entity, laneKey)
55
+ const startMs = lane
56
+ ? Math.max(rawScheduleEnd(lane.schedule).getTime(), now.getTime())
57
+ : now.getTime()
58
+
59
+ return new Date(startMs + durationSec * 1000)
60
+ }
@@ -1,4 +1,4 @@
1
- import {Name, TimePoint, UInt16, UInt32, UInt64} from '@wharfkit/antelope'
1
+ import {Name, UInt16, UInt32, UInt64} from '@wharfkit/antelope'
2
2
  import {ServerContract} from '../contracts'
3
3
  import {Coordinates, TaskType} from '../types'
4
4
  import {
@@ -333,50 +333,27 @@ export interface ProjectionOptions {
333
333
 
334
334
  export function projectEntity(entity: Projectable, options?: ProjectionOptions): ProjectedEntity {
335
335
  const projected = createProjectedEntity(entity)
336
- if (!entity.schedule || entity.schedule.tasks.length === 0) return projected
336
+ const ordered = schedule.orderedTasks(entity)
337
+ if (ordered.length === 0) return projected
337
338
 
338
- const tasks = entity.schedule.tasks
339
339
  const taskCount =
340
340
  options?.upToTaskIndex !== undefined
341
- ? Math.max(0, Math.min(options.upToTaskIndex, tasks.length))
342
- : tasks.length
341
+ ? Math.max(0, Math.min(options.upToTaskIndex, ordered.length))
342
+ : ordered.length
343
343
 
344
344
  for (let i = 0; i < taskCount; i++) {
345
- applyTask(projected, tasks[i])
345
+ applyTask(projected, ordered[i].task)
346
346
  }
347
347
  return projected
348
348
  }
349
349
 
350
- export interface ProjectableSnapshot extends Projectable {
351
- current_task?: ServerContract.Types.task
352
- pending_tasks?: ServerContract.Types.task[]
353
- }
354
-
355
- function buildRemainingProjectable(snapshot: ProjectableSnapshot): Projectable | null {
356
- if (!snapshot.schedule) return null
357
- const remainingTasks: ServerContract.Types.task[] = []
358
- if (snapshot.current_task) remainingTasks.push(snapshot.current_task)
359
- if (snapshot.pending_tasks?.length) remainingTasks.push(...snapshot.pending_tasks)
360
- if (remainingTasks.length === 0) return null
361
-
362
- const completedCount = snapshot.schedule.tasks.length - remainingTasks.length
363
- let startedMs = snapshot.schedule.started.toMilliseconds()
364
- for (let i = 0; i < completedCount; i++) {
365
- startedMs += snapshot.schedule.tasks[i].duration.toNumber() * 1000
366
- }
367
-
368
- return {
369
- ...snapshot,
370
- schedule: ServerContract.Types.schedule.from({
371
- started: TimePoint.fromMilliseconds(startedMs),
372
- tasks: remainingTasks,
373
- }),
350
+ export function projectRemainingAt(entity: Projectable, _now: Date): ProjectedEntity {
351
+ // Resolve is lazy/entity-global: completed tasks are unsettled until resolve, so replay all.
352
+ const projected = createProjectedEntity(entity)
353
+ for (const {task} of schedule.orderedTasks(entity)) {
354
+ applyTask(projected, task)
374
355
  }
375
- }
376
-
377
- export function projectFromCurrentState(snapshot: ProjectableSnapshot): ProjectedEntity {
378
- const projectable = buildRemainingProjectable(snapshot)
379
- return projectable ? projectEntity(projectable) : createProjectedEntity(snapshot)
356
+ return projected
380
357
  }
381
358
 
382
359
  function getRecipeInputsForOutput(outputItemId: number): RecipeInput[] | undefined {
@@ -438,10 +415,11 @@ function validateCraftTask(task: ServerContract.Types.task, projected: Projected
438
415
  }
439
416
 
440
417
  export function validateSchedule(entity: Projectable): void {
441
- if (!entity.schedule || entity.schedule.tasks.length === 0) return
418
+ const ordered = schedule.orderedTasks(entity)
419
+ if (ordered.length === 0) return
442
420
 
443
421
  const projected = createProjectedEntity(entity)
444
- for (const task of entity.schedule.tasks) {
422
+ for (const {task} of ordered) {
445
423
  if (task.type.toNumber() === TaskType.CRAFT) {
446
424
  validateCraftTask(task, projected)
447
425
  }
@@ -455,22 +433,28 @@ export function validateSchedule(entity: Projectable): void {
455
433
  export function projectEntityAt(entity: Projectable, now: Date): ProjectedEntity {
456
434
  const projected = createProjectedEntity(entity)
457
435
 
458
- if (!entity.schedule || entity.schedule.tasks.length === 0) {
436
+ const ordered = schedule.orderedTasks(entity)
437
+ if (ordered.length === 0) {
459
438
  return projected
460
439
  }
461
440
 
462
- for (let i = 0; i < entity.schedule.tasks.length; i++) {
463
- const task = entity.schedule.tasks[i]
464
- const taskComplete = schedule.isTaskComplete(entity, i, now)
465
- const taskInProgress = schedule.isTaskInProgress(entity, i, now)
441
+ const nowMs = now.getTime()
442
+
443
+ for (const {task, startsAt} of ordered) {
444
+ const duration = task.duration.toNumber()
445
+ const isReserved = task.type.toNumber() === TaskType.RESERVED
446
+ const elapsed = Math.min(
447
+ Math.max(0, Math.floor((nowMs - startsAt.getTime()) / 1000)),
448
+ duration
449
+ )
450
+ const taskComplete = !isReserved && elapsed >= duration
451
+ const taskInProgress = elapsed > 0 && elapsed < duration
466
452
 
467
453
  if (!taskComplete && !taskInProgress) {
468
- break
454
+ continue
469
455
  }
470
456
 
471
- const progress = taskInProgress
472
- ? schedule.getTaskElapsed(entity, i, now) / task.duration.toNumber()
473
- : undefined
457
+ const progress = taskInProgress ? elapsed / duration : undefined
474
458
 
475
459
  switch (task.type.toNumber()) {
476
460
  case TaskType.RECHARGE:
@@ -504,11 +488,3 @@ export function projectEntityAt(entity: Projectable, now: Date): ProjectedEntity
504
488
 
505
489
  return projected
506
490
  }
507
-
508
- export function projectFromCurrentStateAt(
509
- snapshot: ProjectableSnapshot,
510
- now: Date
511
- ): ProjectedEntity {
512
- const projectable = buildRemainingProjectable(snapshot)
513
- return projectable ? projectEntityAt(projectable, now) : createProjectedEntity(snapshot)
514
- }