@shipload/sdk 1.0.0-next.3 → 1.0.0-next.31

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 (99) hide show
  1. package/lib/shipload.d.ts +1849 -961
  2. package/lib/shipload.js +9089 -4854
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +8958 -4805
  5. package/lib/shipload.m.js.map +1 -1
  6. package/lib/testing.d.ts +856 -0
  7. package/lib/testing.js +3739 -0
  8. package/lib/testing.js.map +1 -0
  9. package/lib/testing.m.js +3733 -0
  10. package/lib/testing.m.js.map +1 -0
  11. package/package.json +15 -2
  12. package/src/capabilities/craftable.ts +51 -0
  13. package/src/capabilities/crafting.test.ts +7 -0
  14. package/src/capabilities/crafting.ts +3 -3
  15. package/src/capabilities/gathering.ts +17 -7
  16. package/src/capabilities/index.ts +0 -1
  17. package/src/capabilities/modules.ts +6 -0
  18. package/src/capabilities/storage.ts +16 -1
  19. package/src/contracts/platform.ts +231 -3
  20. package/src/contracts/server.ts +816 -471
  21. package/src/data/capabilities.ts +14 -329
  22. package/src/data/capability-formulas.ts +76 -0
  23. package/src/data/catalog.ts +0 -5
  24. package/src/data/colors.ts +14 -47
  25. package/src/data/entities.json +46 -10
  26. package/src/data/item-ids.ts +15 -12
  27. package/src/data/items.json +302 -38
  28. package/src/data/kind-registry.json +85 -0
  29. package/src/data/kind-registry.ts +150 -0
  30. package/src/data/metadata.ts +100 -31
  31. package/src/data/recipes-runtime.ts +3 -23
  32. package/src/data/recipes.json +250 -113
  33. package/src/derivation/build-methods.ts +45 -0
  34. package/src/derivation/capabilities.ts +415 -0
  35. package/src/derivation/capability-mappings.ts +117 -0
  36. package/src/derivation/crafting.ts +23 -24
  37. package/src/derivation/index.ts +17 -2
  38. package/src/derivation/reserve-regen.ts +34 -0
  39. package/src/derivation/resources.ts +125 -38
  40. package/src/derivation/stars.test.ts +51 -0
  41. package/src/derivation/stars.ts +15 -0
  42. package/src/derivation/stats.ts +6 -6
  43. package/src/derivation/stratum.ts +15 -19
  44. package/src/derivation/tiers.ts +28 -7
  45. package/src/entities/entity.ts +98 -0
  46. package/src/entities/gamestate.ts +3 -28
  47. package/src/entities/makers.ts +91 -136
  48. package/src/entities/slot-multiplier.ts +39 -0
  49. package/src/errors.ts +10 -15
  50. package/src/format.ts +26 -4
  51. package/src/index-module.ts +189 -47
  52. package/src/managers/actions.ts +252 -83
  53. package/src/managers/base.ts +6 -2
  54. package/src/managers/construction-types.ts +79 -0
  55. package/src/managers/construction.ts +396 -0
  56. package/src/managers/context.ts +11 -1
  57. package/src/managers/entities.ts +18 -66
  58. package/src/managers/epochs.ts +40 -0
  59. package/src/managers/index.ts +17 -1
  60. package/src/managers/locations.ts +25 -29
  61. package/src/managers/nft.ts +28 -0
  62. package/src/managers/plot.ts +127 -0
  63. package/src/nft/atomicassets.abi.json +1342 -0
  64. package/src/nft/atomicassets.ts +237 -0
  65. package/src/nft/atomicdata.ts +130 -0
  66. package/src/nft/buildImmutableData.ts +321 -0
  67. package/src/nft/description.ts +37 -15
  68. package/src/nft/index.ts +3 -0
  69. package/src/resolution/describe-module.ts +5 -8
  70. package/src/resolution/display-name.ts +38 -10
  71. package/src/resolution/resolve-item.ts +22 -20
  72. package/src/scheduling/accessor.ts +68 -22
  73. package/src/scheduling/availability.ts +108 -0
  74. package/src/scheduling/energy.ts +48 -0
  75. package/src/scheduling/lane-core.ts +130 -0
  76. package/src/scheduling/lanes.ts +60 -0
  77. package/src/scheduling/projection.ts +121 -94
  78. package/src/scheduling/schedule.ts +237 -103
  79. package/src/scheduling/task-cargo.ts +46 -0
  80. package/src/shipload.ts +16 -1
  81. package/src/subscriptions/manager.ts +40 -6
  82. package/src/subscriptions/mappers.ts +3 -8
  83. package/src/subscriptions/types.ts +3 -2
  84. package/src/testing/catalog-hash.ts +19 -0
  85. package/src/testing/index.ts +2 -0
  86. package/src/testing/projection-parity.ts +143 -0
  87. package/src/travel/travel.ts +90 -13
  88. package/src/types/capabilities.ts +1 -0
  89. package/src/types/index.ts +0 -1
  90. package/src/types.ts +19 -12
  91. package/src/utils/cargo.ts +27 -0
  92. package/src/utils/display-name.ts +70 -0
  93. package/src/utils/system.ts +25 -24
  94. package/src/capabilities/loading.ts +0 -8
  95. package/src/entities/container.ts +0 -108
  96. package/src/entities/ship-deploy.ts +0 -258
  97. package/src/entities/ship.ts +0 -204
  98. package/src/entities/warehouse.ts +0 -119
  99. package/src/types/entity-traits.ts +0 -69
@@ -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,6 +1,6 @@
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
- import {Coordinates, PRECISION, TaskType} from '../types'
3
+ import {Coordinates, TaskType} from '../types'
4
4
  import {
5
5
  capsHasLoaders,
6
6
  capsHasMovement,
@@ -14,17 +14,19 @@ import {
14
14
  RECIPE_INPUTS_INSUFFICIENT,
15
15
  RECIPE_INPUTS_INVALID,
16
16
  RECIPE_NOT_FOUND,
17
- SHIP_CARGO_NOT_LOADED,
17
+ ENTITY_CARGO_NOT_LOADED,
18
18
  } from '../errors'
19
- import {getRecipe, type RecipeInput} from '../data/recipes-runtime'
20
- import {getItem} from '../data/catalog'
21
- import {distanceBetweenCoordinates, lerp} from '../travel/travel'
19
+ import {getEntityLayout, getRecipe, type RecipeInput} from '../data/recipes-runtime'
20
+ import {computeEntityCapabilities} from '../derivation/capabilities'
21
+ import {decodeCraftedItemStats} from '../derivation/crafting'
22
+ import {packedModulesToInstalled, type InstalledModule} from '../entities/slot-multiplier'
23
+ import {lerp} from '../travel/travel'
22
24
  import {
23
25
  calcStacksMass,
24
26
  cargoItemToStack,
25
27
  type CargoStack,
26
28
  mergeStacks,
27
- removeFromStacks,
29
+ subtractFromStacks,
28
30
  stackToCargoItem,
29
31
  } from '../capabilities/storage'
30
32
  import * as schedule from './schedule'
@@ -63,19 +65,74 @@ export interface Projectable extends ScheduleData {
63
65
  cargo: ServerContract.Types.cargo_item[]
64
66
  cargomass: UInt32
65
67
  owner?: Name
68
+ stats?: bigint
69
+ item_id?: number | UInt16
70
+ modules?: ServerContract.Types.module_entry[] | InstalledModule[]
66
71
  }
67
72
 
68
- function getHullMass(entity: Projectable): UInt32 {
69
- return UInt32.from(entity.hullmass ?? 0)
73
+ function toInstalledModules(
74
+ modules: ServerContract.Types.module_entry[] | InstalledModule[]
75
+ ): InstalledModule[] {
76
+ if (modules.length > 0 && 'itemId' in modules[0]) {
77
+ return modules as InstalledModule[]
78
+ }
79
+ return packedModulesToInstalled(modules as ServerContract.Types.module_entry[])
80
+ }
81
+
82
+ interface ProjectedCaps {
83
+ hullmass?: UInt32
84
+ capacity?: UInt32
85
+ engines?: ServerContract.Types.movement_stats
86
+ generator?: ServerContract.Types.energy_stats
87
+ loaders?: ServerContract.Types.loader_stats
88
+ hauler?: ServerContract.Types.hauler_stats
89
+ }
90
+
91
+ function recomputeCaps(entity: Projectable): ProjectedCaps | undefined {
92
+ if (
93
+ entity.item_id === undefined ||
94
+ entity.modules === undefined ||
95
+ entity.stats === undefined
96
+ ) {
97
+ return undefined
98
+ }
99
+
100
+ const itemId = Number(
101
+ typeof entity.item_id === 'number' ? entity.item_id : entity.item_id.value
102
+ )
103
+ const hullStats = decodeCraftedItemStats(itemId, entity.stats)
104
+ const layout = getEntityLayout(itemId)?.slots ?? []
105
+ const installed = toInstalledModules(entity.modules)
106
+ const caps = computeEntityCapabilities(hullStats, itemId, installed, layout)
107
+
108
+ return {
109
+ hullmass: UInt32.from(caps.hullmass),
110
+ capacity: UInt32.from(caps.capacity),
111
+ engines: caps.engines ? ServerContract.Types.movement_stats.from(caps.engines) : undefined,
112
+ generator: caps.generator
113
+ ? ServerContract.Types.energy_stats.from(caps.generator)
114
+ : undefined,
115
+ loaders: caps.loaders ? ServerContract.Types.loader_stats.from(caps.loaders) : undefined,
116
+ hauler: caps.hauler ? ServerContract.Types.hauler_stats.from(caps.hauler) : undefined,
117
+ }
70
118
  }
71
119
 
72
120
  export function createProjectedEntity(entity: Projectable): ProjectedEntity {
73
- const shipMass = getHullMass(entity)
74
- const loaders = entity.loaders
75
- const engines = entity.engines
76
- const generator = entity.generator
77
- const hauler = entity.hauler
78
- const capacity = entity.capacity
121
+ const needsRecompute =
122
+ entity.hullmass === undefined ||
123
+ entity.loaders === undefined ||
124
+ entity.engines === undefined ||
125
+ entity.generator === undefined ||
126
+ entity.hauler === undefined ||
127
+ entity.capacity === undefined
128
+ const caps = needsRecompute ? recomputeCaps(entity) : undefined
129
+
130
+ const shipMass = UInt32.from(entity.hullmass ?? caps?.hullmass ?? 0)
131
+ const loaders = entity.loaders ?? caps?.loaders
132
+ const engines = entity.engines ?? caps?.engines
133
+ const generator = entity.generator ?? caps?.generator
134
+ const hauler = entity.hauler ?? caps?.hauler
135
+ const capacity = entity.capacity ?? caps?.capacity
79
136
 
80
137
  const cargo: CargoStack[] = entity.cargo.map(cargoItemToStack)
81
138
 
@@ -160,25 +217,22 @@ function applyFlightTask(
160
217
  task: ServerContract.Types.task,
161
218
  options: {complete: boolean; progress?: number}
162
219
  ): void {
163
- if (!task.coordinates || !projected.engines) return
220
+ if (!task.coordinates) return
164
221
 
165
- const origin = projected.location
166
222
  const destination = Coordinates.from(task.coordinates)
167
- const distance = distanceBetweenCoordinates(origin, task.coordinates)
168
- const energyUsage = distance.dividing(PRECISION).multiplying(projected.engines.drain)
169
223
 
170
224
  if (options.complete) {
171
- projected.energy = projected.energy.gt(energyUsage)
172
- ? UInt16.from(projected.energy.subtracting(energyUsage))
173
- : UInt16.from(0)
225
+ applyEnergyCost(projected, task)
174
226
  projected.location = destination
175
227
  } else if (options.progress !== undefined) {
176
- const interpolated = lerp(origin, destination, options.progress)
228
+ const interpolated = lerp(projected.location, destination, options.progress)
177
229
  projected.location = Coordinates.from({
178
230
  x: Math.round(interpolated.x),
179
231
  y: Math.round(interpolated.y),
180
232
  })
181
- const partialEnergy = UInt64.from(Math.floor(Number(energyUsage) * options.progress))
233
+ const partialEnergy = UInt64.from(
234
+ Math.floor(Number(task.energy_cost ?? 0) * options.progress)
235
+ )
182
236
  projected.energy = projected.energy.gt(partialEnergy)
183
237
  ? UInt16.from(projected.energy.subtracting(partialEnergy))
184
238
  : UInt16.from(0)
@@ -190,7 +244,7 @@ function addCargoItem(projected: ProjectedEntity, item: ServerContract.Types.car
190
244
  }
191
245
 
192
246
  function removeCargoItem(projected: ProjectedEntity, item: ServerContract.Types.cargo_item): void {
193
- projected.cargo = removeFromStacks(projected.cargo, cargoItemToStack(item))
247
+ projected.cargo = subtractFromStacks(projected.cargo, cargoItemToStack(item))
194
248
  }
195
249
 
196
250
  function applyAddCargoTask(projected: ProjectedEntity, task: ServerContract.Types.task): void {
@@ -248,6 +302,7 @@ function applyTask(projected: ProjectedEntity, task: ServerContract.Types.task):
248
302
  applyRechargeTask(projected, task, {complete: true})
249
303
  break
250
304
  case TaskType.TRAVEL:
305
+ case TaskType.WARP:
251
306
  applyFlightTask(projected, task, {complete: true})
252
307
  break
253
308
  case TaskType.LOAD:
@@ -255,7 +310,6 @@ function applyTask(projected: ProjectedEntity, task: ServerContract.Types.task):
255
310
  applyAddCargoTask(projected, task)
256
311
  break
257
312
  case TaskType.UNLOAD:
258
- case TaskType.WRAP:
259
313
  applyRemoveCargoTask(projected, task)
260
314
  break
261
315
  case TaskType.GATHER:
@@ -267,6 +321,9 @@ function applyTask(projected: ProjectedEntity, task: ServerContract.Types.task):
267
321
  case TaskType.DEPLOY:
268
322
  applyDeployTask(projected, task)
269
323
  break
324
+ case TaskType.UNDEPLOY:
325
+ case TaskType.DEMOLISH:
326
+ break
270
327
  }
271
328
  }
272
329
 
@@ -276,50 +333,27 @@ export interface ProjectionOptions {
276
333
 
277
334
  export function projectEntity(entity: Projectable, options?: ProjectionOptions): ProjectedEntity {
278
335
  const projected = createProjectedEntity(entity)
279
- if (!entity.schedule || entity.schedule.tasks.length === 0) return projected
336
+ const ordered = schedule.orderedTasks(entity)
337
+ if (ordered.length === 0) return projected
280
338
 
281
- const tasks = entity.schedule.tasks
282
339
  const taskCount =
283
340
  options?.upToTaskIndex !== undefined
284
- ? Math.max(0, Math.min(options.upToTaskIndex, tasks.length))
285
- : tasks.length
341
+ ? Math.max(0, Math.min(options.upToTaskIndex, ordered.length))
342
+ : ordered.length
286
343
 
287
344
  for (let i = 0; i < taskCount; i++) {
288
- applyTask(projected, tasks[i])
345
+ applyTask(projected, ordered[i].task)
289
346
  }
290
347
  return projected
291
348
  }
292
349
 
293
- export interface ProjectableSnapshot extends Projectable {
294
- current_task?: ServerContract.Types.task
295
- pending_tasks?: ServerContract.Types.task[]
296
- }
297
-
298
- function buildRemainingProjectable(snapshot: ProjectableSnapshot): Projectable | null {
299
- if (!snapshot.schedule) return null
300
- const remainingTasks: ServerContract.Types.task[] = []
301
- if (snapshot.current_task) remainingTasks.push(snapshot.current_task)
302
- if (snapshot.pending_tasks?.length) remainingTasks.push(...snapshot.pending_tasks)
303
- if (remainingTasks.length === 0) return null
304
-
305
- const completedCount = snapshot.schedule.tasks.length - remainingTasks.length
306
- let startedMs = snapshot.schedule.started.toMilliseconds()
307
- for (let i = 0; i < completedCount; i++) {
308
- startedMs += snapshot.schedule.tasks[i].duration.toNumber() * 1000
309
- }
310
-
311
- return {
312
- ...snapshot,
313
- schedule: ServerContract.Types.schedule.from({
314
- started: TimePoint.fromMilliseconds(startedMs),
315
- tasks: remainingTasks,
316
- }),
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)
317
355
  }
318
- }
319
-
320
- export function projectFromCurrentState(snapshot: ProjectableSnapshot): ProjectedEntity {
321
- const projectable = buildRemainingProjectable(snapshot)
322
- return projectable ? projectEntity(projectable) : createProjectedEntity(snapshot)
356
+ return projected
323
357
  }
324
358
 
325
359
  function getRecipeInputsForOutput(outputItemId: number): RecipeInput[] | undefined {
@@ -342,19 +376,10 @@ function validateCraftTask(task: ServerContract.Types.task, projected: Projected
342
376
  let matched = false
343
377
  for (let ri = 0; ri < recipe.length; ri++) {
344
378
  const req = recipe[ri]
345
- if ('itemId' in req) {
346
- if (input.item_id.toNumber() === req.itemId) {
347
- groupedInputs[ri].push(input)
348
- matched = true
349
- break
350
- }
351
- } else {
352
- const item = getItem(input.item_id)
353
- if (item.category === req.category && item.tier === req.tier) {
354
- groupedInputs[ri].push(input)
355
- matched = true
356
- break
357
- }
379
+ if (input.item_id.toNumber() === req.itemId) {
380
+ groupedInputs[ri].push(input)
381
+ matched = true
382
+ break
358
383
  }
359
384
  }
360
385
  if (!matched) throw new Error(RECIPE_INPUTS_INVALID)
@@ -385,15 +410,16 @@ function validateCraftTask(task: ServerContract.Types.task, projected: Projected
385
410
  break
386
411
  }
387
412
  }
388
- if (!found) throw new Error(SHIP_CARGO_NOT_LOADED)
413
+ if (!found) throw new Error(ENTITY_CARGO_NOT_LOADED)
389
414
  }
390
415
  }
391
416
 
392
417
  export function validateSchedule(entity: Projectable): void {
393
- if (!entity.schedule || entity.schedule.tasks.length === 0) return
418
+ const ordered = schedule.orderedTasks(entity)
419
+ if (ordered.length === 0) return
394
420
 
395
421
  const projected = createProjectedEntity(entity)
396
- for (const task of entity.schedule.tasks) {
422
+ for (const {task} of ordered) {
397
423
  if (task.type.toNumber() === TaskType.CRAFT) {
398
424
  validateCraftTask(task, projected)
399
425
  }
@@ -407,28 +433,35 @@ export function validateSchedule(entity: Projectable): void {
407
433
  export function projectEntityAt(entity: Projectable, now: Date): ProjectedEntity {
408
434
  const projected = createProjectedEntity(entity)
409
435
 
410
- if (!entity.schedule || entity.schedule.tasks.length === 0) {
436
+ const ordered = schedule.orderedTasks(entity)
437
+ if (ordered.length === 0) {
411
438
  return projected
412
439
  }
413
440
 
414
- for (let i = 0; i < entity.schedule.tasks.length; i++) {
415
- const task = entity.schedule.tasks[i]
416
- const taskComplete = schedule.isTaskComplete(entity, i, now)
417
- 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
418
452
 
419
453
  if (!taskComplete && !taskInProgress) {
420
- break
454
+ continue
421
455
  }
422
456
 
423
- const progress = taskInProgress
424
- ? schedule.getTaskElapsed(entity, i, now) / task.duration.toNumber()
425
- : undefined
457
+ const progress = taskInProgress ? elapsed / duration : undefined
426
458
 
427
459
  switch (task.type.toNumber()) {
428
460
  case TaskType.RECHARGE:
429
461
  applyRechargeTask(projected, task, {complete: taskComplete, progress})
430
462
  break
431
463
  case TaskType.TRAVEL:
464
+ case TaskType.WARP:
432
465
  applyFlightTask(projected, task, {complete: taskComplete, progress})
433
466
  break
434
467
  case TaskType.LOAD:
@@ -436,7 +469,6 @@ export function projectEntityAt(entity: Projectable, now: Date): ProjectedEntity
436
469
  if (taskComplete) applyAddCargoTask(projected, task)
437
470
  break
438
471
  case TaskType.UNLOAD:
439
- case TaskType.WRAP:
440
472
  if (taskComplete) applyRemoveCargoTask(projected, task)
441
473
  break
442
474
  case TaskType.GATHER:
@@ -448,16 +480,11 @@ export function projectEntityAt(entity: Projectable, now: Date): ProjectedEntity
448
480
  case TaskType.DEPLOY:
449
481
  if (taskComplete) applyDeployTask(projected, task)
450
482
  break
483
+ case TaskType.UNDEPLOY:
484
+ case TaskType.DEMOLISH:
485
+ break
451
486
  }
452
487
  }
453
488
 
454
489
  return projected
455
490
  }
456
-
457
- export function projectFromCurrentStateAt(
458
- snapshot: ProjectableSnapshot,
459
- now: Date
460
- ): ProjectedEntity {
461
- const projectable = buildRemainingProjectable(snapshot)
462
- return projectable ? projectEntityAt(projectable, now) : createProjectedEntity(snapshot)
463
- }