@shipload/sdk 1.0.0-next.6 → 1.0.0-next.61
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/scan.d.ts +52 -0
- package/lib/scan.js +162 -0
- package/lib/scan.js.map +1 -0
- package/lib/scan.m.js +152 -0
- package/lib/scan.m.js.map +1 -0
- package/lib/shipload.d.ts +3601 -1364
- package/lib/shipload.js +15671 -5261
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +15392 -5209
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +1173 -0
- package/lib/testing.js +4971 -0
- package/lib/testing.js.map +1 -0
- package/lib/testing.m.js +4965 -0
- package/lib/testing.m.js.map +1 -0
- package/package.json +20 -2
- package/src/capabilities/craftable.test.ts +82 -0
- package/src/capabilities/craftable.ts +82 -0
- package/src/capabilities/crafting.test.ts +35 -0
- package/src/capabilities/crafting.ts +34 -7
- package/src/capabilities/gathering.test.ts +29 -0
- package/src/capabilities/gathering.ts +37 -19
- package/src/capabilities/hauling.ts +0 -5
- package/src/capabilities/index.ts +0 -1
- package/src/capabilities/modules.ts +27 -30
- package/src/capabilities/movement.ts +1 -1
- package/src/capabilities/storage.ts +16 -1
- package/src/contracts/platform.ts +231 -3
- package/src/contracts/server.ts +1518 -501
- package/src/coordinates/address.ts +88 -0
- package/src/coordinates/constants.test.ts +15 -0
- package/src/coordinates/constants.ts +23 -0
- package/src/coordinates/index.ts +15 -0
- package/src/coordinates/memo.test.ts +47 -0
- package/src/coordinates/memo.ts +20 -0
- package/src/coordinates/permutation.ts +77 -0
- package/src/coordinates/regions.ts +48 -0
- package/src/coordinates/sectors.ts +115 -0
- package/src/data/capabilities.ts +49 -18
- package/src/data/capability-formulas.ts +26 -14
- package/src/data/catalog.ts +7 -8
- package/src/data/colors.ts +14 -48
- package/src/data/entities.json +380 -15
- package/src/data/item-ids.ts +51 -13
- package/src/data/items.json +318 -77
- package/src/data/kind-registry.json +195 -0
- package/src/data/kind-registry.ts +182 -0
- package/src/data/metadata.ts +291 -51
- package/src/data/recipes-runtime.ts +6 -23
- package/src/data/recipes.json +2260 -173
- package/src/data/tiers.ts +9 -6
- package/src/derivation/build-methods.test.ts +13 -0
- package/src/derivation/build-methods.ts +48 -0
- package/src/derivation/capabilities.test.ts +348 -0
- package/src/derivation/capabilities.ts +693 -0
- package/src/derivation/capability-mappings.ts +60 -16
- package/src/derivation/crafting.test.ts +17 -0
- package/src/derivation/crafting.ts +57 -34
- package/src/derivation/index.ts +27 -2
- package/src/derivation/recipe-usage.test.ts +90 -0
- package/src/derivation/recipe-usage.ts +141 -0
- package/src/derivation/reserve-regen.ts +34 -0
- package/src/derivation/resources.ts +9 -1
- package/src/derivation/rollups.test.ts +55 -0
- package/src/derivation/rollups.ts +72 -0
- package/src/derivation/stars.test.ts +67 -0
- package/src/derivation/stars.ts +25 -0
- package/src/derivation/stat-scaling.ts +12 -0
- package/src/derivation/stats.ts +6 -6
- package/src/derivation/stratum.ts +26 -22
- package/src/derivation/tiers.ts +40 -7
- package/src/derivation/upgrades.ts +14 -0
- package/src/derivation/wormhole.ts +154 -0
- package/src/entities/entity.ts +102 -0
- package/src/entities/gamestate.ts +3 -28
- package/src/entities/makers.ts +141 -135
- package/src/entities/slot-multiplier.ts +43 -0
- package/src/errors.ts +12 -16
- package/src/format.ts +26 -4
- package/src/index-module.ts +333 -35
- package/src/managers/actions.ts +668 -103
- package/src/managers/base.ts +6 -2
- package/src/managers/cluster.test.ts +39 -0
- package/src/managers/cluster.ts +53 -0
- package/src/managers/construction-types.ts +80 -0
- package/src/managers/construction.ts +443 -0
- package/src/managers/context.ts +29 -1
- package/src/managers/coordinates.ts +14 -0
- package/src/managers/entities.ts +27 -66
- package/src/managers/epochs.ts +40 -0
- package/src/managers/index.ts +20 -1
- package/src/managers/locations.ts +25 -29
- package/src/managers/nft.test.ts +14 -0
- package/src/managers/nft.ts +70 -0
- package/src/managers/players.ts +25 -0
- package/src/managers/plot.ts +122 -0
- package/src/nft/atomicassets.abi.json +1342 -0
- package/src/nft/atomicassets.ts +237 -0
- package/src/nft/atomicdata.ts +130 -0
- package/src/nft/buildImmutableData.ts +388 -0
- package/src/nft/description.ts +215 -99
- package/src/nft/index.ts +3 -0
- package/src/planner/index.ts +242 -0
- package/src/planner/planner.test.ts +334 -0
- package/src/resolution/describe-module.ts +43 -25
- package/src/resolution/display-name.ts +38 -10
- package/src/resolution/resolve-item.test.ts +48 -0
- package/src/resolution/resolve-item.ts +112 -58
- package/src/scan/index.ts +244 -0
- package/src/scan/scan-wasm.base64.ts +2 -0
- package/src/scheduling/accessor.ts +65 -23
- package/src/scheduling/availability.test.ts +204 -0
- package/src/scheduling/availability.ts +211 -0
- package/src/scheduling/cancel.test.ts +542 -0
- package/src/scheduling/cancel.ts +254 -0
- package/src/scheduling/cluster-stock.test.ts +31 -0
- package/src/scheduling/cluster-stock.ts +15 -0
- package/src/scheduling/energy.ts +47 -0
- package/src/scheduling/idle-resolve.ts +45 -0
- package/src/scheduling/jobs.ts +71 -0
- package/src/scheduling/lane-core.ts +128 -0
- package/src/scheduling/lanes.test.ts +250 -0
- package/src/scheduling/lanes.ts +227 -0
- package/src/scheduling/projection.ts +257 -133
- package/src/scheduling/schedule.test.ts +119 -0
- package/src/scheduling/schedule.ts +257 -117
- package/src/scheduling/task-cargo.test.ts +44 -0
- package/src/scheduling/task-cargo.ts +46 -0
- package/src/scheduling/unwrap.test.ts +86 -0
- package/src/scheduling/unwrap.ts +190 -0
- package/src/shipload.ts +26 -1
- package/src/subscriptions/manager.cluster.test.ts +51 -0
- package/src/subscriptions/manager.ts +245 -172
- package/src/subscriptions/mappers.ts +5 -8
- package/src/subscriptions/types.ts +28 -3
- package/src/testing/catalog-hash.ts +19 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/projection-parity.ts +167 -0
- package/src/travel/reach.ts +21 -0
- package/src/travel/route-planner.ts +288 -0
- package/src/travel/route-simulator.ts +170 -0
- package/src/travel/travel.ts +188 -122
- package/src/types/capabilities.ts +37 -8
- package/src/types/entity.ts +3 -3
- package/src/types/index.ts +0 -1
- package/src/types.ts +51 -16
- package/src/utils/cargo.test.ts +14 -0
- package/src/utils/cargo.ts +29 -0
- package/src/utils/display-name.ts +70 -0
- package/src/utils/system.ts +15 -16
- package/src/capabilities/loading.ts +0 -8
- package/src/entities/container.ts +0 -123
- package/src/entities/ship-deploy.ts +0 -295
- package/src/entities/ship.ts +0 -221
- package/src/entities/warehouse.ts +0 -127
- package/src/types/entity-traits.ts +0 -69
|
@@ -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
|
+
})
|
|
@@ -1,193 +1,333 @@
|
|
|
1
1
|
import type {ServerContract} from '../contracts'
|
|
2
2
|
import {TaskType} from '../types'
|
|
3
|
+
import * as core from './lane-core'
|
|
3
4
|
|
|
4
5
|
type Schedule = ServerContract.Types.schedule
|
|
5
6
|
type Task = ServerContract.Types.task
|
|
7
|
+
type Lane = ServerContract.Types.lane
|
|
8
|
+
type Hold = ServerContract.Types.hold
|
|
9
|
+
|
|
10
|
+
export const LANE_MOBILITY = 0
|
|
11
|
+
export const LANE_BARRIER = 255
|
|
6
12
|
|
|
7
13
|
export interface ScheduleData {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
14
|
+
lanes?: Lane[]
|
|
15
|
+
holds?: Hold[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface LaneView {
|
|
19
|
+
laneKey: number
|
|
20
|
+
schedule: Schedule
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
laneStartsIn,
|
|
25
|
+
currentTaskIndexForLane,
|
|
26
|
+
laneTaskComplete,
|
|
27
|
+
laneTaskInProgress,
|
|
28
|
+
laneCompletesAt,
|
|
29
|
+
currentTaskProgressFloatForLane,
|
|
30
|
+
} from './lane-core'
|
|
31
|
+
|
|
32
|
+
export function getLanes(entity: ScheduleData): LaneView[] {
|
|
33
|
+
const lanes = entity.lanes
|
|
34
|
+
if (!lanes || lanes.length === 0) return []
|
|
35
|
+
return lanes.map((l) => ({laneKey: l.lane_key.toNumber(), schedule: l.schedule}))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getLane(entity: ScheduleData, laneKey: number): LaneView | undefined {
|
|
39
|
+
const lanes = entity.lanes
|
|
40
|
+
if (!lanes) return undefined
|
|
41
|
+
for (const l of lanes) {
|
|
42
|
+
if (l.lane_key.toNumber() === laneKey) return {laneKey, schedule: l.schedule}
|
|
43
|
+
}
|
|
44
|
+
return undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function mobilityLane(entity: ScheduleData): LaneView | undefined {
|
|
48
|
+
return getLane(entity, LANE_MOBILITY)
|
|
29
49
|
}
|
|
30
50
|
|
|
31
51
|
export function hasSchedule(entity: ScheduleData): boolean {
|
|
32
|
-
|
|
52
|
+
const lanes = entity.lanes
|
|
53
|
+
if (!lanes) return false
|
|
54
|
+
return lanes.some((l) => l.schedule.tasks.length > 0)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function hasHolds(entity: ScheduleData): boolean {
|
|
58
|
+
const holds = entity.holds
|
|
59
|
+
return !!holds && holds.length > 0
|
|
33
60
|
}
|
|
34
61
|
|
|
35
62
|
export function isIdle(entity: ScheduleData): boolean {
|
|
36
|
-
return !hasSchedule(entity)
|
|
63
|
+
return !hasSchedule(entity) && !hasHolds(entity)
|
|
64
|
+
}
|
|
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
|
+
|
|
80
|
+
export function isEntityIdle(entity: ScheduleData, now: Date): boolean {
|
|
81
|
+
if (hasHolds(entity)) return false
|
|
82
|
+
const lanes = entity.lanes
|
|
83
|
+
if (!lanes) return true
|
|
84
|
+
return lanes.every((l) => core.currentTaskIndexForLane(l.schedule, now) < 0)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function entityIdleAt(entity: ScheduleData, _now: Date): Date | undefined {
|
|
88
|
+
const lanes = entity.lanes
|
|
89
|
+
if (!lanes) return undefined
|
|
90
|
+
let maxMs: number | undefined
|
|
91
|
+
for (const l of lanes) {
|
|
92
|
+
if (l.schedule.tasks.length === 0) continue
|
|
93
|
+
const endMs = l.schedule.started.toDate().getTime() + core.laneDuration(l.schedule) * 1000
|
|
94
|
+
if (maxMs === undefined || endMs > maxMs) maxMs = endMs
|
|
95
|
+
}
|
|
96
|
+
return maxMs === undefined ? undefined : new Date(maxMs)
|
|
37
97
|
}
|
|
38
98
|
|
|
39
99
|
export function getTasks(entity: ScheduleData): Task[] {
|
|
40
|
-
|
|
100
|
+
const lanes = entity.lanes
|
|
101
|
+
if (!lanes) return []
|
|
102
|
+
return lanes.flatMap((l) => l.schedule.tasks)
|
|
41
103
|
}
|
|
42
104
|
|
|
43
105
|
export function scheduleDuration(entity: ScheduleData): number {
|
|
44
|
-
|
|
45
|
-
|
|
106
|
+
let max = 0
|
|
107
|
+
for (const l of entity.lanes ?? []) max = Math.max(max, core.laneDuration(l.schedule))
|
|
108
|
+
return max
|
|
46
109
|
}
|
|
47
110
|
|
|
48
111
|
export function scheduleElapsed(entity: ScheduleData, now: Date): number {
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
return Math.max(0, elapsed)
|
|
112
|
+
let max = 0
|
|
113
|
+
for (const l of entity.lanes ?? []) max = Math.max(max, core.laneElapsed(l.schedule, now))
|
|
114
|
+
return max
|
|
53
115
|
}
|
|
54
116
|
|
|
55
117
|
export function scheduleRemaining(entity: ScheduleData, now: Date): number {
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
118
|
+
let remaining = 0
|
|
119
|
+
for (const l of entity.lanes ?? []) {
|
|
120
|
+
remaining = Math.max(remaining, core.laneRemaining(l.schedule, now))
|
|
121
|
+
}
|
|
122
|
+
return remaining
|
|
60
123
|
}
|
|
61
124
|
|
|
62
125
|
export function scheduleComplete(entity: ScheduleData, now: Date): boolean {
|
|
63
|
-
|
|
126
|
+
const lanes = entity.lanes
|
|
127
|
+
if (!lanes) return false
|
|
128
|
+
let hasAnyTask = false
|
|
129
|
+
let remaining = 0
|
|
130
|
+
for (const l of lanes) {
|
|
131
|
+
if (l.schedule.tasks.length > 0) hasAnyTask = true
|
|
132
|
+
remaining = Math.max(remaining, core.laneRemaining(l.schedule, now))
|
|
133
|
+
}
|
|
134
|
+
if (!hasAnyTask) return false
|
|
135
|
+
return remaining === 0
|
|
64
136
|
}
|
|
65
137
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const taskDuration = entity.schedule.tasks[i].duration.toNumber()
|
|
74
|
-
if (elapsed < timeAccum + taskDuration) {
|
|
75
|
-
return i
|
|
76
|
-
}
|
|
77
|
-
timeAccum += taskDuration
|
|
138
|
+
// Mirrors lane_front_complete && !capper_front_gated for own-entity holds (workshop/undeploy-target gates need cross-entity context this ScheduleData lacks).
|
|
139
|
+
export function hasResolvable(entity: ScheduleData, now: Date): boolean {
|
|
140
|
+
for (const l of entity.lanes ?? []) {
|
|
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
|
|
78
145
|
}
|
|
146
|
+
return false
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function currentTaskForLane(
|
|
150
|
+
entity: ScheduleData,
|
|
151
|
+
laneKey: number,
|
|
152
|
+
now: Date
|
|
153
|
+
): Task | undefined {
|
|
154
|
+
const lane = getLane(entity, laneKey)
|
|
155
|
+
return lane ? core.currentTask(lane.schedule, now) : undefined
|
|
156
|
+
}
|
|
79
157
|
|
|
80
|
-
|
|
158
|
+
export function currentTaskTypeForLane(
|
|
159
|
+
entity: ScheduleData,
|
|
160
|
+
laneKey: number,
|
|
161
|
+
now: Date
|
|
162
|
+
): TaskType | undefined {
|
|
163
|
+
const lane = getLane(entity, laneKey)
|
|
164
|
+
return lane ? core.currentTaskType(lane.schedule, now) : undefined
|
|
81
165
|
}
|
|
82
166
|
|
|
83
|
-
export function
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
167
|
+
export function activeTasks(entity: ScheduleData, now: Date): Task[] {
|
|
168
|
+
const out: Task[] = []
|
|
169
|
+
for (const l of entity.lanes ?? []) {
|
|
170
|
+
const idx = core.currentTaskIndexForLane(l.schedule, now)
|
|
171
|
+
if (idx >= 0) out.push(l.schedule.tasks[idx])
|
|
172
|
+
}
|
|
173
|
+
return out
|
|
87
174
|
}
|
|
88
175
|
|
|
89
|
-
export
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
176
|
+
export interface ResolvedEvent {
|
|
177
|
+
laneKey: number
|
|
178
|
+
taskIndex: number
|
|
179
|
+
task: Task
|
|
180
|
+
completesAt: Date
|
|
93
181
|
}
|
|
94
182
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
183
|
+
// Canonical lane-front order (mirrors contract front_precedes): completion, then RECHARGE-last, then lane key.
|
|
184
|
+
function frontPrecedes(
|
|
185
|
+
a: {completesAt: Date; task: Task; laneKey: number},
|
|
186
|
+
b: {completesAt: Date; task: Task; laneKey: number}
|
|
187
|
+
): number {
|
|
188
|
+
if (a.completesAt.getTime() !== b.completesAt.getTime()) {
|
|
189
|
+
return a.completesAt.getTime() - b.completesAt.getTime()
|
|
100
190
|
}
|
|
101
|
-
|
|
191
|
+
const aRecharge = a.task.type.toNumber() === TaskType.RECHARGE
|
|
192
|
+
const bRecharge = b.task.type.toNumber() === TaskType.RECHARGE
|
|
193
|
+
if (aRecharge !== bRecharge) return aRecharge ? 1 : -1
|
|
194
|
+
return a.laneKey - b.laneKey
|
|
102
195
|
}
|
|
103
196
|
|
|
104
|
-
|
|
105
|
-
|
|
197
|
+
// Completed lane-fronts in canonical order (mirrors contract front_precedes).
|
|
198
|
+
export function resolveOrder(entity: ScheduleData, now: Date): ResolvedEvent[] {
|
|
199
|
+
const events: ResolvedEvent[] = []
|
|
200
|
+
for (const l of entity.lanes ?? []) {
|
|
201
|
+
const laneKey = l.lane_key.toNumber()
|
|
202
|
+
const startedMs = l.schedule.started.toDate().getTime()
|
|
203
|
+
let endSec = 0
|
|
204
|
+
for (let i = 0; i < l.schedule.tasks.length; i++) {
|
|
205
|
+
const task = l.schedule.tasks[i]
|
|
206
|
+
endSec += task.duration.toNumber()
|
|
207
|
+
const completesAt = new Date(startedMs + endSec * 1000)
|
|
208
|
+
if (completesAt.getTime() > now.getTime()) break
|
|
209
|
+
events.push({laneKey, taskIndex: i, task, completesAt})
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
events.sort(frontPrecedes)
|
|
213
|
+
return events
|
|
214
|
+
}
|
|
106
215
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
216
|
+
export interface OrderedTask {
|
|
217
|
+
laneKey: number
|
|
218
|
+
taskIndex: number
|
|
219
|
+
task: Task
|
|
220
|
+
startsAt: Date
|
|
221
|
+
completesAt: Date
|
|
222
|
+
}
|
|
110
223
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
224
|
+
// Every task across all lanes in canonical order (mirrors contract front_precedes).
|
|
225
|
+
export function orderedTasks(entity: ScheduleData): OrderedTask[] {
|
|
226
|
+
const out: OrderedTask[] = []
|
|
227
|
+
for (const l of entity.lanes ?? []) {
|
|
228
|
+
const laneKey = l.lane_key.toNumber()
|
|
229
|
+
const startedMs = l.schedule.started.toDate().getTime()
|
|
230
|
+
let endSec = 0
|
|
231
|
+
for (let i = 0; i < l.schedule.tasks.length; i++) {
|
|
232
|
+
const task = l.schedule.tasks[i]
|
|
233
|
+
const startsAt = new Date(startedMs + endSec * 1000)
|
|
234
|
+
endSec += task.duration.toNumber()
|
|
235
|
+
const completesAt = new Date(startedMs + endSec * 1000)
|
|
236
|
+
out.push({laneKey, taskIndex: i, task, startsAt, completesAt})
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
out.sort(frontPrecedes)
|
|
240
|
+
return out
|
|
114
241
|
}
|
|
115
242
|
|
|
116
|
-
export function
|
|
117
|
-
|
|
243
|
+
export function laneRemainingOf(entity: ScheduleData, laneKey: number, now: Date): number {
|
|
244
|
+
const lane = getLane(entity, laneKey)
|
|
245
|
+
return lane ? core.laneRemaining(lane.schedule, now) : 0
|
|
246
|
+
}
|
|
118
247
|
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
return
|
|
248
|
+
export function laneStartsInOf(entity: ScheduleData, laneKey: number, now: Date): number {
|
|
249
|
+
const lane = getLane(entity, laneKey)
|
|
250
|
+
return lane ? core.laneStartsIn(lane.schedule, now) : 0
|
|
122
251
|
}
|
|
123
252
|
|
|
124
|
-
export function
|
|
125
|
-
|
|
253
|
+
export function laneCompleteOf(entity: ScheduleData, laneKey: number, now: Date): boolean {
|
|
254
|
+
const lane = getLane(entity, laneKey)
|
|
255
|
+
return lane ? core.laneComplete(lane.schedule, now) : false
|
|
256
|
+
}
|
|
126
257
|
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
return
|
|
258
|
+
export function laneProgressOf(entity: ScheduleData, laneKey: number, now: Date): number {
|
|
259
|
+
const lane = getLane(entity, laneKey)
|
|
260
|
+
return lane ? core.laneProgress(lane.schedule, now) : 0
|
|
130
261
|
}
|
|
131
262
|
|
|
132
|
-
export function
|
|
133
|
-
|
|
263
|
+
export function laneTaskElapsedOf(
|
|
264
|
+
entity: ScheduleData,
|
|
265
|
+
laneKey: number,
|
|
266
|
+
index: number,
|
|
267
|
+
now: Date
|
|
268
|
+
): number {
|
|
269
|
+
const lane = getLane(entity, laneKey)
|
|
270
|
+
return lane ? core.laneTaskElapsed(lane.schedule, index, now) : 0
|
|
271
|
+
}
|
|
134
272
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
273
|
+
export function laneTaskRemainingOf(
|
|
274
|
+
entity: ScheduleData,
|
|
275
|
+
laneKey: number,
|
|
276
|
+
index: number,
|
|
277
|
+
now: Date
|
|
278
|
+
): number {
|
|
279
|
+
const lane = getLane(entity, laneKey)
|
|
280
|
+
return lane ? core.laneTaskRemaining(lane.schedule, index, now) : 0
|
|
138
281
|
}
|
|
139
282
|
|
|
140
|
-
export function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
return
|
|
283
|
+
export function laneTaskCompleteOf(
|
|
284
|
+
entity: ScheduleData,
|
|
285
|
+
laneKey: number,
|
|
286
|
+
index: number,
|
|
287
|
+
now: Date
|
|
288
|
+
): boolean {
|
|
289
|
+
const lane = getLane(entity, laneKey)
|
|
290
|
+
return lane ? core.laneTaskComplete(lane.schedule, index, now) : false
|
|
148
291
|
}
|
|
149
292
|
|
|
150
|
-
export function
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const taskStartMs = startedMs + getTaskStartTime(entity, index) * 1000
|
|
159
|
-
const elapsedMs = now.getTime() - taskStartMs
|
|
160
|
-
if (elapsedMs <= 0) return 0
|
|
161
|
-
return Math.min(1, elapsedMs / durationMs)
|
|
293
|
+
export function laneTaskInProgressOf(
|
|
294
|
+
entity: ScheduleData,
|
|
295
|
+
laneKey: number,
|
|
296
|
+
index: number,
|
|
297
|
+
now: Date
|
|
298
|
+
): boolean {
|
|
299
|
+
const lane = getLane(entity, laneKey)
|
|
300
|
+
return lane ? core.laneTaskInProgress(lane.schedule, index, now) : false
|
|
162
301
|
}
|
|
163
302
|
|
|
164
|
-
export function
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
const elapsed = scheduleElapsed(entity, now)
|
|
168
|
-
return Math.min(1, elapsed / duration)
|
|
303
|
+
export function currentTaskIndexOf(entity: ScheduleData, laneKey: number, now: Date): number {
|
|
304
|
+
const lane = getLane(entity, laneKey)
|
|
305
|
+
return lane ? core.currentTaskIndexForLane(lane.schedule, now) : -1
|
|
169
306
|
}
|
|
170
307
|
|
|
171
|
-
|
|
172
|
-
return
|
|
308
|
+
function entityDoesTaskType(entity: ScheduleData, taskType: TaskType, now: Date): boolean {
|
|
309
|
+
return activeTasks(entity, now).some((t) => t.type.toNumber() === taskType)
|
|
173
310
|
}
|
|
174
311
|
|
|
175
312
|
export function isInFlight(entity: ScheduleData, now: Date): boolean {
|
|
176
|
-
|
|
313
|
+
const lane = mobilityLane(entity)
|
|
314
|
+
if (!lane) return false
|
|
315
|
+
const t = core.currentTaskType(lane.schedule, now)
|
|
316
|
+
return t === TaskType.TRAVEL || t === TaskType.TRANSIT
|
|
177
317
|
}
|
|
178
318
|
|
|
179
319
|
export function isRecharging(entity: ScheduleData, now: Date): boolean {
|
|
180
|
-
return
|
|
320
|
+
return entityDoesTaskType(entity, TaskType.RECHARGE, now)
|
|
181
321
|
}
|
|
182
322
|
|
|
183
323
|
export function isLoading(entity: ScheduleData, now: Date): boolean {
|
|
184
|
-
return
|
|
324
|
+
return entityDoesTaskType(entity, TaskType.LOAD, now)
|
|
185
325
|
}
|
|
186
326
|
|
|
187
327
|
export function isUnloading(entity: ScheduleData, now: Date): boolean {
|
|
188
|
-
return
|
|
328
|
+
return entityDoesTaskType(entity, TaskType.UNLOAD, now)
|
|
189
329
|
}
|
|
190
330
|
|
|
191
331
|
export function isGathering(entity: ScheduleData, now: Date): boolean {
|
|
192
|
-
return
|
|
332
|
+
return entityDoesTaskType(entity, TaskType.GATHER, now)
|
|
193
333
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {describe, expect, test} from 'bun:test'
|
|
2
|
+
import {ServerContract, TaskType} from '../index-module'
|
|
3
|
+
import {taskCargoChanges} from './task-cargo'
|
|
4
|
+
|
|
5
|
+
function cargoItem(itemId: number, quantity: number) {
|
|
6
|
+
return ServerContract.Types.cargo_item.from({item_id: itemId, stats: 0, modules: [], quantity})
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function craftTask(couplings: ReturnType<typeof coupling>[]) {
|
|
10
|
+
return ServerContract.Types.task.from({
|
|
11
|
+
type: TaskType.CRAFT,
|
|
12
|
+
duration: 60,
|
|
13
|
+
cancelable: 0,
|
|
14
|
+
cargo: [cargoItem(401, 19680), cargoItem(10008, 1968)],
|
|
15
|
+
couplings,
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function coupling() {
|
|
20
|
+
return ServerContract.Types.coupling.from({
|
|
21
|
+
counterpart: {entity_type: 'warehouse', entity_id: 51},
|
|
22
|
+
hold: 1,
|
|
23
|
+
kind: 3,
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('taskCargoChanges', () => {
|
|
28
|
+
test('a self-craft consumes inputs and delivers the output to the crafter', () => {
|
|
29
|
+
const changes = taskCargoChanges(craftTask([]))
|
|
30
|
+
|
|
31
|
+
expect(changes).toEqual([
|
|
32
|
+
{direction: 'out', item_id: 401, stats: 0n, modules: [], quantity: 19680},
|
|
33
|
+
{direction: 'in', item_id: 10008, stats: 0n, modules: [], quantity: 1968},
|
|
34
|
+
])
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('a coupled craft consumes inputs but does not deliver the output to the crafter', () => {
|
|
38
|
+
const changes = taskCargoChanges(craftTask([coupling()]))
|
|
39
|
+
|
|
40
|
+
expect(changes).toEqual([
|
|
41
|
+
{direction: 'out', item_id: 401, stats: 0n, modules: [], quantity: 19680},
|
|
42
|
+
])
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type {ServerContract} from '../contracts'
|
|
2
|
+
import {TaskType} from '../types'
|
|
3
|
+
|
|
4
|
+
export type TaskCargoDirection = 'in' | 'out'
|
|
5
|
+
|
|
6
|
+
export interface TaskCargoChange {
|
|
7
|
+
direction: TaskCargoDirection
|
|
8
|
+
item_id: number
|
|
9
|
+
stats: bigint
|
|
10
|
+
modules: ServerContract.Types.module_entry[]
|
|
11
|
+
quantity: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function toChange(
|
|
15
|
+
item: ServerContract.Types.cargo_item,
|
|
16
|
+
direction: TaskCargoDirection
|
|
17
|
+
): TaskCargoChange {
|
|
18
|
+
return {
|
|
19
|
+
direction,
|
|
20
|
+
item_id: Number(item.item_id),
|
|
21
|
+
stats: BigInt(item.stats.toString()),
|
|
22
|
+
modules: item.modules ?? [],
|
|
23
|
+
quantity: Number(item.quantity),
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function taskCargoChanges(task: ServerContract.Types.task): TaskCargoChange[] {
|
|
28
|
+
const items = task.cargo ?? []
|
|
29
|
+
if (items.length === 0) return []
|
|
30
|
+
switch (Number(task.type)) {
|
|
31
|
+
case TaskType.LOAD:
|
|
32
|
+
case TaskType.UNWRAP:
|
|
33
|
+
return items.map((i) => toChange(i, 'in'))
|
|
34
|
+
case TaskType.GATHER:
|
|
35
|
+
return task.couplings.length > 0 ? [] : items.map((i) => toChange(i, 'in'))
|
|
36
|
+
case TaskType.UNLOAD:
|
|
37
|
+
return items.map((i) => toChange(i, 'out'))
|
|
38
|
+
case TaskType.CRAFT:
|
|
39
|
+
return [
|
|
40
|
+
...items.slice(0, -1).map((i) => toChange(i, 'out')),
|
|
41
|
+
...(task.couplings.length > 0 ? [] : [toChange(items[items.length - 1], 'in')]),
|
|
42
|
+
]
|
|
43
|
+
default:
|
|
44
|
+
return []
|
|
45
|
+
}
|
|
46
|
+
}
|