@shipload/sdk 1.0.0-next.7 → 1.0.0-next.71
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 +5822 -1484
- package/lib/shipload.js +20838 -5419
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +20674 -5669
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +1691 -0
- package/lib/testing.js +7109 -0
- package/lib/testing.js.map +1 -0
- package/lib/testing.m.js +7103 -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 +80 -0
- package/src/capabilities/crafting.ts +50 -8
- package/src/capabilities/gathering.test.ts +29 -0
- package/src/capabilities/gathering.ts +38 -20
- 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.test.ts +26 -0
- package/src/capabilities/storage.ts +22 -7
- package/src/contracts/fund.ts +306 -0
- package/src/contracts/platform.ts +312 -3
- package/src/contracts/server.ts +2793 -478
- package/src/contracts/token.ts +170 -0
- package/src/contracts.ts +2 -0
- 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.test.ts +19 -0
- package/src/data/catalog.ts +13 -3
- package/src/data/charters.json +396 -0
- package/src/data/citizenry-adjectives.json +42 -0
- package/src/data/citizenry-nouns.json +50 -0
- package/src/data/colors.ts +14 -32
- package/src/data/entities.json +421 -15
- package/src/data/item-ids.ts +53 -13
- package/src/data/items.json +330 -77
- package/src/data/kind-registry.json +212 -0
- package/src/data/kind-registry.ts +192 -0
- package/src/data/metadata.ts +312 -51
- package/src/data/recipes-runtime.ts +6 -23
- package/src/data/recipes.json +2173 -173
- package/src/data/tiers.ts +9 -6
- package/src/derivation/build-methods.test.ts +71 -0
- package/src/derivation/build-methods.ts +50 -0
- package/src/derivation/capabilities.test.ts +348 -0
- package/src/derivation/capabilities.ts +707 -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 +88 -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/strata.ts +5 -3
- 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 +144 -136
- package/src/entities/slot-multiplier.ts +43 -0
- package/src/entity/hydrate.test.ts +156 -0
- package/src/entity/hydrate.ts +86 -0
- package/src/errors.ts +21 -16
- package/src/format.ts +42 -9
- package/src/index-module.ts +425 -32
- package/src/influence/ballot.test.ts +103 -0
- package/src/influence/ballot.ts +98 -0
- package/src/influence/categories.ts +32 -0
- package/src/influence/charter-metadata.test.ts +136 -0
- package/src/influence/charter-metadata.ts +236 -0
- package/src/influence/charters.test.ts +112 -0
- package/src/influence/charters.ts +150 -0
- package/src/influence/citizenry.ts +38 -0
- package/src/influence/civic-shuttle.test.ts +202 -0
- package/src/influence/civic-shuttle.ts +142 -0
- package/src/influence/civic.test.ts +287 -0
- package/src/influence/civic.ts +269 -0
- package/src/influence/constants.ts +84 -0
- package/src/influence/decay.ts +20 -0
- package/src/influence/decomp.ts +95 -0
- package/src/influence/demand.ts +106 -0
- package/src/influence/duration.ts +101 -0
- package/src/influence/index.ts +118 -0
- package/src/influence/influence.test.ts +450 -0
- package/src/influence/quality.ts +25 -0
- package/src/influence/valuation.ts +154 -0
- package/src/managers/actions.ts +950 -93
- package/src/managers/balances.test.ts +161 -0
- package/src/managers/balances.ts +93 -0
- package/src/managers/base.ts +10 -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 +453 -0
- package/src/managers/context.ts +58 -1
- package/src/managers/coordinates.ts +14 -0
- package/src/managers/entities.ts +35 -63
- package/src/managers/epochs.ts +72 -1
- package/src/managers/index.ts +38 -1
- package/src/managers/influence.test.ts +56 -0
- package/src/managers/influence.ts +425 -0
- package/src/managers/jobs.test.ts +96 -0
- package/src/managers/jobs.ts +58 -0
- package/src/managers/locations.ts +25 -29
- package/src/managers/nft.test.ts +21 -0
- package/src/managers/nft.ts +110 -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 +222 -100
- package/src/nft/index.ts +3 -0
- package/src/planner/index.ts +296 -0
- package/src/planner/planner.test.ts +459 -0
- package/src/receipts.ts +22 -0
- package/src/resolution/describe-module.ts +43 -25
- package/src/resolution/display-name.ts +39 -11
- 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 +320 -0
- package/src/scheduling/availability.ts +251 -0
- package/src/scheduling/cancel.test.ts +550 -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/epoch.ts +1 -0
- package/src/scheduling/idle-resolve.ts +45 -0
- package/src/scheduling/jobs.test.ts +448 -0
- package/src/scheduling/jobs.ts +343 -0
- package/src/scheduling/lane-core.ts +128 -0
- package/src/scheduling/lanes.test.ts +250 -0
- package/src/scheduling/lanes.ts +279 -0
- package/src/scheduling/projection.ts +265 -149
- package/src/scheduling/schedule.test.ts +163 -0
- package/src/scheduling/schedule.ts +282 -116
- package/src/scheduling/task-cargo.test.ts +110 -0
- package/src/scheduling/task-cargo.ts +62 -0
- package/src/scheduling/task-effects.test.ts +35 -0
- package/src/scheduling/task-effects.ts +34 -0
- package/src/scheduling/unwrap.test.ts +86 -0
- package/src/scheduling/unwrap.ts +190 -0
- package/src/shipload.ts +63 -4
- 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.test.ts +86 -0
- package/src/travel/reach.ts +34 -0
- package/src/travel/route-build.test.ts +63 -0
- package/src/travel/route-build.ts +60 -0
- package/src/travel/route-planner.ts +288 -0
- package/src/travel/route-simulator.ts +170 -0
- package/src/travel/travel.ts +190 -123
- 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 +81 -23
- package/src/utils/cargo.test.ts +14 -0
- package/src/utils/cargo.ts +2 -0
- package/src/utils/display-name.ts +70 -0
- package/src/utils/hash.ts +22 -2
- package/src/utils/system.ts +24 -5
- 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,163 @@
|
|
|
1
|
+
import {describe, expect, test} from 'bun:test'
|
|
2
|
+
import {ServerContract, TaskType} from '../index-module'
|
|
3
|
+
import {
|
|
4
|
+
appliedTaskCount,
|
|
5
|
+
hasPendingCapper,
|
|
6
|
+
hasResolvable,
|
|
7
|
+
isCapperTaskType,
|
|
8
|
+
orderedTasks,
|
|
9
|
+
unappliedTasks,
|
|
10
|
+
} from './schedule'
|
|
11
|
+
|
|
12
|
+
const T0 = '2026-06-19T00:00:00'
|
|
13
|
+
const NOW = new Date('2026-06-19T00:01:00.000Z')
|
|
14
|
+
|
|
15
|
+
function task(over: Partial<{type: number; duration: number}>) {
|
|
16
|
+
return ServerContract.Types.task.from({
|
|
17
|
+
type: over.type ?? TaskType.TRAVEL,
|
|
18
|
+
duration: over.duration ?? 30,
|
|
19
|
+
cancelable: 0,
|
|
20
|
+
cargo: [],
|
|
21
|
+
couplings: [],
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function entity(
|
|
26
|
+
tasks: ReturnType<typeof task>[],
|
|
27
|
+
holds: ServerContract.Types.hold[] = [],
|
|
28
|
+
startedISO = T0
|
|
29
|
+
) {
|
|
30
|
+
return ServerContract.Types.entity_info.from({
|
|
31
|
+
projected_at: 0,
|
|
32
|
+
type: 'ship',
|
|
33
|
+
id: 1,
|
|
34
|
+
owner: 'player.gm',
|
|
35
|
+
entity_name: 'Ship 1',
|
|
36
|
+
coordinates: {x: 0, y: 0, z: 0},
|
|
37
|
+
item_id: 1,
|
|
38
|
+
cargomass: 0,
|
|
39
|
+
cargo: [],
|
|
40
|
+
modules: [],
|
|
41
|
+
lanes: [{lane_key: 0, schedule: {started: startedISO, tasks}}],
|
|
42
|
+
gatherer_lanes: [],
|
|
43
|
+
crafter_lanes: [],
|
|
44
|
+
builder_lanes: [],
|
|
45
|
+
loader_lanes: [],
|
|
46
|
+
holds,
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function hold(kind: number) {
|
|
51
|
+
return ServerContract.Types.hold.from({
|
|
52
|
+
id: 1,
|
|
53
|
+
kind,
|
|
54
|
+
counterpart: {entity_type: 'ship', entity_id: 2},
|
|
55
|
+
until: T0,
|
|
56
|
+
incoming_mass: 0,
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('isCapperTaskType', () => {
|
|
61
|
+
test('UNDEPLOY, DEMOLISH are cappers', () => {
|
|
62
|
+
expect(isCapperTaskType(TaskType.UNDEPLOY)).toBe(true)
|
|
63
|
+
expect(isCapperTaskType(TaskType.DEMOLISH)).toBe(true)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('REFIT is not a capper', () => {
|
|
67
|
+
expect(isCapperTaskType(TaskType.REFIT)).toBe(false)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('non-capper task types are not cappers', () => {
|
|
71
|
+
expect(isCapperTaskType(TaskType.TRAVEL)).toBe(false)
|
|
72
|
+
expect(isCapperTaskType(TaskType.LOAD)).toBe(false)
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('hasPendingCapper', () => {
|
|
77
|
+
test('false with no schedule', () => {
|
|
78
|
+
expect(hasPendingCapper(entity([]))).toBe(false)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('false with only non-capper tasks queued', () => {
|
|
82
|
+
expect(hasPendingCapper(entity([task({type: TaskType.TRAVEL})]))).toBe(false)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('true when a DEMOLISH task is queued anywhere in the lane', () => {
|
|
86
|
+
const e = entity([task({type: TaskType.TRAVEL}), task({type: TaskType.DEMOLISH})])
|
|
87
|
+
expect(hasPendingCapper(e)).toBe(true)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('true when a DEMOLISH task is queued', () => {
|
|
91
|
+
expect(hasPendingCapper(entity([task({type: TaskType.DEMOLISH})]))).toBe(true)
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('hasResolvable — capper gating', () => {
|
|
96
|
+
test('a completed non-capper front is resolvable', () => {
|
|
97
|
+
const e = entity([task({type: TaskType.TRAVEL, duration: 30})])
|
|
98
|
+
expect(hasResolvable(e, NOW)).toBe(true)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('a completed capper front with no holds is resolvable', () => {
|
|
102
|
+
const e = entity([task({type: TaskType.DEMOLISH, duration: 30})])
|
|
103
|
+
expect(hasResolvable(e, NOW)).toBe(true)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('a completed capper front with a live hold is gated', () => {
|
|
107
|
+
const e = entity([task({type: TaskType.DEMOLISH, duration: 30})], [hold(2)])
|
|
108
|
+
expect(hasResolvable(e, NOW)).toBe(false)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('a capper front clears once holds empty', () => {
|
|
112
|
+
const withHold = entity([task({type: TaskType.DEMOLISH, duration: 30})], [hold(2)])
|
|
113
|
+
const cleared = entity([task({type: TaskType.DEMOLISH, duration: 30})], [])
|
|
114
|
+
expect(hasResolvable(withHold, NOW)).toBe(false)
|
|
115
|
+
expect(hasResolvable(cleared, NOW)).toBe(true)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('a live hold does not gate a non-capper front', () => {
|
|
119
|
+
const e = entity([task({type: TaskType.TRAVEL, duration: 30})], [hold(2)])
|
|
120
|
+
expect(hasResolvable(e, NOW)).toBe(true)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('an in-progress front is not resolvable regardless of holds', () => {
|
|
124
|
+
const e = entity([task({type: TaskType.DEMOLISH, duration: 3600})])
|
|
125
|
+
expect(hasResolvable(e, NOW)).toBe(false)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
function anchoredEntity(anchorISO: string) {
|
|
130
|
+
const e = entity([
|
|
131
|
+
task({type: TaskType.TRAVEL, duration: 30}),
|
|
132
|
+
task({type: TaskType.TRAVEL, duration: 30}),
|
|
133
|
+
task({type: TaskType.TRAVEL, duration: 30}),
|
|
134
|
+
])
|
|
135
|
+
return ServerContract.Types.entity_info.from({
|
|
136
|
+
...ServerContract.Types.entity_info.from(e),
|
|
137
|
+
projected_at: anchorISO,
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
describe('appliedTaskCount / unappliedTasks', () => {
|
|
142
|
+
test('no anchor leaves every task unapplied', () => {
|
|
143
|
+
const e = entity([task({duration: 30}), task({duration: 30})])
|
|
144
|
+
expect(appliedTaskCount({lanes: e.lanes}, orderedTasks(e))).toBe(0)
|
|
145
|
+
expect(unappliedTasks({lanes: e.lanes})).toHaveLength(2)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('tasks completed at or before the anchor are already applied', () => {
|
|
149
|
+
const e = anchoredEntity('2026-06-19T00:01:00')
|
|
150
|
+
expect(appliedTaskCount(e, orderedTasks(e))).toBe(2)
|
|
151
|
+
expect(unappliedTasks(e)).toHaveLength(1)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('the anchor is inclusive, mirroring completed_task_count_at', () => {
|
|
155
|
+
const e = anchoredEntity('2026-06-19T00:00:30')
|
|
156
|
+
expect(appliedTaskCount(e, orderedTasks(e))).toBe(1)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('a whole schedule finished before the anchor leaves nothing unapplied', () => {
|
|
160
|
+
const e = anchoredEntity('2026-06-19T12:00:00')
|
|
161
|
+
expect(unappliedTasks(e)).toHaveLength(0)
|
|
162
|
+
})
|
|
163
|
+
})
|
|
@@ -1,193 +1,359 @@
|
|
|
1
|
+
import type {TimePoint} from '@wharfkit/antelope'
|
|
1
2
|
import type {ServerContract} from '../contracts'
|
|
2
3
|
import {TaskType} from '../types'
|
|
4
|
+
import * as core from './lane-core'
|
|
3
5
|
|
|
4
6
|
type Schedule = ServerContract.Types.schedule
|
|
5
7
|
type Task = ServerContract.Types.task
|
|
8
|
+
type Lane = ServerContract.Types.lane
|
|
9
|
+
type Hold = ServerContract.Types.hold
|
|
10
|
+
|
|
11
|
+
export const LANE_MOBILITY = 0
|
|
12
|
+
export const LANE_BARRIER = 255
|
|
6
13
|
|
|
7
14
|
export interface ScheduleData {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
15
|
+
lanes?: Lane[]
|
|
16
|
+
holds?: Hold[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AnchoredScheduleData extends ScheduleData {
|
|
20
|
+
projected_at?: TimePoint
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface LaneView {
|
|
24
|
+
laneKey: number
|
|
25
|
+
schedule: Schedule
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
laneStartsIn,
|
|
30
|
+
currentTaskIndexForLane,
|
|
31
|
+
laneTaskComplete,
|
|
32
|
+
laneTaskInProgress,
|
|
33
|
+
laneCompletesAt,
|
|
34
|
+
currentTaskProgressFloatForLane,
|
|
35
|
+
} from './lane-core'
|
|
36
|
+
|
|
37
|
+
export function getLanes(entity: ScheduleData): LaneView[] {
|
|
38
|
+
const lanes = entity.lanes
|
|
39
|
+
if (!lanes || lanes.length === 0) return []
|
|
40
|
+
return lanes.map((l) => ({laneKey: l.lane_key.toNumber(), schedule: l.schedule}))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getLane(entity: ScheduleData, laneKey: number): LaneView | undefined {
|
|
44
|
+
const lanes = entity.lanes
|
|
45
|
+
if (!lanes) return undefined
|
|
46
|
+
for (const l of lanes) {
|
|
47
|
+
if (l.lane_key.toNumber() === laneKey) return {laneKey, schedule: l.schedule}
|
|
48
|
+
}
|
|
49
|
+
return undefined
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function mobilityLane(entity: ScheduleData): LaneView | undefined {
|
|
53
|
+
return getLane(entity, LANE_MOBILITY)
|
|
29
54
|
}
|
|
30
55
|
|
|
31
56
|
export function hasSchedule(entity: ScheduleData): boolean {
|
|
32
|
-
|
|
57
|
+
const lanes = entity.lanes
|
|
58
|
+
if (!lanes) return false
|
|
59
|
+
return lanes.some((l) => l.schedule.tasks.length > 0)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function hasHolds(entity: ScheduleData): boolean {
|
|
63
|
+
const holds = entity.holds
|
|
64
|
+
return !!holds && holds.length > 0
|
|
33
65
|
}
|
|
34
66
|
|
|
35
67
|
export function isIdle(entity: ScheduleData): boolean {
|
|
36
|
-
return !hasSchedule(entity)
|
|
68
|
+
return !hasSchedule(entity) && !hasHolds(entity)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Mirrors is_capper_task_type: demolish/undeploy cap a plan — no further appends once queued.
|
|
72
|
+
export function isCapperTaskType(taskType: number): boolean {
|
|
73
|
+
return taskType === TaskType.UNDEPLOY || taskType === TaskType.DEMOLISH
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function hasPendingCapper(entity: ScheduleData): boolean {
|
|
77
|
+
for (const l of entity.lanes ?? []) {
|
|
78
|
+
for (const t of l.schedule.tasks) {
|
|
79
|
+
if (isCapperTaskType(t.type.toNumber())) return true
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function isEntityIdle(entity: ScheduleData, now: Date): boolean {
|
|
86
|
+
if (hasHolds(entity)) return false
|
|
87
|
+
const lanes = entity.lanes
|
|
88
|
+
if (!lanes) return true
|
|
89
|
+
return lanes.every((l) => core.currentTaskIndexForLane(l.schedule, now) < 0)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function entityIdleAt(entity: ScheduleData, _now: Date): Date | undefined {
|
|
93
|
+
const lanes = entity.lanes
|
|
94
|
+
if (!lanes) return undefined
|
|
95
|
+
let maxMs: number | undefined
|
|
96
|
+
for (const l of lanes) {
|
|
97
|
+
if (l.schedule.tasks.length === 0) continue
|
|
98
|
+
const endMs = l.schedule.started.toDate().getTime() + core.laneDuration(l.schedule) * 1000
|
|
99
|
+
if (maxMs === undefined || endMs > maxMs) maxMs = endMs
|
|
100
|
+
}
|
|
101
|
+
return maxMs === undefined ? undefined : new Date(maxMs)
|
|
37
102
|
}
|
|
38
103
|
|
|
39
104
|
export function getTasks(entity: ScheduleData): Task[] {
|
|
40
|
-
|
|
105
|
+
const lanes = entity.lanes
|
|
106
|
+
if (!lanes) return []
|
|
107
|
+
return lanes.flatMap((l) => l.schedule.tasks)
|
|
41
108
|
}
|
|
42
109
|
|
|
43
110
|
export function scheduleDuration(entity: ScheduleData): number {
|
|
44
|
-
|
|
45
|
-
|
|
111
|
+
let max = 0
|
|
112
|
+
for (const l of entity.lanes ?? []) max = Math.max(max, core.laneDuration(l.schedule))
|
|
113
|
+
return max
|
|
46
114
|
}
|
|
47
115
|
|
|
48
116
|
export function scheduleElapsed(entity: ScheduleData, now: Date): number {
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
return Math.max(0, elapsed)
|
|
117
|
+
let max = 0
|
|
118
|
+
for (const l of entity.lanes ?? []) max = Math.max(max, core.laneElapsed(l.schedule, now))
|
|
119
|
+
return max
|
|
53
120
|
}
|
|
54
121
|
|
|
55
122
|
export function scheduleRemaining(entity: ScheduleData, now: Date): number {
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
123
|
+
let remaining = 0
|
|
124
|
+
for (const l of entity.lanes ?? []) {
|
|
125
|
+
remaining = Math.max(remaining, core.laneRemaining(l.schedule, now))
|
|
126
|
+
}
|
|
127
|
+
return remaining
|
|
60
128
|
}
|
|
61
129
|
|
|
62
130
|
export function scheduleComplete(entity: ScheduleData, now: Date): boolean {
|
|
63
|
-
|
|
131
|
+
const lanes = entity.lanes
|
|
132
|
+
if (!lanes) return false
|
|
133
|
+
let hasAnyTask = false
|
|
134
|
+
let remaining = 0
|
|
135
|
+
for (const l of lanes) {
|
|
136
|
+
if (l.schedule.tasks.length > 0) hasAnyTask = true
|
|
137
|
+
remaining = Math.max(remaining, core.laneRemaining(l.schedule, now))
|
|
138
|
+
}
|
|
139
|
+
if (!hasAnyTask) return false
|
|
140
|
+
return remaining === 0
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Mirrors lane_front_complete && !capper_front_gated for own-entity holds (workshop/undeploy-target gates need cross-entity context this ScheduleData lacks).
|
|
144
|
+
export function hasResolvable(entity: ScheduleData, now: Date): boolean {
|
|
145
|
+
for (const l of entity.lanes ?? []) {
|
|
146
|
+
if (!core.laneTaskComplete(l.schedule, 0, now)) continue
|
|
147
|
+
const front = l.schedule.tasks[0]
|
|
148
|
+
if (isCapperTaskType(front.type.toNumber()) && hasHolds(entity)) continue
|
|
149
|
+
return true
|
|
150
|
+
}
|
|
151
|
+
return false
|
|
64
152
|
}
|
|
65
153
|
|
|
66
|
-
export function
|
|
67
|
-
|
|
154
|
+
export function currentTaskForLane(
|
|
155
|
+
entity: ScheduleData,
|
|
156
|
+
laneKey: number,
|
|
157
|
+
now: Date
|
|
158
|
+
): Task | undefined {
|
|
159
|
+
const lane = getLane(entity, laneKey)
|
|
160
|
+
return lane ? core.currentTask(lane.schedule, now) : undefined
|
|
161
|
+
}
|
|
68
162
|
|
|
69
|
-
|
|
70
|
-
|
|
163
|
+
export function currentTaskTypeForLane(
|
|
164
|
+
entity: ScheduleData,
|
|
165
|
+
laneKey: number,
|
|
166
|
+
now: Date
|
|
167
|
+
): TaskType | undefined {
|
|
168
|
+
const lane = getLane(entity, laneKey)
|
|
169
|
+
return lane ? core.currentTaskType(lane.schedule, now) : undefined
|
|
170
|
+
}
|
|
71
171
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
timeAccum += taskDuration
|
|
172
|
+
export function activeTasks(entity: ScheduleData, now: Date): Task[] {
|
|
173
|
+
const out: Task[] = []
|
|
174
|
+
for (const l of entity.lanes ?? []) {
|
|
175
|
+
const idx = core.currentTaskIndexForLane(l.schedule, now)
|
|
176
|
+
if (idx >= 0) out.push(l.schedule.tasks[idx])
|
|
78
177
|
}
|
|
79
|
-
|
|
80
|
-
return -1
|
|
178
|
+
return out
|
|
81
179
|
}
|
|
82
180
|
|
|
83
|
-
export
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
181
|
+
export interface ResolvedEvent {
|
|
182
|
+
laneKey: number
|
|
183
|
+
taskIndex: number
|
|
184
|
+
task: Task
|
|
185
|
+
completesAt: Date
|
|
87
186
|
}
|
|
88
187
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
188
|
+
// Canonical lane-front order (mirrors contract front_precedes): completion, then RECHARGE-last, then lane key.
|
|
189
|
+
function frontPrecedes(
|
|
190
|
+
a: {completesAt: Date; task: Task; laneKey: number},
|
|
191
|
+
b: {completesAt: Date; task: Task; laneKey: number}
|
|
192
|
+
): number {
|
|
193
|
+
if (a.completesAt.getTime() !== b.completesAt.getTime()) {
|
|
194
|
+
return a.completesAt.getTime() - b.completesAt.getTime()
|
|
195
|
+
}
|
|
196
|
+
const aRecharge = a.task.type.toNumber() === TaskType.RECHARGE
|
|
197
|
+
const bRecharge = b.task.type.toNumber() === TaskType.RECHARGE
|
|
198
|
+
if (aRecharge !== bRecharge) return aRecharge ? 1 : -1
|
|
199
|
+
return a.laneKey - b.laneKey
|
|
93
200
|
}
|
|
94
201
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
for (
|
|
99
|
-
|
|
202
|
+
// Completed lane-fronts in canonical order (mirrors contract front_precedes).
|
|
203
|
+
export function resolveOrder(entity: ScheduleData, now: Date): ResolvedEvent[] {
|
|
204
|
+
const events: ResolvedEvent[] = []
|
|
205
|
+
for (const l of entity.lanes ?? []) {
|
|
206
|
+
const laneKey = l.lane_key.toNumber()
|
|
207
|
+
const startedMs = l.schedule.started.toDate().getTime()
|
|
208
|
+
let endSec = 0
|
|
209
|
+
for (let i = 0; i < l.schedule.tasks.length; i++) {
|
|
210
|
+
const task = l.schedule.tasks[i]
|
|
211
|
+
endSec += task.duration.toNumber()
|
|
212
|
+
const completesAt = new Date(startedMs + endSec * 1000)
|
|
213
|
+
if (completesAt.getTime() > now.getTime()) break
|
|
214
|
+
events.push({laneKey, taskIndex: i, task, completesAt})
|
|
215
|
+
}
|
|
100
216
|
}
|
|
101
|
-
|
|
217
|
+
events.sort(frontPrecedes)
|
|
218
|
+
return events
|
|
102
219
|
}
|
|
103
220
|
|
|
104
|
-
export
|
|
105
|
-
|
|
221
|
+
export interface OrderedTask {
|
|
222
|
+
laneKey: number
|
|
223
|
+
taskIndex: number
|
|
224
|
+
task: Task
|
|
225
|
+
startsAt: Date
|
|
226
|
+
completesAt: Date
|
|
227
|
+
}
|
|
106
228
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
229
|
+
// Every task across all lanes in canonical order (mirrors contract front_precedes).
|
|
230
|
+
export function orderedTasks(entity: ScheduleData): OrderedTask[] {
|
|
231
|
+
const out: OrderedTask[] = []
|
|
232
|
+
for (const l of entity.lanes ?? []) {
|
|
233
|
+
const laneKey = l.lane_key.toNumber()
|
|
234
|
+
const startedMs = l.schedule.started.toDate().getTime()
|
|
235
|
+
let endSec = 0
|
|
236
|
+
for (let i = 0; i < l.schedule.tasks.length; i++) {
|
|
237
|
+
const task = l.schedule.tasks[i]
|
|
238
|
+
const startsAt = new Date(startedMs + endSec * 1000)
|
|
239
|
+
endSec += task.duration.toNumber()
|
|
240
|
+
const completesAt = new Date(startedMs + endSec * 1000)
|
|
241
|
+
out.push({laneKey, taskIndex: i, task, startsAt, completesAt})
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
out.sort(frontPrecedes)
|
|
245
|
+
return out
|
|
246
|
+
}
|
|
110
247
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
248
|
+
// Mirrors the contract's completed_task_count_at: inclusive at the anchor.
|
|
249
|
+
export function appliedTaskCount(
|
|
250
|
+
entity: AnchoredScheduleData,
|
|
251
|
+
ordered: readonly OrderedTask[]
|
|
252
|
+
): number {
|
|
253
|
+
const anchor = entity.projected_at
|
|
254
|
+
if (anchor === undefined) return 0
|
|
255
|
+
const anchorMs = Number(anchor.toMilliseconds())
|
|
256
|
+
let count = 0
|
|
257
|
+
for (const {completesAt} of ordered) {
|
|
258
|
+
if (completesAt.getTime() <= anchorMs) count++
|
|
259
|
+
}
|
|
260
|
+
return count
|
|
114
261
|
}
|
|
115
262
|
|
|
116
|
-
|
|
117
|
-
|
|
263
|
+
// Tasks an entity_info snapshot has NOT yet folded into its cargo and state.
|
|
264
|
+
export function unappliedTasks(entity: AnchoredScheduleData): OrderedTask[] {
|
|
265
|
+
const ordered = orderedTasks(entity)
|
|
266
|
+
return ordered.slice(appliedTaskCount(entity, ordered))
|
|
267
|
+
}
|
|
118
268
|
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
return
|
|
269
|
+
export function laneRemainingOf(entity: ScheduleData, laneKey: number, now: Date): number {
|
|
270
|
+
const lane = getLane(entity, laneKey)
|
|
271
|
+
return lane ? core.laneRemaining(lane.schedule, now) : 0
|
|
122
272
|
}
|
|
123
273
|
|
|
124
|
-
export function
|
|
125
|
-
|
|
274
|
+
export function laneStartsInOf(entity: ScheduleData, laneKey: number, now: Date): number {
|
|
275
|
+
const lane = getLane(entity, laneKey)
|
|
276
|
+
return lane ? core.laneStartsIn(lane.schedule, now) : 0
|
|
277
|
+
}
|
|
126
278
|
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
return
|
|
279
|
+
export function laneCompleteOf(entity: ScheduleData, laneKey: number, now: Date): boolean {
|
|
280
|
+
const lane = getLane(entity, laneKey)
|
|
281
|
+
return lane ? core.laneComplete(lane.schedule, now) : false
|
|
130
282
|
}
|
|
131
283
|
|
|
132
|
-
export function
|
|
133
|
-
|
|
284
|
+
export function laneProgressOf(entity: ScheduleData, laneKey: number, now: Date): number {
|
|
285
|
+
const lane = getLane(entity, laneKey)
|
|
286
|
+
return lane ? core.laneProgress(lane.schedule, now) : 0
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function laneTaskElapsedOf(
|
|
290
|
+
entity: ScheduleData,
|
|
291
|
+
laneKey: number,
|
|
292
|
+
index: number,
|
|
293
|
+
now: Date
|
|
294
|
+
): number {
|
|
295
|
+
const lane = getLane(entity, laneKey)
|
|
296
|
+
return lane ? core.laneTaskElapsed(lane.schedule, index, now) : 0
|
|
297
|
+
}
|
|
134
298
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
299
|
+
export function laneTaskRemainingOf(
|
|
300
|
+
entity: ScheduleData,
|
|
301
|
+
laneKey: number,
|
|
302
|
+
index: number,
|
|
303
|
+
now: Date
|
|
304
|
+
): number {
|
|
305
|
+
const lane = getLane(entity, laneKey)
|
|
306
|
+
return lane ? core.laneTaskRemaining(lane.schedule, index, now) : 0
|
|
138
307
|
}
|
|
139
308
|
|
|
140
|
-
export function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
return
|
|
309
|
+
export function laneTaskCompleteOf(
|
|
310
|
+
entity: ScheduleData,
|
|
311
|
+
laneKey: number,
|
|
312
|
+
index: number,
|
|
313
|
+
now: Date
|
|
314
|
+
): boolean {
|
|
315
|
+
const lane = getLane(entity, laneKey)
|
|
316
|
+
return lane ? core.laneTaskComplete(lane.schedule, index, now) : false
|
|
148
317
|
}
|
|
149
318
|
|
|
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)
|
|
319
|
+
export function laneTaskInProgressOf(
|
|
320
|
+
entity: ScheduleData,
|
|
321
|
+
laneKey: number,
|
|
322
|
+
index: number,
|
|
323
|
+
now: Date
|
|
324
|
+
): boolean {
|
|
325
|
+
const lane = getLane(entity, laneKey)
|
|
326
|
+
return lane ? core.laneTaskInProgress(lane.schedule, index, now) : false
|
|
162
327
|
}
|
|
163
328
|
|
|
164
|
-
export function
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
const elapsed = scheduleElapsed(entity, now)
|
|
168
|
-
return Math.min(1, elapsed / duration)
|
|
329
|
+
export function currentTaskIndexOf(entity: ScheduleData, laneKey: number, now: Date): number {
|
|
330
|
+
const lane = getLane(entity, laneKey)
|
|
331
|
+
return lane ? core.currentTaskIndexForLane(lane.schedule, now) : -1
|
|
169
332
|
}
|
|
170
333
|
|
|
171
|
-
|
|
172
|
-
return
|
|
334
|
+
function entityDoesTaskType(entity: ScheduleData, taskType: TaskType, now: Date): boolean {
|
|
335
|
+
return activeTasks(entity, now).some((t) => t.type.toNumber() === taskType)
|
|
173
336
|
}
|
|
174
337
|
|
|
175
338
|
export function isInFlight(entity: ScheduleData, now: Date): boolean {
|
|
176
|
-
|
|
339
|
+
const lane = mobilityLane(entity)
|
|
340
|
+
if (!lane) return false
|
|
341
|
+
const t = core.currentTaskType(lane.schedule, now)
|
|
342
|
+
return t === TaskType.TRAVEL || t === TaskType.TRANSIT
|
|
177
343
|
}
|
|
178
344
|
|
|
179
345
|
export function isRecharging(entity: ScheduleData, now: Date): boolean {
|
|
180
|
-
return
|
|
346
|
+
return entityDoesTaskType(entity, TaskType.RECHARGE, now)
|
|
181
347
|
}
|
|
182
348
|
|
|
183
349
|
export function isLoading(entity: ScheduleData, now: Date): boolean {
|
|
184
|
-
return
|
|
350
|
+
return entityDoesTaskType(entity, TaskType.LOAD, now)
|
|
185
351
|
}
|
|
186
352
|
|
|
187
353
|
export function isUnloading(entity: ScheduleData, now: Date): boolean {
|
|
188
|
-
return
|
|
354
|
+
return entityDoesTaskType(entity, TaskType.UNLOAD, now)
|
|
189
355
|
}
|
|
190
356
|
|
|
191
357
|
export function isGathering(entity: ScheduleData, now: Date): boolean {
|
|
192
|
-
return
|
|
358
|
+
return entityDoesTaskType(entity, TaskType.GATHER, now)
|
|
193
359
|
}
|