@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,254 @@
|
|
|
1
|
+
import type {ServerContract} from '../contracts'
|
|
2
|
+
import {HoldKind, TaskCancelable, TaskType} from '../types'
|
|
3
|
+
import {calcCargoItemMass} from '../capabilities/storage'
|
|
4
|
+
import {
|
|
5
|
+
taskCargoEffect,
|
|
6
|
+
cargoKey,
|
|
7
|
+
isIncomingCouplingKind,
|
|
8
|
+
type IncomingSource,
|
|
9
|
+
} from './availability'
|
|
10
|
+
import * as schedule from './schedule'
|
|
11
|
+
import {validateSchedule, type Projectable} from './projection'
|
|
12
|
+
|
|
13
|
+
export enum CancelBlockReason {
|
|
14
|
+
TASK_NEVER = 'TASK_NEVER',
|
|
15
|
+
BEFORE_START_RUNNING = 'BEFORE_START_RUNNING',
|
|
16
|
+
DONE = 'DONE',
|
|
17
|
+
CONTAINS_LINKED_TASK = 'CONTAINS_LINKED_TASK',
|
|
18
|
+
WOULD_STRAND = 'WOULD_STRAND',
|
|
19
|
+
WOULD_STRAND_COUNTERPART = 'WOULD_STRAND_COUNTERPART',
|
|
20
|
+
WOULD_OVERFILL = 'WOULD_OVERFILL',
|
|
21
|
+
NOT_OWNER = 'NOT_OWNER',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type Task = InstanceType<typeof ServerContract.Types.task>
|
|
25
|
+
type EntityInfo = InstanceType<typeof ServerContract.Types.entity_info>
|
|
26
|
+
type EntityRef = InstanceType<typeof ServerContract.Types.entity_ref>
|
|
27
|
+
type CargoItem = InstanceType<typeof ServerContract.Types.cargo_item>
|
|
28
|
+
|
|
29
|
+
export interface CancelRefund {
|
|
30
|
+
giver: EntityRef
|
|
31
|
+
cargo: CargoItem[]
|
|
32
|
+
}
|
|
33
|
+
export interface CancelReleasedHold {
|
|
34
|
+
counterpart: EntityRef
|
|
35
|
+
kind: number
|
|
36
|
+
}
|
|
37
|
+
export interface CancelEffects {
|
|
38
|
+
refunds: CancelRefund[]
|
|
39
|
+
releasedHolds: CancelReleasedHold[]
|
|
40
|
+
abandonsRunning: boolean
|
|
41
|
+
keepsPlotDeposits?: {plot: EntityRef}
|
|
42
|
+
energyForfeited?: number
|
|
43
|
+
}
|
|
44
|
+
export interface CancelPlan {
|
|
45
|
+
ok: boolean
|
|
46
|
+
blockedReason?: CancelBlockReason
|
|
47
|
+
blockedByCounterpart?: EntityRef
|
|
48
|
+
range: {count: number; taskIndices: number[]}
|
|
49
|
+
effects: CancelEffects
|
|
50
|
+
}
|
|
51
|
+
export interface CancelEligibilityInput {
|
|
52
|
+
now: Date
|
|
53
|
+
counterparts?: Map<string, EntityInfo>
|
|
54
|
+
counterpartIncoming?: Map<string, readonly IncomingSource[]>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const EMPTY_EFFECTS: CancelEffects = {refunds: [], releasedHolds: [], abandonsRunning: false}
|
|
58
|
+
|
|
59
|
+
function postCancelEntity(entity: EntityInfo, laneKey: number, fromTaskIndex: number): EntityInfo {
|
|
60
|
+
const clone = (entity.constructor as typeof ServerContract.Types.entity_info).from(
|
|
61
|
+
JSON.parse(JSON.stringify(entity.toJSON()))
|
|
62
|
+
) as EntityInfo
|
|
63
|
+
const lane = clone.lanes.find((l) => l.lane_key.toNumber() === laneKey)!
|
|
64
|
+
lane.schedule.tasks = lane.schedule.tasks.slice(0, fromTaskIndex)
|
|
65
|
+
return clone
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// C1: every pending consumer keeps its inputs covered by on-hand + own-lane producers strictly-before + incoming strictly-before.
|
|
69
|
+
function queueFeasible(entity: EntityInfo, incoming: readonly IncomingSource[] = []): boolean {
|
|
70
|
+
const ordered = schedule.orderedTasks(entity as unknown as Projectable)
|
|
71
|
+
const base = new Map<string, number>()
|
|
72
|
+
for (const c of entity.cargo ?? []) {
|
|
73
|
+
const k = cargoKey(c)
|
|
74
|
+
base.set(k, (base.get(k) ?? 0) + c.quantity.toNumber())
|
|
75
|
+
}
|
|
76
|
+
const isConsumer = (t: Task) =>
|
|
77
|
+
t.type.toNumber() === TaskType.CRAFT ||
|
|
78
|
+
t.type.toNumber() === TaskType.UNLOAD ||
|
|
79
|
+
t.type.toNumber() === TaskType.UPGRADE
|
|
80
|
+
for (const self of ordered) {
|
|
81
|
+
if (!isConsumer(self.task)) continue
|
|
82
|
+
const map = new Map(base)
|
|
83
|
+
for (const other of ordered) {
|
|
84
|
+
if (other.completesAt.getTime() >= self.completesAt.getTime()) continue
|
|
85
|
+
for (const out of taskCargoEffect(other.task).added) {
|
|
86
|
+
map.set(cargoKey(out), (map.get(cargoKey(out)) ?? 0) + out.quantity.toNumber())
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
for (const src of incoming) {
|
|
90
|
+
if (src.until.getTime() >= self.completesAt.getTime()) continue
|
|
91
|
+
for (const item of src.items) {
|
|
92
|
+
map.set(cargoKey(item), (map.get(cargoKey(item)) ?? 0) + item.quantity.toNumber())
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (const other of ordered) {
|
|
96
|
+
if (other === self) continue
|
|
97
|
+
for (const inp of taskCargoEffect(other.task).removed) {
|
|
98
|
+
const cur = map.get(cargoKey(inp)) ?? 0
|
|
99
|
+
map.set(cargoKey(inp), Math.max(0, cur - inp.quantity.toNumber()))
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
for (const inp of taskCargoEffect(self.task).removed) {
|
|
103
|
+
if ((map.get(cargoKey(inp)) ?? 0) < inp.quantity.toNumber()) return false
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return true
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function feasibleAfterCancel(post: EntityInfo): boolean {
|
|
110
|
+
if (!queueFeasible(post)) return false
|
|
111
|
+
try {
|
|
112
|
+
validateSchedule(post as unknown as Projectable)
|
|
113
|
+
} catch {
|
|
114
|
+
return false
|
|
115
|
+
}
|
|
116
|
+
return true
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
interface Timing {
|
|
120
|
+
startsAt: number
|
|
121
|
+
completesAt: number
|
|
122
|
+
running: boolean
|
|
123
|
+
done: boolean
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function laneTiming(
|
|
127
|
+
lane: {schedule: {started: {toDate(): Date}; tasks: Task[]}},
|
|
128
|
+
nowMs: number
|
|
129
|
+
): Timing[] {
|
|
130
|
+
const startedMs = lane.schedule.started.toDate().getTime()
|
|
131
|
+
let endSec = 0
|
|
132
|
+
return lane.schedule.tasks.map((t) => {
|
|
133
|
+
const startsAt = startedMs + endSec * 1000
|
|
134
|
+
endSec += t.duration.toNumber()
|
|
135
|
+
const completesAt = startedMs + endSec * 1000
|
|
136
|
+
return {
|
|
137
|
+
startsAt,
|
|
138
|
+
completesAt,
|
|
139
|
+
running: nowMs >= startsAt && nowMs < completesAt,
|
|
140
|
+
done: nowMs >= completesAt,
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function cancelEligibility(
|
|
146
|
+
entity: EntityInfo,
|
|
147
|
+
laneKey: number,
|
|
148
|
+
fromTaskIndex: number,
|
|
149
|
+
input: CancelEligibilityInput
|
|
150
|
+
): CancelPlan {
|
|
151
|
+
const lane = (entity.lanes ?? []).find((l) => l.lane_key.equals(laneKey))
|
|
152
|
+
if (!lane || fromTaskIndex < 0 || fromTaskIndex >= lane.schedule.tasks.length) {
|
|
153
|
+
return {ok: false, range: {count: 0, taskIndices: []}, effects: {...EMPTY_EFFECTS}}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const tasks = lane.schedule.tasks
|
|
157
|
+
const timing = laneTiming(lane, input.now.getTime())
|
|
158
|
+
const taskIndices: number[] = []
|
|
159
|
+
for (let i = fromTaskIndex; i < tasks.length; i++) taskIndices.push(i)
|
|
160
|
+
const range = {count: taskIndices.length, taskIndices}
|
|
161
|
+
|
|
162
|
+
const block = (blockedReason: CancelBlockReason): CancelPlan => ({
|
|
163
|
+
ok: false,
|
|
164
|
+
blockedReason,
|
|
165
|
+
range,
|
|
166
|
+
effects: {...EMPTY_EFFECTS},
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
for (const i of taskIndices) {
|
|
170
|
+
const t = tasks[i]
|
|
171
|
+
if (t.entitygroup && !t.entitygroup.equals(0))
|
|
172
|
+
return block(CancelBlockReason.CONTAINS_LINKED_TASK)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const i of taskIndices) {
|
|
176
|
+
const t = tasks[i]
|
|
177
|
+
if (timing[i].done) return block(CancelBlockReason.DONE)
|
|
178
|
+
if (t.cancelable.equals(TaskCancelable.NEVER) && !t.type.equals(TaskType.IDLE))
|
|
179
|
+
return block(CancelBlockReason.TASK_NEVER)
|
|
180
|
+
if (t.cancelable.equals(TaskCancelable.BEFORE_START) && timing[i].running)
|
|
181
|
+
return block(CancelBlockReason.BEFORE_START_RUNNING)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const post = postCancelEntity(entity, laneKey, fromTaskIndex)
|
|
185
|
+
if (!feasibleAfterCancel(post)) return block(CancelBlockReason.WOULD_STRAND)
|
|
186
|
+
|
|
187
|
+
// cross-entity C1: a counterpart losing this delivery must still feed its own queued consumers.
|
|
188
|
+
const releasedByCounterpart = new Map<string, {counterpart: EntityRef; holdIds: Set<string>}>()
|
|
189
|
+
for (const i of taskIndices) {
|
|
190
|
+
for (const c of tasks[i].couplings ?? []) {
|
|
191
|
+
if (!isIncomingCouplingKind(c.kind.toNumber())) continue
|
|
192
|
+
const id = c.counterpart.entity_id.toString()
|
|
193
|
+
const entry = releasedByCounterpart.get(id) ?? {
|
|
194
|
+
counterpart: c.counterpart,
|
|
195
|
+
holdIds: new Set<string>(),
|
|
196
|
+
}
|
|
197
|
+
entry.holdIds.add(c.hold.toString())
|
|
198
|
+
releasedByCounterpart.set(id, entry)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
for (const [counterpartId, released] of releasedByCounterpart) {
|
|
202
|
+
const counterpart = input.counterparts?.get(counterpartId)
|
|
203
|
+
if (!counterpart) continue
|
|
204
|
+
const incoming = (input.counterpartIncoming?.get(counterpartId) ?? []).filter(
|
|
205
|
+
(src) => !released.holdIds.has(src.holdId)
|
|
206
|
+
)
|
|
207
|
+
if (!queueFeasible(counterpart, incoming)) {
|
|
208
|
+
return {
|
|
209
|
+
ok: false,
|
|
210
|
+
blockedReason: CancelBlockReason.WOULD_STRAND_COUNTERPART,
|
|
211
|
+
blockedByCounterpart: released.counterpart,
|
|
212
|
+
range,
|
|
213
|
+
effects: {...EMPTY_EFFECTS},
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const effects: CancelEffects = {refunds: [], releasedHolds: [], abandonsRunning: false}
|
|
219
|
+
let energyForfeited = 0
|
|
220
|
+
for (const i of taskIndices) {
|
|
221
|
+
const t = tasks[i]
|
|
222
|
+
if (timing[i].running && t.cancelable.equals(TaskCancelable.ALWAYS))
|
|
223
|
+
effects.abandonsRunning = true
|
|
224
|
+
if (t.energy_cost) energyForfeited += t.energy_cost.toNumber()
|
|
225
|
+
if (t.type.equals(TaskType.BUILDPLOT) && t.couplings.length > 0)
|
|
226
|
+
effects.keepsPlotDeposits = {plot: t.couplings[0].counterpart}
|
|
227
|
+
for (const c of t.couplings) {
|
|
228
|
+
const kind = c.kind.toNumber()
|
|
229
|
+
effects.releasedHolds.push({counterpart: c.counterpart, kind})
|
|
230
|
+
if (kind === HoldKind.PULL) {
|
|
231
|
+
effects.refunds.push({giver: c.counterpart, cargo: t.cargo})
|
|
232
|
+
const giver = input.counterparts?.get(c.counterpart.entity_id.toString())
|
|
233
|
+
if (giver) {
|
|
234
|
+
const returned = t.cargo.reduce(
|
|
235
|
+
(s, c) => s + calcCargoItemMass(c).toNumber(),
|
|
236
|
+
0
|
|
237
|
+
)
|
|
238
|
+
const cap = giver.capacity ? giver.capacity.toNumber() : Number.MAX_SAFE_INTEGER
|
|
239
|
+
if (giver.cargomass.toNumber() + returned > cap) {
|
|
240
|
+
return {
|
|
241
|
+
ok: false,
|
|
242
|
+
blockedReason: CancelBlockReason.WOULD_OVERFILL,
|
|
243
|
+
range,
|
|
244
|
+
effects: {...EMPTY_EFFECTS},
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (energyForfeited > 0) effects.energyForfeited = energyForfeited
|
|
252
|
+
|
|
253
|
+
return {ok: true, range, effects}
|
|
254
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {describe, expect, test} from 'bun:test'
|
|
2
|
+
import {ServerContract, TaskType} from '../index-module'
|
|
3
|
+
import {availableForItem} from './availability'
|
|
4
|
+
import {clusterStockAvailable} from './cluster-stock'
|
|
5
|
+
|
|
6
|
+
const AT = new Date('2026-06-19T00:00:00')
|
|
7
|
+
|
|
8
|
+
function cargoItem(itemId: number, stats: number, quantity: number) {
|
|
9
|
+
return ServerContract.Types.cargo_item.from({item_id: itemId, stats, modules: [], quantity})
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function member(cargo: ReturnType<typeof cargoItem>[]) {
|
|
13
|
+
return {cargo, tasks: [], lanes: []} as unknown as Parameters<
|
|
14
|
+
typeof clusterStockAvailable
|
|
15
|
+
>[0][number]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('clusterStockAvailable', () => {
|
|
19
|
+
test('sums the same item split across two members', () => {
|
|
20
|
+
const stock = clusterStockAvailable(
|
|
21
|
+
[member([cargoItem(1001, 0, 30)]), member([cargoItem(1001, 0, 20)])],
|
|
22
|
+
AT
|
|
23
|
+
)
|
|
24
|
+
expect(availableForItem(stock, 1001)).toBe(50n)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('an empty cluster yields nothing', () => {
|
|
28
|
+
const stock = clusterStockAvailable([], AT)
|
|
29
|
+
expect(availableForItem(stock, 1001)).toBe(0n)
|
|
30
|
+
})
|
|
31
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {type AvailabilityInput, projectedCargoAvailableAt} from './availability'
|
|
2
|
+
|
|
3
|
+
// Union of every cluster member's present available cargo (present-only; incoming/future not credited).
|
|
4
|
+
export function clusterStockAvailable(
|
|
5
|
+
members: readonly AvailabilityInput[],
|
|
6
|
+
at: Date
|
|
7
|
+
): Map<string, bigint> {
|
|
8
|
+
const total = new Map<string, bigint>()
|
|
9
|
+
for (const member of members) {
|
|
10
|
+
for (const [key, qty] of projectedCargoAvailableAt(member, at)) {
|
|
11
|
+
total.set(key, (total.get(key) ?? 0n) + qty)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return total
|
|
15
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {TaskType} from '../types'
|
|
2
|
+
import {createProjectedEntity, type Projectable} from './projection'
|
|
3
|
+
import {orderedTasks} from './schedule'
|
|
4
|
+
|
|
5
|
+
export function energyAtTime(entity: Projectable, now: Date): number {
|
|
6
|
+
const projected = createProjectedEntity(entity)
|
|
7
|
+
const capacity = projected.generator ? Number(projected.generator.capacity) : undefined
|
|
8
|
+
|
|
9
|
+
const clamp = (value: number): number => {
|
|
10
|
+
const floored = Math.max(0, value)
|
|
11
|
+
return capacity !== undefined ? Math.min(capacity, floored) : floored
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let running = Number(projected.energy)
|
|
15
|
+
|
|
16
|
+
const ordered = orderedTasks(entity)
|
|
17
|
+
if (ordered.length === 0) return clamp(running)
|
|
18
|
+
|
|
19
|
+
const nowMs = now.getTime()
|
|
20
|
+
|
|
21
|
+
for (const {task, startsAt} of ordered) {
|
|
22
|
+
const duration = task.duration.toNumber()
|
|
23
|
+
const elapsed = Math.min(
|
|
24
|
+
Math.max(0, Math.floor((nowMs - startsAt.getTime()) / 1000)),
|
|
25
|
+
duration
|
|
26
|
+
)
|
|
27
|
+
const complete = elapsed >= duration
|
|
28
|
+
const inProgress = !complete && elapsed > 0 && elapsed < duration
|
|
29
|
+
|
|
30
|
+
if (!complete && !inProgress) continue
|
|
31
|
+
|
|
32
|
+
const fraction = complete ? 1 : duration === 0 ? 1 : elapsed / duration
|
|
33
|
+
|
|
34
|
+
if (task.type.toNumber() === TaskType.RECHARGE) {
|
|
35
|
+
if (capacity !== undefined) {
|
|
36
|
+
running = complete ? capacity : running + (capacity - running) * fraction
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
const cost = Number(task.energy_cost ?? 0)
|
|
40
|
+
running -= cost * fraction
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
running = clamp(running)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return clamp(running)
|
|
47
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type {Action, UInt64} from '@wharfkit/antelope'
|
|
2
|
+
import type {ServerContract} from '../contracts'
|
|
3
|
+
import type {ActionsManager} from '../managers/actions'
|
|
4
|
+
import {hasResolvable, type ScheduleData} from './schedule'
|
|
5
|
+
|
|
6
|
+
type EntityInfo = ServerContract.Types.entity_info
|
|
7
|
+
|
|
8
|
+
export type CounterpartLookup = (entityId: UInt64) => EntityInfo | undefined
|
|
9
|
+
|
|
10
|
+
export type IdleResolveTarget = ScheduleData & {id: UInt64}
|
|
11
|
+
|
|
12
|
+
// A hold's driving task lives on its counterpart, so a hold resolves the counterpart, never the blocker.
|
|
13
|
+
export function composeIdleResolve(
|
|
14
|
+
blocker: IdleResolveTarget,
|
|
15
|
+
action: Action,
|
|
16
|
+
actions: ActionsManager,
|
|
17
|
+
now: Date,
|
|
18
|
+
lookupCounterpart?: CounterpartLookup
|
|
19
|
+
): Action[] {
|
|
20
|
+
const ids: UInt64[] = []
|
|
21
|
+
const seen = new Set<string>()
|
|
22
|
+
|
|
23
|
+
const add = (id: UInt64) => {
|
|
24
|
+
const key = id.toString()
|
|
25
|
+
if (seen.has(key)) return
|
|
26
|
+
seen.add(key)
|
|
27
|
+
ids.push(id)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (hasResolvable(blocker, now)) {
|
|
31
|
+
add(blocker.id)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Without a lookup we cannot confirm the counterpart has a completed task, so skip it.
|
|
35
|
+
if (lookupCounterpart) {
|
|
36
|
+
for (const hold of blocker.holds ?? []) {
|
|
37
|
+
const counterpartId = hold.counterpart.entity_id
|
|
38
|
+
const counterpart = lookupCounterpart(counterpartId)
|
|
39
|
+
if (!counterpart || !hasResolvable(counterpart, now)) continue
|
|
40
|
+
add(counterpartId)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return [...ids.map((id) => actions.resolve(id)), action]
|
|
45
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export interface JobWindow {
|
|
2
|
+
id: number
|
|
3
|
+
socket: number
|
|
4
|
+
owner: string
|
|
5
|
+
startsAt: Date
|
|
6
|
+
completesAt: Date
|
|
7
|
+
recipeId: number
|
|
8
|
+
quantity: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface JobLaneEntry {
|
|
12
|
+
kind: 'idle' | 'job'
|
|
13
|
+
startsAt: Date
|
|
14
|
+
completesAt: Date
|
|
15
|
+
job?: JobWindow
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface JobLane {
|
|
19
|
+
socket: number
|
|
20
|
+
entries: JobLaneEntry[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const JOB_QUEUE_CAP = 25
|
|
24
|
+
|
|
25
|
+
function activeOn(jobs: JobWindow[], socket: number, now: Date): JobWindow[] {
|
|
26
|
+
return jobs
|
|
27
|
+
.filter((j) => j.socket === socket && j.completesAt > now)
|
|
28
|
+
.sort((a, b) => a.startsAt.getTime() - b.startsAt.getTime())
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function jobsToLanes(jobs: JobWindow[], socketCount: number, now: Date): JobLane[] {
|
|
32
|
+
const lanes: JobLane[] = []
|
|
33
|
+
for (let socket = 0; socket < socketCount; socket++) {
|
|
34
|
+
const entries: JobLaneEntry[] = []
|
|
35
|
+
let cursor: Date | null = null
|
|
36
|
+
for (const j of activeOn(jobs, socket, now)) {
|
|
37
|
+
if (cursor && j.startsAt > cursor) {
|
|
38
|
+
entries.push({kind: 'idle', startsAt: cursor, completesAt: j.startsAt})
|
|
39
|
+
}
|
|
40
|
+
entries.push({kind: 'job', startsAt: j.startsAt, completesAt: j.completesAt, job: j})
|
|
41
|
+
cursor = j.completesAt
|
|
42
|
+
}
|
|
43
|
+
lanes.push({socket, entries})
|
|
44
|
+
}
|
|
45
|
+
return lanes
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function socketTail(jobs: JobWindow[], socket: number, now: Date): Date {
|
|
49
|
+
const active = activeOn(jobs, socket, now)
|
|
50
|
+
const last = active[active.length - 1]
|
|
51
|
+
return last && last.completesAt > now ? last.completesAt : now
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function pickFabricator(
|
|
55
|
+
jobs: JobWindow[],
|
|
56
|
+
sockets: Array<{open: boolean}>,
|
|
57
|
+
durationBySocketMinutes: number[],
|
|
58
|
+
now: Date
|
|
59
|
+
): {slot: number; startsAt: Date; completesAt: Date} | null {
|
|
60
|
+
let best: {slot: number; startsAt: Date; completesAt: Date} | null = null
|
|
61
|
+
for (let slot = 0; slot < sockets.length; slot++) {
|
|
62
|
+
if (!sockets[slot].open) continue
|
|
63
|
+
if (activeOn(jobs, slot, now).length >= JOB_QUEUE_CAP) continue
|
|
64
|
+
const startsAt = socketTail(jobs, slot, now)
|
|
65
|
+
const completesAt = new Date(startsAt.getTime() + durationBySocketMinutes[slot] * 60_000)
|
|
66
|
+
if (!best || completesAt < best.completesAt) {
|
|
67
|
+
best = {slot, startsAt, completesAt}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return best
|
|
71
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type {ServerContract} from '../contracts'
|
|
2
|
+
import type {TaskType} from '../types'
|
|
3
|
+
|
|
4
|
+
type Schedule = ServerContract.Types.schedule
|
|
5
|
+
type Task = ServerContract.Types.task
|
|
6
|
+
|
|
7
|
+
export function laneDuration(schedule: Schedule): number {
|
|
8
|
+
return schedule.tasks.reduce((sum, task) => sum + task.duration.toNumber(), 0)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function laneRawElapsed(schedule: Schedule, now: Date): number {
|
|
12
|
+
const started = schedule.started.toDate()
|
|
13
|
+
return Math.floor((now.getTime() - started.getTime()) / 1000)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function laneElapsed(schedule: Schedule, now: Date): number {
|
|
17
|
+
return Math.max(0, laneRawElapsed(schedule, now))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function laneStartsIn(schedule: Schedule, now: Date): number {
|
|
21
|
+
return Math.max(0, -laneRawElapsed(schedule, now))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function laneRemaining(schedule: Schedule, now: Date): number {
|
|
25
|
+
return Math.max(0, laneDuration(schedule) - laneRawElapsed(schedule, now))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function laneComplete(schedule: Schedule, now: Date): boolean {
|
|
29
|
+
if (schedule.tasks.length === 0) return false
|
|
30
|
+
return laneRemaining(schedule, now) === 0
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function laneProgress(schedule: Schedule, now: Date): number {
|
|
34
|
+
const duration = laneDuration(schedule)
|
|
35
|
+
if (duration === 0) return schedule.tasks.length > 0 ? 1 : 0
|
|
36
|
+
return Math.min(1, laneElapsed(schedule, now) / duration)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function currentTaskIndexForLane(schedule: Schedule, now: Date): number {
|
|
40
|
+
if (schedule.tasks.length === 0) return -1
|
|
41
|
+
if (laneRawElapsed(schedule, now) < 0) return -1
|
|
42
|
+
const elapsed = laneElapsed(schedule, now)
|
|
43
|
+
let timeAccum = 0
|
|
44
|
+
for (let i = 0; i < schedule.tasks.length; i++) {
|
|
45
|
+
const taskDuration = schedule.tasks[i].duration.toNumber()
|
|
46
|
+
if (elapsed < timeAccum + taskDuration) return i
|
|
47
|
+
timeAccum += taskDuration
|
|
48
|
+
}
|
|
49
|
+
return -1
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function currentTask(schedule: Schedule, now: Date): Task | undefined {
|
|
53
|
+
const index = currentTaskIndexForLane(schedule, now)
|
|
54
|
+
if (index < 0) return undefined
|
|
55
|
+
return schedule.tasks[index]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function currentTaskType(schedule: Schedule, now: Date): TaskType | undefined {
|
|
59
|
+
const task = currentTask(schedule, now)
|
|
60
|
+
return task ? (task.type.toNumber() as TaskType) : undefined
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function laneTaskStartTime(schedule: Schedule, index: number): number {
|
|
64
|
+
if (index < 0 || index >= schedule.tasks.length) return 0
|
|
65
|
+
let timeAccum = 0
|
|
66
|
+
for (let i = 0; i < index; i++) {
|
|
67
|
+
timeAccum += schedule.tasks[i].duration.toNumber()
|
|
68
|
+
}
|
|
69
|
+
return timeAccum
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function laneTaskElapsed(schedule: Schedule, index: number, now: Date): number {
|
|
73
|
+
if (index < 0 || index >= schedule.tasks.length) return 0
|
|
74
|
+
const elapsed = laneElapsed(schedule, now)
|
|
75
|
+
const taskStart = laneTaskStartTime(schedule, index)
|
|
76
|
+
const taskDuration = schedule.tasks[index].duration.toNumber()
|
|
77
|
+
if (elapsed <= taskStart) return 0
|
|
78
|
+
return Math.min(elapsed - taskStart, taskDuration)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function laneTaskRemaining(schedule: Schedule, index: number, now: Date): number {
|
|
82
|
+
if (index < 0 || index >= schedule.tasks.length) return 0
|
|
83
|
+
const taskDuration = schedule.tasks[index].duration.toNumber()
|
|
84
|
+
return Math.max(0, taskDuration - laneTaskElapsed(schedule, index, now))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function laneTaskComplete(schedule: Schedule, index: number, now: Date): boolean {
|
|
88
|
+
if (index < 0 || index >= schedule.tasks.length) return false
|
|
89
|
+
const taskDuration = schedule.tasks[index].duration.toNumber()
|
|
90
|
+
return laneTaskElapsed(schedule, index, now) >= taskDuration
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function laneTaskInProgress(schedule: Schedule, index: number, now: Date): boolean {
|
|
94
|
+
if (index < 0 || index >= schedule.tasks.length) return false
|
|
95
|
+
const taskElapsed = laneTaskElapsed(schedule, index, now)
|
|
96
|
+
const taskDuration = schedule.tasks[index].duration.toNumber()
|
|
97
|
+
return taskElapsed > 0 && taskElapsed < taskDuration
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function laneCompletesAt(schedule: Schedule, index: number): Date {
|
|
101
|
+
const startedMs = schedule.started.toDate().getTime()
|
|
102
|
+
const endSec =
|
|
103
|
+
laneTaskStartTime(schedule, index) + (schedule.tasks[index]?.duration.toNumber() ?? 0)
|
|
104
|
+
return new Date(startedMs + endSec * 1000)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function currentTaskProgress(schedule: Schedule, now: Date): number {
|
|
108
|
+
const index = currentTaskIndexForLane(schedule, now)
|
|
109
|
+
if (index < 0) return 0
|
|
110
|
+
const elapsed = laneTaskElapsed(schedule, index, now)
|
|
111
|
+
const duration = schedule.tasks[index].duration.toNumber()
|
|
112
|
+
if (duration === 0) return 1
|
|
113
|
+
return Math.min(1, elapsed / duration)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function currentTaskProgressFloatForLane(schedule: Schedule, now: Date): number {
|
|
117
|
+
if (schedule.tasks.length === 0) return 0
|
|
118
|
+
const index = currentTaskIndexForLane(schedule, now)
|
|
119
|
+
if (index < 0) return 0
|
|
120
|
+
const task = schedule.tasks[index]
|
|
121
|
+
const durationMs = task.duration.toNumber() * 1000
|
|
122
|
+
if (durationMs === 0) return 1
|
|
123
|
+
const startedMs = schedule.started.toDate().getTime()
|
|
124
|
+
const taskStartMs = startedMs + laneTaskStartTime(schedule, index) * 1000
|
|
125
|
+
const elapsedMs = now.getTime() - taskStartMs
|
|
126
|
+
if (elapsedMs <= 0) return 0
|
|
127
|
+
return Math.min(1, elapsedMs / durationMs)
|
|
128
|
+
}
|