@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,320 @@
|
|
|
1
|
+
import {describe, expect, test} from 'bun:test'
|
|
2
|
+
import {HoldKind, ServerContract, TaskType} from '../index-module'
|
|
3
|
+
import {
|
|
4
|
+
calcCounterpartDelivery,
|
|
5
|
+
cargoReadyAt,
|
|
6
|
+
type CargoInput,
|
|
7
|
+
hasIncomingCoupling,
|
|
8
|
+
hasSourceCoupling,
|
|
9
|
+
incomingSources,
|
|
10
|
+
type IncomingSource,
|
|
11
|
+
projectedCargoAvailableAt,
|
|
12
|
+
taskCargoEffect,
|
|
13
|
+
} from './availability'
|
|
14
|
+
|
|
15
|
+
const T0 = '2026-06-19T00:00:00'
|
|
16
|
+
const EPOCH = new Date(0)
|
|
17
|
+
|
|
18
|
+
function cargoItem(itemId: number, stats: number, quantity: number) {
|
|
19
|
+
return ServerContract.Types.cargo_item.from({item_id: itemId, stats, modules: [], quantity})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function task(over: {type?: number; duration?: number; cargo?: ReturnType<typeof cargoItem>[]}) {
|
|
23
|
+
return ServerContract.Types.task.from({
|
|
24
|
+
type: over.type ?? TaskType.LOAD,
|
|
25
|
+
duration: over.duration ?? 60,
|
|
26
|
+
cancelable: 0,
|
|
27
|
+
cargo: over.cargo ?? [],
|
|
28
|
+
couplings: [],
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function coupling(kind: number) {
|
|
33
|
+
return ServerContract.Types.coupling.from({
|
|
34
|
+
counterpart: {entity_type: 'ship', entity_id: 2},
|
|
35
|
+
hold: 1,
|
|
36
|
+
kind,
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function craftTask(
|
|
41
|
+
cargo: ReturnType<typeof cargoItem>[],
|
|
42
|
+
couplings: ReturnType<typeof coupling>[]
|
|
43
|
+
) {
|
|
44
|
+
return ServerContract.Types.task.from({
|
|
45
|
+
type: TaskType.CRAFT,
|
|
46
|
+
duration: 60,
|
|
47
|
+
cancelable: 0,
|
|
48
|
+
cargo,
|
|
49
|
+
couplings,
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function entity(
|
|
54
|
+
cargo: ReturnType<typeof cargoItem>[],
|
|
55
|
+
tasks: ReturnType<typeof task>[] = [],
|
|
56
|
+
startedISO = T0
|
|
57
|
+
) {
|
|
58
|
+
return ServerContract.Types.entity_info.from({
|
|
59
|
+
projected_at: 0,
|
|
60
|
+
type: 'ship',
|
|
61
|
+
id: 1,
|
|
62
|
+
owner: 'player.gm',
|
|
63
|
+
entity_name: 'Ship 1',
|
|
64
|
+
coordinates: {x: 0, y: 0, z: 0},
|
|
65
|
+
item_id: 1,
|
|
66
|
+
cargomass: 0,
|
|
67
|
+
cargo: cargo.map((c) => ({
|
|
68
|
+
item_id: c.item_id,
|
|
69
|
+
stats: c.stats,
|
|
70
|
+
modules: c.modules,
|
|
71
|
+
quantity: c.quantity,
|
|
72
|
+
id: 0,
|
|
73
|
+
})),
|
|
74
|
+
modules: [],
|
|
75
|
+
lanes: [{lane_key: 0, schedule: {started: startedISO, tasks}}],
|
|
76
|
+
gatherer_lanes: [],
|
|
77
|
+
crafter_lanes: [],
|
|
78
|
+
builder_lanes: [],
|
|
79
|
+
loader_lanes: [],
|
|
80
|
+
holds: [],
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe('cargoReadyAt', () => {
|
|
85
|
+
test('on-hand covers demand → epoch', () => {
|
|
86
|
+
const e = entity([cargoItem(7, 0, 10)])
|
|
87
|
+
const demand: CargoInput[] = [{itemId: 7, stats: 0n, quantity: 5}]
|
|
88
|
+
expect(cargoReadyAt(e, demand)).toEqual(EPOCH)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('own-lane LOAD covers demand → its completion', () => {
|
|
92
|
+
const loadCompletesAt = new Date('2026-06-19T00:01:00.000Z')
|
|
93
|
+
const e = entity(
|
|
94
|
+
[],
|
|
95
|
+
[task({type: TaskType.LOAD, duration: 60, cargo: [cargoItem(7, 0, 10)]})]
|
|
96
|
+
)
|
|
97
|
+
const demand: CargoInput[] = [{itemId: 7, stats: 0n, quantity: 5}]
|
|
98
|
+
expect(cargoReadyAt(e, demand)).toEqual(loadCompletesAt)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('incoming source covers demand → its until', () => {
|
|
102
|
+
const until = new Date('2026-06-19T00:00:30.000Z')
|
|
103
|
+
const e = entity([])
|
|
104
|
+
const incoming: IncomingSource[] = [{holdId: '1', until, items: [cargoItem(7, 0, 10)]}]
|
|
105
|
+
const demand: CargoInput[] = [{itemId: 7, stats: 0n, quantity: 5}]
|
|
106
|
+
expect(cargoReadyAt(e, demand, incoming)).toEqual(until)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('on-hand covers demand; unrelated same-item different-stats inflow does not delay readiness', () => {
|
|
110
|
+
const laterCompletesAt = new Date('2026-06-19T00:01:00.000Z')
|
|
111
|
+
const e = entity(
|
|
112
|
+
[cargoItem(7, 0, 10)],
|
|
113
|
+
[task({type: TaskType.LOAD, duration: 60, cargo: [cargoItem(7, 99, 10)]})]
|
|
114
|
+
)
|
|
115
|
+
const demand: CargoInput[] = [{itemId: 7, stats: 0n, quantity: 5}]
|
|
116
|
+
const result = cargoReadyAt(e, demand)
|
|
117
|
+
expect(result).toEqual(EPOCH)
|
|
118
|
+
expect(result).not.toEqual(laterCompletesAt)
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('calcCounterpartDelivery', () => {
|
|
123
|
+
test('coupled CRAFT task returns output cargo only', () => {
|
|
124
|
+
const output = cargoItem(9, 0, 1)
|
|
125
|
+
const t = task({
|
|
126
|
+
type: TaskType.CRAFT,
|
|
127
|
+
cargo: [cargoItem(7, 0, 2), cargoItem(8, 0, 1), output],
|
|
128
|
+
})
|
|
129
|
+
const result = calcCounterpartDelivery(t, coupling(HoldKind.PUSH))
|
|
130
|
+
expect(result.length).toBe(1)
|
|
131
|
+
expect(result[0].item_id.toNumber()).toBe(9)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('CRAFT task with empty cargo returns []', () => {
|
|
135
|
+
const t = task({type: TaskType.CRAFT, cargo: []})
|
|
136
|
+
expect(calcCounterpartDelivery(t, coupling(HoldKind.PUSH))).toEqual([])
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('coupled UNLOAD task returns whole cargo', () => {
|
|
140
|
+
const t = task({type: TaskType.UNLOAD, cargo: [cargoItem(7, 0, 2), cargoItem(8, 0, 1)]})
|
|
141
|
+
const result = calcCounterpartDelivery(t, coupling(HoldKind.GATHER))
|
|
142
|
+
expect(result.length).toBe(2)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('non-incoming coupling kind returns []', () => {
|
|
146
|
+
const t = task({type: TaskType.UNLOAD, cargo: [cargoItem(7, 0, 2)]})
|
|
147
|
+
expect(calcCounterpartDelivery(t, coupling(HoldKind.PULL))).toEqual([])
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
describe('projectedCargoAvailableAt — incoming', () => {
|
|
152
|
+
const until = new Date('2026-06-19T00:00:30.000Z')
|
|
153
|
+
|
|
154
|
+
function incomingFor(qty: number): IncomingSource[] {
|
|
155
|
+
return [{holdId: '1', until, items: [cargoItem(7, 0, qty)]}]
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
test('credits an incoming source strictly before its until', () => {
|
|
159
|
+
const e = entity([])
|
|
160
|
+
const avail = projectedCargoAvailableAt(e, new Date(until.getTime() + 1), incomingFor(10))
|
|
161
|
+
expect(avail.get('7:0')).toBe(10n)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('does not credit an incoming source at exactly its until', () => {
|
|
165
|
+
const e = entity([])
|
|
166
|
+
const avail = projectedCargoAvailableAt(e, until, incomingFor(10))
|
|
167
|
+
expect(avail.get('7:0') ?? 0n).toBe(0n)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('does not credit an incoming source before its until', () => {
|
|
171
|
+
const e = entity([])
|
|
172
|
+
const avail = projectedCargoAvailableAt(e, new Date(until.getTime() - 1), incomingFor(10))
|
|
173
|
+
expect(avail.get('7:0') ?? 0n).toBe(0n)
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
describe('clustercraft projection parity', () => {
|
|
178
|
+
const inputs = [cargoItem(1001, 0, 10), cargoItem(1002, 0, 5)]
|
|
179
|
+
const output = cargoItem(2001, 0, 1)
|
|
180
|
+
|
|
181
|
+
test('own-hold craft: inputs removed, output added', () => {
|
|
182
|
+
const eff = taskCargoEffect(craftTask([...inputs, output], []))
|
|
183
|
+
expect(eff.removed.length).toBe(2)
|
|
184
|
+
expect(eff.added).toEqual([output])
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
test('clustercraft to a target: no crafter debit, no crafter credit', () => {
|
|
188
|
+
const t = craftTask(
|
|
189
|
+
[...inputs, output],
|
|
190
|
+
[coupling(HoldKind.SOURCE), coupling(HoldKind.GATHER)]
|
|
191
|
+
)
|
|
192
|
+
expect(hasSourceCoupling(t)).toBe(true)
|
|
193
|
+
expect(hasIncomingCoupling(t)).toBe(true)
|
|
194
|
+
const eff = taskCargoEffect(t)
|
|
195
|
+
expect(eff.removed).toEqual([])
|
|
196
|
+
expect(eff.added).toEqual([])
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
test('clustercraft with target omitted: output falls back to crafter, still no input debit', () => {
|
|
200
|
+
const t = craftTask([...inputs, output], [coupling(HoldKind.SOURCE)])
|
|
201
|
+
expect(hasIncomingCoupling(t)).toBe(false)
|
|
202
|
+
const eff = taskCargoEffect(t)
|
|
203
|
+
expect(eff.removed).toEqual([])
|
|
204
|
+
expect(eff.added).toEqual([output])
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
function simpleTask(type: number) {
|
|
209
|
+
return ServerContract.Types.task.from({
|
|
210
|
+
type,
|
|
211
|
+
duration: 60,
|
|
212
|
+
cancelable: 0,
|
|
213
|
+
cargo: [cargoItem(401, 0, 100)],
|
|
214
|
+
couplings: [],
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
describe('taskCargoEffect task coverage', () => {
|
|
219
|
+
test('a contribute removes its cargo', () => {
|
|
220
|
+
const effect = taskCargoEffect(simpleTask(TaskType.CONTRIBUTE))
|
|
221
|
+
expect(effect.removed).toHaveLength(1)
|
|
222
|
+
expect(effect.added).toHaveLength(0)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test('a depot store removes its cargo', () => {
|
|
226
|
+
const effect = taskCargoEffect(simpleTask(TaskType.CIVIC_DEPOSIT))
|
|
227
|
+
expect(effect.removed).toHaveLength(1)
|
|
228
|
+
expect(effect.added).toHaveLength(0)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
test('a depot take adds its cargo', () => {
|
|
232
|
+
const effect = taskCargoEffect(simpleTask(TaskType.CIVIC_WITHDRAW))
|
|
233
|
+
expect(effect.added).toHaveLength(1)
|
|
234
|
+
expect(effect.removed).toHaveLength(0)
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
describe('incomingSources', () => {
|
|
239
|
+
const RECEIVER = 1
|
|
240
|
+
const SENDER = 2
|
|
241
|
+
|
|
242
|
+
function receiver(holds: {id: number; kind: number; from: number; until: string}[]) {
|
|
243
|
+
const e = entity([])
|
|
244
|
+
return ServerContract.Types.entity_info.from({
|
|
245
|
+
projected_at: 0,
|
|
246
|
+
...JSON.parse(JSON.stringify(e)),
|
|
247
|
+
id: RECEIVER,
|
|
248
|
+
holds: holds.map((h) => ({
|
|
249
|
+
id: h.id,
|
|
250
|
+
kind: h.kind,
|
|
251
|
+
counterpart: {entity_type: 'ship', entity_id: h.from},
|
|
252
|
+
until: h.until,
|
|
253
|
+
incoming_mass: 100,
|
|
254
|
+
})),
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function sender(tasks: ReturnType<typeof craftTask>[]) {
|
|
259
|
+
const e = entity([], [])
|
|
260
|
+
return ServerContract.Types.entity_info.from({
|
|
261
|
+
projected_at: 0,
|
|
262
|
+
...JSON.parse(JSON.stringify(e)),
|
|
263
|
+
id: SENDER,
|
|
264
|
+
lanes: [{lane_key: 0, schedule: {started: T0, tasks}}],
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function coupledTo(hold: number, entityId: number, kind: number) {
|
|
269
|
+
return ServerContract.Types.coupling.from({
|
|
270
|
+
counterpart: {entity_type: 'ship', entity_id: entityId},
|
|
271
|
+
hold,
|
|
272
|
+
kind,
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
test('a coupled craft output resolves to the receiving hold', () => {
|
|
277
|
+
const to = receiver([{id: 1, kind: HoldKind.GATHER, from: SENDER, until: T0}])
|
|
278
|
+
const from = sender([
|
|
279
|
+
craftTask(
|
|
280
|
+
[cargoItem(7, 0, 4), cargoItem(9, 0, 1)],
|
|
281
|
+
[coupledTo(1, RECEIVER, HoldKind.GATHER)]
|
|
282
|
+
),
|
|
283
|
+
])
|
|
284
|
+
const sources = incomingSources(to, () => from)
|
|
285
|
+
expect(sources).toHaveLength(1)
|
|
286
|
+
// A craft delivers its output slot alone, never its inputs.
|
|
287
|
+
expect(sources[0].items).toHaveLength(1)
|
|
288
|
+
expect(sources[0].items[0].item_id.toNumber()).toBe(9)
|
|
289
|
+
expect(sources[0].holdId).toBe('1')
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
test('a hold whose counterpart is unknown yields nothing', () => {
|
|
293
|
+
const to = receiver([{id: 1, kind: HoldKind.GATHER, from: SENDER, until: T0}])
|
|
294
|
+
expect(incomingSources(to, () => undefined)).toHaveLength(0)
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
test('an excluded hold is skipped', () => {
|
|
298
|
+
const to = receiver([{id: 1, kind: HoldKind.GATHER, from: SENDER, until: T0}])
|
|
299
|
+
const from = sender([
|
|
300
|
+
craftTask([cargoItem(9, 0, 1)], [coupledTo(1, RECEIVER, HoldKind.GATHER)]),
|
|
301
|
+
])
|
|
302
|
+
expect(incomingSources(to, () => from, ['1'])).toHaveLength(0)
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
test('a coupling naming another hold does not fill this one', () => {
|
|
306
|
+
const to = receiver([{id: 1, kind: HoldKind.GATHER, from: SENDER, until: T0}])
|
|
307
|
+
const from = sender([
|
|
308
|
+
craftTask([cargoItem(9, 0, 1)], [coupledTo(2, RECEIVER, HoldKind.GATHER)]),
|
|
309
|
+
])
|
|
310
|
+
expect(incomingSources(to, () => from)).toHaveLength(0)
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
test('an outgoing hold kind is not an incoming source', () => {
|
|
314
|
+
const to = receiver([{id: 1, kind: HoldKind.PULL, from: SENDER, until: T0}])
|
|
315
|
+
const from = sender([
|
|
316
|
+
craftTask([cargoItem(9, 0, 1)], [coupledTo(1, RECEIVER, HoldKind.GATHER)]),
|
|
317
|
+
])
|
|
318
|
+
expect(incomingSources(to, () => from)).toHaveLength(0)
|
|
319
|
+
})
|
|
320
|
+
})
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type {ServerContract} from '../contracts'
|
|
2
|
+
import {HoldKind, TaskType} from '../types'
|
|
3
|
+
import * as schedule from './schedule'
|
|
4
|
+
import {taskCargoRule} from './task-effects'
|
|
5
|
+
|
|
6
|
+
type Task = ServerContract.Types.task
|
|
7
|
+
type Coupling = ServerContract.Types.coupling
|
|
8
|
+
type CargoItem = ServerContract.Types.cargo_item
|
|
9
|
+
type ModuleEntry = ServerContract.Types.module_entry
|
|
10
|
+
|
|
11
|
+
export interface CargoEffect {
|
|
12
|
+
added: CargoItem[]
|
|
13
|
+
removed: CargoItem[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AvailabilityInput extends schedule.ScheduleData {
|
|
17
|
+
cargo: CargoItem[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CargoInput {
|
|
21
|
+
itemId: number
|
|
22
|
+
stats: bigint
|
|
23
|
+
modules?: ModuleEntry[]
|
|
24
|
+
quantity: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface IncomingSource {
|
|
28
|
+
holdId: string
|
|
29
|
+
until: Date
|
|
30
|
+
items: CargoItem[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const INCOMING_COUPLING_KINDS = new Set<number>([HoldKind.PUSH, HoldKind.GATHER, HoldKind.FLIGHT])
|
|
34
|
+
|
|
35
|
+
// Mirrors is_incoming_hold_kind: PUSH/GATHER/FLIGHT couplings deliver to the counterpart.
|
|
36
|
+
export function isIncomingCouplingKind(kind: number): boolean {
|
|
37
|
+
return INCOMING_COUPLING_KINDS.has(kind)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Mirrors has_source_hold: a HOLD_SOURCE coupling marks an escrowed clustercraft.
|
|
41
|
+
export function hasSourceCoupling(task: Task): boolean {
|
|
42
|
+
return task.couplings.some((c) => c.kind.toNumber() === HoldKind.SOURCE)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Mirrors has_incoming_coupling: any PUSH/GATHER/FLIGHT coupling delivers output to a counterpart.
|
|
46
|
+
export function hasIncomingCoupling(task: Task): boolean {
|
|
47
|
+
return task.couplings.some((c) => isIncomingCouplingKind(c.kind.toNumber()))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// A CRAFT task's output ownership: clustered escrows inputs off other members; own-output lands on the crafter.
|
|
51
|
+
export function craftCargoOwnership(task: Task): {clustered: boolean; ownOutput: boolean} {
|
|
52
|
+
const clustered = hasSourceCoupling(task)
|
|
53
|
+
return {
|
|
54
|
+
clustered,
|
|
55
|
+
ownOutput: clustered ? !hasIncomingCoupling(task) : task.couplings.length === 0,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Mirrors calc_counterpart_delivery: incoming-kind couplings only; a coupled CRAFT yields its output slot.
|
|
60
|
+
export function calcCounterpartDelivery(task: Task, coupling: Coupling): CargoItem[] {
|
|
61
|
+
if (!isIncomingCouplingKind(coupling.kind.toNumber())) return []
|
|
62
|
+
if (task.type.toNumber() === TaskType.CRAFT) {
|
|
63
|
+
return task.cargo.length === 0 ? [] : [task.cargo[task.cargo.length - 1]]
|
|
64
|
+
}
|
|
65
|
+
return task.cargo
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface IncomingHoldEntity extends schedule.ScheduleData {
|
|
69
|
+
id: {toString(): string}
|
|
70
|
+
holds?: ServerContract.Types.hold[]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Mirrors incoming_sources: each incoming hold resolved through the counterpart task that will fill it.
|
|
74
|
+
export function incomingSources(
|
|
75
|
+
entity: IncomingHoldEntity,
|
|
76
|
+
findCounterpart: (entityId: string) => schedule.ScheduleData | undefined,
|
|
77
|
+
excludedHoldIds: readonly string[] = []
|
|
78
|
+
): IncomingSource[] {
|
|
79
|
+
const out: IncomingSource[] = []
|
|
80
|
+
const excluded = new Set(excludedHoldIds.map((id) => String(id)))
|
|
81
|
+
const selfId = entity.id.toString()
|
|
82
|
+
for (const hold of entity.holds ?? []) {
|
|
83
|
+
if (!isIncomingCouplingKind(hold.kind.toNumber())) continue
|
|
84
|
+
const holdId = hold.id.toString()
|
|
85
|
+
if (excluded.has(holdId)) continue
|
|
86
|
+
const counterpart = findCounterpart(hold.counterpart.entity_id.toString())
|
|
87
|
+
if (!counterpart) continue
|
|
88
|
+
let matched = false
|
|
89
|
+
for (const lane of counterpart.lanes ?? []) {
|
|
90
|
+
if (matched) break
|
|
91
|
+
for (const task of lane.schedule.tasks) {
|
|
92
|
+
if (matched) break
|
|
93
|
+
for (const coupling of task.couplings) {
|
|
94
|
+
if (coupling.hold.toString() !== holdId) continue
|
|
95
|
+
if (coupling.counterpart.entity_id.toString() !== selfId) continue
|
|
96
|
+
const items = calcCounterpartDelivery(task, coupling)
|
|
97
|
+
if (items.length > 0) {
|
|
98
|
+
out.push({holdId, until: hold.until.toDate(), items})
|
|
99
|
+
}
|
|
100
|
+
matched = true
|
|
101
|
+
break
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return out
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function taskCargoEffect(task: Task): CargoEffect {
|
|
110
|
+
switch (taskCargoRule(task.type.toNumber())) {
|
|
111
|
+
case 'all-in':
|
|
112
|
+
case 'undeploy':
|
|
113
|
+
return {added: task.cargo, removed: []}
|
|
114
|
+
case 'all-out':
|
|
115
|
+
return {added: [], removed: task.cargo}
|
|
116
|
+
case 'gather':
|
|
117
|
+
return task.couplings.length > 0
|
|
118
|
+
? {added: [], removed: []}
|
|
119
|
+
: {added: task.cargo, removed: []}
|
|
120
|
+
case 'craft': {
|
|
121
|
+
if (task.cargo.length === 0) return {added: [], removed: []}
|
|
122
|
+
const {clustered, ownOutput} = craftCargoOwnership(task)
|
|
123
|
+
return {
|
|
124
|
+
added: ownOutput ? [task.cargo[task.cargo.length - 1]] : [],
|
|
125
|
+
removed: clustered ? [] : task.cargo.slice(0, -1),
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
default:
|
|
129
|
+
return {added: [], removed: []}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function refKey(itemId: number, stats: bigint, modules: ModuleEntry[], entityId: string): string {
|
|
134
|
+
const base = `${itemId}:${stats.toString()}`
|
|
135
|
+
const normalizedEntityId = entityId !== '0' ? entityId : ''
|
|
136
|
+
if (modules.length === 0 && normalizedEntityId === '') return base
|
|
137
|
+
return `${base}:modules=${JSON.stringify(modules)}:entity=${normalizedEntityId}`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function cargoKey(item: CargoItem): string {
|
|
141
|
+
return refKey(
|
|
142
|
+
item.item_id.toNumber(),
|
|
143
|
+
BigInt(item.stats.toString()),
|
|
144
|
+
item.modules ?? [],
|
|
145
|
+
item.entity_id?.toString() ?? ''
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function cargoInputKey(input: CargoInput): string {
|
|
150
|
+
return refKey(input.itemId, input.stats, input.modules ?? [], '')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function cargoQuantity(item: CargoItem): bigint {
|
|
154
|
+
return BigInt(item.quantity.toString())
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function projectedCargoAvailableAt(
|
|
158
|
+
entity: AvailabilityInput,
|
|
159
|
+
at: Date,
|
|
160
|
+
incoming: readonly IncomingSource[] = []
|
|
161
|
+
): Map<string, bigint> {
|
|
162
|
+
const avail = new Map<string, bigint>()
|
|
163
|
+
|
|
164
|
+
for (const item of entity.cargo) {
|
|
165
|
+
const key = cargoKey(item)
|
|
166
|
+
avail.set(key, (avail.get(key) ?? 0n) + cargoQuantity(item))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Every scheduled task reserves inputs against the unsettled cargo base, even already-elapsed ones.
|
|
170
|
+
const tasks = schedule.orderedTasks(entity)
|
|
171
|
+
|
|
172
|
+
for (const ordered of tasks) {
|
|
173
|
+
if (ordered.completesAt.getTime() >= at.getTime()) continue
|
|
174
|
+
|
|
175
|
+
for (const item of taskCargoEffect(ordered.task).added) {
|
|
176
|
+
const key = cargoKey(item)
|
|
177
|
+
avail.set(key, (avail.get(key) ?? 0n) + cargoQuantity(item))
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (const src of incoming) {
|
|
182
|
+
if (src.until.getTime() >= at.getTime()) continue
|
|
183
|
+
for (const item of src.items) {
|
|
184
|
+
const key = cargoKey(item)
|
|
185
|
+
avail.set(key, (avail.get(key) ?? 0n) + cargoQuantity(item))
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for (const ordered of tasks) {
|
|
190
|
+
for (const item of taskCargoEffect(ordered.task).removed) {
|
|
191
|
+
const key = cargoKey(item)
|
|
192
|
+
const current = avail.get(key) ?? 0n
|
|
193
|
+
const quantity = cargoQuantity(item)
|
|
194
|
+
avail.set(key, current > quantity ? current - quantity : 0n)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return avail
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Earliest-sufficient candidate (epoch, own-lane producer completions, incoming untils); else the latest candidate.
|
|
202
|
+
export function cargoReadyAt(
|
|
203
|
+
entity: AvailabilityInput,
|
|
204
|
+
inputs: readonly CargoInput[],
|
|
205
|
+
incoming: readonly IncomingSource[] = []
|
|
206
|
+
): Date {
|
|
207
|
+
const demand = new Map<string, bigint>()
|
|
208
|
+
for (const input of inputs) {
|
|
209
|
+
const key = cargoInputKey(input)
|
|
210
|
+
demand.set(key, (demand.get(key) ?? 0n) + BigInt(input.quantity))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const candidates: number[] = [0]
|
|
214
|
+
for (const ordered of schedule.orderedTasks(entity)) {
|
|
215
|
+
for (const item of taskCargoEffect(ordered.task).added) {
|
|
216
|
+
if (demand.has(cargoKey(item))) {
|
|
217
|
+
candidates.push(ordered.completesAt.getTime())
|
|
218
|
+
break
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
for (const src of incoming) {
|
|
223
|
+
if (src.items.some((item) => demand.has(cargoKey(item)))) {
|
|
224
|
+
candidates.push(src.until.getTime())
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
candidates.sort((a, b) => a - b)
|
|
228
|
+
|
|
229
|
+
for (const candidateMs of candidates) {
|
|
230
|
+
// +1ms mirrors the contract's candidate+1µs probe: inclusive at the candidate instant.
|
|
231
|
+
const available = projectedCargoAvailableAt(entity, new Date(candidateMs + 1), incoming)
|
|
232
|
+
let sufficient = true
|
|
233
|
+
for (const [key, quantity] of demand) {
|
|
234
|
+
if ((available.get(key) ?? 0n) < quantity) {
|
|
235
|
+
sufficient = false
|
|
236
|
+
break
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (sufficient) return new Date(candidateMs)
|
|
240
|
+
}
|
|
241
|
+
return new Date(candidates[candidates.length - 1])
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function availableForItem(avail: Map<string, bigint>, itemId: number): bigint {
|
|
245
|
+
const prefix = `${itemId}:`
|
|
246
|
+
let total = 0n
|
|
247
|
+
for (const [key, quantity] of avail) {
|
|
248
|
+
if (key.startsWith(prefix)) total += quantity
|
|
249
|
+
}
|
|
250
|
+
return total
|
|
251
|
+
}
|