@shipload/sdk 1.0.0-next.53 → 1.0.0-next.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/shipload.d.ts +64 -19
- package/lib/shipload.js +172 -22
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +170 -23
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +15 -6
- package/lib/testing.js +64 -20
- package/lib/testing.js.map +1 -1
- package/lib/testing.m.js +64 -20
- package/lib/testing.m.js.map +1 -1
- package/package.json +1 -1
- package/src/contracts/server.ts +55 -21
- package/src/data/entities.json +16 -0
- package/src/data/item-ids.ts +1 -0
- package/src/data/items.json +6 -0
- package/src/data/kind-registry.json +12 -0
- package/src/data/kind-registry.ts +5 -0
- package/src/data/metadata.ts +7 -0
- package/src/data/recipes.json +39 -0
- package/src/derivation/capabilities.ts +2 -0
- package/src/derivation/recipe-usage.test.ts +7 -1
- package/src/entities/entity.ts +4 -0
- package/src/index-module.ts +2 -0
- package/src/nft/description.ts +7 -0
- package/src/scheduling/schedule.test.ts +119 -0
- package/src/scheduling/schedule.ts +19 -2
- package/src/types.ts +7 -0
|
@@ -218,6 +218,7 @@ export function computeBatteryCapabilities(stats: Record<string, number>): {
|
|
|
218
218
|
import {
|
|
219
219
|
ITEM_CONTAINER_T1_PACKED,
|
|
220
220
|
ITEM_CONTAINER_T2_PACKED,
|
|
221
|
+
ITEM_CONSTRUCTION_DOCK_T1_PACKED,
|
|
221
222
|
ITEM_EXTRACTOR_T1_PACKED,
|
|
222
223
|
ITEM_FACTORY_T1_PACKED,
|
|
223
224
|
ITEM_MASS_CATCHER_T1_PACKED,
|
|
@@ -277,6 +278,7 @@ export function computeBaseCapacity(itemId: number, stats: Record<string, number
|
|
|
277
278
|
switch (itemId) {
|
|
278
279
|
case ITEM_EXTRACTOR_T1_PACKED:
|
|
279
280
|
case ITEM_FACTORY_T1_PACKED:
|
|
281
|
+
case ITEM_CONSTRUCTION_DOCK_T1_PACKED:
|
|
280
282
|
case ITEM_MASS_DRIVER_T1_PACKED:
|
|
281
283
|
case ITEM_MASS_CATCHER_T1_PACKED:
|
|
282
284
|
case ITEM_CONTAINER_T1_PACKED:
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
ITEM_BEAM,
|
|
14
14
|
ITEM_GATHERER_T1,
|
|
15
15
|
ITEM_CRAFTER_T1,
|
|
16
|
+
ITEM_BUILDER_T1,
|
|
16
17
|
ITEM_EXTRACTOR_T1_PACKED,
|
|
17
18
|
ITEM_ROUSTABOUT_T1_PACKED,
|
|
18
19
|
ITEM_PROSPECTOR_T1_PACKED,
|
|
@@ -33,7 +34,12 @@ test('getRecipeConsumers lists every recipe that consumes Sensor', () => {
|
|
|
33
34
|
const consumers = getRecipeConsumers(ITEM_SENSOR)
|
|
34
35
|
const ids = consumers.map((c) => c.outputItemId).sort((a, b) => a - b)
|
|
35
36
|
expect(ids).toEqual(
|
|
36
|
-
[
|
|
37
|
+
[
|
|
38
|
+
ITEM_CRAFTER_T1,
|
|
39
|
+
ITEM_BUILDER_T1,
|
|
40
|
+
ITEM_EXTRACTOR_T1_PACKED,
|
|
41
|
+
ITEM_ROUSTABOUT_T1_PACKED,
|
|
42
|
+
].sort((a, b) => a - b)
|
|
37
43
|
)
|
|
38
44
|
})
|
|
39
45
|
|
package/src/entities/entity.ts
CHANGED
|
@@ -31,6 +31,10 @@ export class Entity extends ServerContract.Types.entity_info {
|
|
|
31
31
|
return schedule.isIdle(this)
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
get hasPendingCapper(): boolean {
|
|
35
|
+
return schedule.hasPendingCapper(this)
|
|
36
|
+
}
|
|
37
|
+
|
|
34
38
|
get sched(): ScheduleAccessor {
|
|
35
39
|
this._sched ??= new ScheduleAccessor(this)
|
|
36
40
|
return this._sched
|
package/src/index-module.ts
CHANGED
|
@@ -333,6 +333,7 @@ export {
|
|
|
333
333
|
ENTITY_WAREHOUSE,
|
|
334
334
|
ENTITY_EXTRACTOR,
|
|
335
335
|
ENTITY_FACTORY,
|
|
336
|
+
ENTITY_CONSTRUCTION_DOCK,
|
|
336
337
|
ENTITY_WORKSHOP,
|
|
337
338
|
ENTITY_CONTAINER,
|
|
338
339
|
ENTITY_NEXUS,
|
|
@@ -351,6 +352,7 @@ export {
|
|
|
351
352
|
isWarehouse,
|
|
352
353
|
isExtractor,
|
|
353
354
|
isFactory,
|
|
355
|
+
isConstructionDock,
|
|
354
356
|
isWorkshop,
|
|
355
357
|
isContainer,
|
|
356
358
|
isNexus,
|
package/src/nft/description.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
ITEM_CONTAINER_T1_PACKED,
|
|
16
16
|
ITEM_CONTAINER_T2_PACKED,
|
|
17
|
+
ITEM_CONSTRUCTION_DOCK_T1_PACKED,
|
|
17
18
|
ITEM_BUILDER_T1,
|
|
18
19
|
ITEM_CRAFTER_T1,
|
|
19
20
|
ITEM_ENGINE_T1,
|
|
@@ -37,6 +38,7 @@ import {
|
|
|
37
38
|
ITEM_TUG_T1_PACKED,
|
|
38
39
|
ITEM_PORTER_T1_PACKED,
|
|
39
40
|
ITEM_WRANGLER_T1_PACKED,
|
|
41
|
+
ITEM_WRIGHT_T1_PACKED,
|
|
40
42
|
ITEM_DREDGER_T1_PACKED,
|
|
41
43
|
ITEM_WAREHOUSE_T1_PACKED,
|
|
42
44
|
ITEM_WARP_T1,
|
|
@@ -159,6 +161,8 @@ export function entityDisplayName(itemId: number): string {
|
|
|
159
161
|
return 'Porter'
|
|
160
162
|
case ITEM_WRANGLER_T1_PACKED:
|
|
161
163
|
return 'Wrangler'
|
|
164
|
+
case ITEM_WRIGHT_T1_PACKED:
|
|
165
|
+
return 'Wright'
|
|
162
166
|
case ITEM_DREDGER_T1_PACKED:
|
|
163
167
|
return 'Dredger'
|
|
164
168
|
case ITEM_WAREHOUSE_T1_PACKED:
|
|
@@ -167,6 +171,8 @@ export function entityDisplayName(itemId: number): string {
|
|
|
167
171
|
return 'Mining Rig'
|
|
168
172
|
case ITEM_FACTORY_T1_PACKED:
|
|
169
173
|
return 'Factory'
|
|
174
|
+
case ITEM_CONSTRUCTION_DOCK_T1_PACKED:
|
|
175
|
+
return 'Construction Dock'
|
|
170
176
|
case ITEM_CONTAINER_T1_PACKED:
|
|
171
177
|
return 'Container'
|
|
172
178
|
case ITEM_CONTAINER_T2_PACKED:
|
|
@@ -314,6 +320,7 @@ export function buildEntityDescription(
|
|
|
314
320
|
} else if (
|
|
315
321
|
itemId === ITEM_EXTRACTOR_T1_PACKED ||
|
|
316
322
|
itemId === ITEM_FACTORY_T1_PACKED ||
|
|
323
|
+
itemId === ITEM_CONSTRUCTION_DOCK_T1_PACKED ||
|
|
317
324
|
itemId === ITEM_CONTAINER_T1_PACKED ||
|
|
318
325
|
itemId === ITEM_CONTAINER_T2_PACKED
|
|
319
326
|
) {
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {describe, expect, test} from 'bun:test'
|
|
2
|
+
import {ServerContract, TaskType} from '../index-module'
|
|
3
|
+
import {hasPendingCapper, hasResolvable, isCapperTaskType} from './schedule'
|
|
4
|
+
|
|
5
|
+
const T0 = '2026-06-19T00:00:00'
|
|
6
|
+
const NOW = new Date('2026-06-19T00:01:00.000Z')
|
|
7
|
+
|
|
8
|
+
function task(over: Partial<{type: number; duration: number}>) {
|
|
9
|
+
return ServerContract.Types.task.from({
|
|
10
|
+
type: over.type ?? TaskType.TRAVEL,
|
|
11
|
+
duration: over.duration ?? 30,
|
|
12
|
+
cancelable: 0,
|
|
13
|
+
cargo: [],
|
|
14
|
+
couplings: [],
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function entity(
|
|
19
|
+
tasks: ReturnType<typeof task>[],
|
|
20
|
+
holds: ServerContract.Types.hold[] = [],
|
|
21
|
+
startedISO = T0
|
|
22
|
+
) {
|
|
23
|
+
return ServerContract.Types.entity_info.from({
|
|
24
|
+
type: 'ship',
|
|
25
|
+
id: 1,
|
|
26
|
+
owner: 'player.gm',
|
|
27
|
+
entity_name: 'Ship 1',
|
|
28
|
+
coordinates: {x: 0, y: 0, z: 0},
|
|
29
|
+
item_id: 1,
|
|
30
|
+
cargomass: 0,
|
|
31
|
+
cargo: [],
|
|
32
|
+
modules: [],
|
|
33
|
+
lanes: [{lane_key: 0, schedule: {started: startedISO, tasks}}],
|
|
34
|
+
gatherer_lanes: [],
|
|
35
|
+
crafter_lanes: [],
|
|
36
|
+
builder_lanes: [],
|
|
37
|
+
loader_lanes: [],
|
|
38
|
+
holds,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function hold(kind: number) {
|
|
43
|
+
return ServerContract.Types.hold.from({
|
|
44
|
+
id: 1,
|
|
45
|
+
kind,
|
|
46
|
+
counterpart: {entity_type: 'ship', entity_id: 2},
|
|
47
|
+
until: T0,
|
|
48
|
+
incoming_mass: 0,
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('isCapperTaskType', () => {
|
|
53
|
+
test('UNDEPLOY, DEMOLISH are cappers', () => {
|
|
54
|
+
expect(isCapperTaskType(TaskType.UNDEPLOY)).toBe(true)
|
|
55
|
+
expect(isCapperTaskType(TaskType.DEMOLISH)).toBe(true)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('REFIT is not a capper', () => {
|
|
59
|
+
expect(isCapperTaskType(TaskType.REFIT)).toBe(false)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('non-capper task types are not cappers', () => {
|
|
63
|
+
expect(isCapperTaskType(TaskType.TRAVEL)).toBe(false)
|
|
64
|
+
expect(isCapperTaskType(TaskType.LOAD)).toBe(false)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('hasPendingCapper', () => {
|
|
69
|
+
test('false with no schedule', () => {
|
|
70
|
+
expect(hasPendingCapper(entity([]))).toBe(false)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('false with only non-capper tasks queued', () => {
|
|
74
|
+
expect(hasPendingCapper(entity([task({type: TaskType.TRAVEL})]))).toBe(false)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('true when a DEMOLISH task is queued anywhere in the lane', () => {
|
|
78
|
+
const e = entity([task({type: TaskType.TRAVEL}), task({type: TaskType.DEMOLISH})])
|
|
79
|
+
expect(hasPendingCapper(e)).toBe(true)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('true when a DEMOLISH task is queued', () => {
|
|
83
|
+
expect(hasPendingCapper(entity([task({type: TaskType.DEMOLISH})]))).toBe(true)
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('hasResolvable — capper gating', () => {
|
|
88
|
+
test('a completed non-capper front is resolvable', () => {
|
|
89
|
+
const e = entity([task({type: TaskType.TRAVEL, duration: 30})])
|
|
90
|
+
expect(hasResolvable(e, NOW)).toBe(true)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('a completed capper front with no holds is resolvable', () => {
|
|
94
|
+
const e = entity([task({type: TaskType.DEMOLISH, duration: 30})])
|
|
95
|
+
expect(hasResolvable(e, NOW)).toBe(true)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('a completed capper front with a live hold is gated', () => {
|
|
99
|
+
const e = entity([task({type: TaskType.DEMOLISH, duration: 30})], [hold(2)])
|
|
100
|
+
expect(hasResolvable(e, NOW)).toBe(false)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('a capper front clears once holds empty', () => {
|
|
104
|
+
const withHold = entity([task({type: TaskType.DEMOLISH, duration: 30})], [hold(2)])
|
|
105
|
+
const cleared = entity([task({type: TaskType.DEMOLISH, duration: 30})], [])
|
|
106
|
+
expect(hasResolvable(withHold, NOW)).toBe(false)
|
|
107
|
+
expect(hasResolvable(cleared, NOW)).toBe(true)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('a live hold does not gate a non-capper front', () => {
|
|
111
|
+
const e = entity([task({type: TaskType.TRAVEL, duration: 30})], [hold(2)])
|
|
112
|
+
expect(hasResolvable(e, NOW)).toBe(true)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('an in-progress front is not resolvable regardless of holds', () => {
|
|
116
|
+
const e = entity([task({type: TaskType.DEMOLISH, duration: 3600})])
|
|
117
|
+
expect(hasResolvable(e, NOW)).toBe(false)
|
|
118
|
+
})
|
|
119
|
+
})
|
|
@@ -63,6 +63,20 @@ export function isIdle(entity: ScheduleData): boolean {
|
|
|
63
63
|
return !hasSchedule(entity) && !hasHolds(entity)
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
// Mirrors is_capper_task_type: demolish/undeploy cap a plan — no further appends once queued.
|
|
67
|
+
export function isCapperTaskType(taskType: number): boolean {
|
|
68
|
+
return taskType === TaskType.UNDEPLOY || taskType === TaskType.DEMOLISH
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function hasPendingCapper(entity: ScheduleData): boolean {
|
|
72
|
+
for (const l of entity.lanes ?? []) {
|
|
73
|
+
for (const t of l.schedule.tasks) {
|
|
74
|
+
if (isCapperTaskType(t.type.toNumber())) return true
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
|
|
66
80
|
export function isEntityIdle(entity: ScheduleData, now: Date): boolean {
|
|
67
81
|
if (hasHolds(entity)) return false
|
|
68
82
|
const lanes = entity.lanes
|
|
@@ -121,10 +135,13 @@ export function scheduleComplete(entity: ScheduleData, now: Date): boolean {
|
|
|
121
135
|
return remaining === 0
|
|
122
136
|
}
|
|
123
137
|
|
|
124
|
-
// Mirrors
|
|
138
|
+
// Mirrors lane_front_complete && !capper_front_gated for own-entity holds (workshop/undeploy-target gates need cross-entity context this ScheduleData lacks).
|
|
125
139
|
export function hasResolvable(entity: ScheduleData, now: Date): boolean {
|
|
126
140
|
for (const l of entity.lanes ?? []) {
|
|
127
|
-
if (core.laneTaskComplete(l.schedule, 0, now))
|
|
141
|
+
if (!core.laneTaskComplete(l.schedule, 0, now)) continue
|
|
142
|
+
const front = l.schedule.tasks[0]
|
|
143
|
+
if (isCapperTaskType(front.type.toNumber()) && hasHolds(entity)) continue
|
|
144
|
+
return true
|
|
128
145
|
}
|
|
129
146
|
return false
|
|
130
147
|
}
|