@shipload/sdk 1.0.0-next.51 → 1.0.0-next.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/shipload.d.ts +471 -158
- package/lib/shipload.js +2320 -485
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +2290 -484
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +123 -8
- package/lib/testing.js +450 -19
- package/lib/testing.js.map +1 -1
- package/lib/testing.m.js +451 -20
- package/lib/testing.m.js.map +1 -1
- package/package.json +1 -1
- package/src/capabilities/crafting.ts +10 -1
- package/src/capabilities/gathering.ts +1 -1
- package/src/capabilities/hauling.ts +0 -5
- package/src/capabilities/modules.ts +6 -0
- package/src/capabilities/movement.ts +1 -1
- package/src/contracts/server.ts +293 -13
- package/src/data/capabilities.ts +18 -2
- package/src/data/capability-formulas.ts +5 -0
- package/src/data/entities.json +235 -20
- package/src/data/item-ids.ts +10 -0
- package/src/data/items.json +61 -0
- package/src/data/kind-registry.json +53 -1
- package/src/data/kind-registry.ts +5 -0
- package/src/data/metadata.ts +74 -5
- package/src/data/recipes-runtime.ts +1 -0
- package/src/data/recipes.json +895 -119
- package/src/derivation/capabilities.test.ts +105 -4
- package/src/derivation/capabilities.ts +162 -57
- package/src/derivation/capability-mappings.ts +2 -0
- package/src/derivation/crafting.ts +2 -0
- package/src/derivation/recipe-usage.test.ts +10 -7
- package/src/derivation/rollups.ts +16 -0
- package/src/derivation/stat-scaling.ts +12 -0
- package/src/entities/makers.ts +10 -1
- package/src/index-module.ts +29 -3
- package/src/managers/actions.ts +77 -46
- package/src/managers/construction-types.ts +2 -2
- package/src/managers/construction.ts +15 -15
- package/src/managers/plot.ts +1 -1
- package/src/nft/buildImmutableData.ts +55 -9
- package/src/nft/description.ts +99 -36
- package/src/planner/planner.test.ts +50 -41
- package/src/resolution/resolve-item.test.ts +1 -1
- package/src/resolution/resolve-item.ts +26 -11
- package/src/scheduling/availability.ts +7 -6
- package/src/scheduling/cancel.test.ts +40 -6
- package/src/scheduling/cancel.ts +21 -29
- package/src/scheduling/jobs.ts +71 -0
- package/src/scheduling/lanes.ts +29 -0
- package/src/scheduling/projection.ts +42 -25
- package/src/scheduling/task-cargo.ts +1 -1
- package/src/testing/projection-parity.ts +2 -2
- package/src/travel/reach.ts +4 -6
- package/src/travel/route-planner.ts +60 -49
- package/src/travel/route-simulator.ts +3 -7
- package/src/travel/travel.ts +14 -5
- package/src/types/capabilities.ts +9 -3
- package/src/types.ts +5 -3
- package/src/managers/flatten-gather-plan.test.ts +0 -80
|
@@ -10,6 +10,7 @@ function task(over: Partial<{type: number; duration: number; cancelable: number;
|
|
|
10
10
|
duration: over.duration ?? 100,
|
|
11
11
|
cancelable: over.cancelable ?? TaskCancelable.BEFORE_START,
|
|
12
12
|
cargo: [],
|
|
13
|
+
couplings: [],
|
|
13
14
|
...(over.group ? {entitygroup: over.group} : {}),
|
|
14
15
|
})
|
|
15
16
|
}
|
|
@@ -28,6 +29,7 @@ function entity(tasks: ReturnType<typeof task>[], startedISO = T0) {
|
|
|
28
29
|
lanes: [{lane_key: 0, schedule: {started: startedISO, tasks}}],
|
|
29
30
|
gatherer_lanes: [],
|
|
30
31
|
crafter_lanes: [],
|
|
32
|
+
builder_lanes: [],
|
|
31
33
|
loader_lanes: [],
|
|
32
34
|
holds: [],
|
|
33
35
|
})
|
|
@@ -101,14 +103,20 @@ describe('cancelEligibility — linked tasks', () => {
|
|
|
101
103
|
})
|
|
102
104
|
|
|
103
105
|
const HOLD_PULL = 1
|
|
106
|
+
const HOLD_BUILD = 4
|
|
104
107
|
function loadTask(giverType: string, giverId: number, holdId: number, qty: number) {
|
|
105
108
|
return ServerContract.Types.task.from({
|
|
106
109
|
type: TaskType.LOAD,
|
|
107
110
|
duration: 100,
|
|
108
111
|
cancelable: TaskCancelable.ALWAYS,
|
|
109
112
|
cargo: [{item_id: 7, stats: 0, modules: [], quantity: qty}],
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
couplings: [
|
|
114
|
+
{
|
|
115
|
+
counterpart: {entity_type: giverType, entity_id: giverId},
|
|
116
|
+
hold: holdId,
|
|
117
|
+
kind: HOLD_PULL,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
112
120
|
})
|
|
113
121
|
}
|
|
114
122
|
|
|
@@ -129,7 +137,13 @@ describe('cancelEligibility — effects', () => {
|
|
|
129
137
|
duration: 100,
|
|
130
138
|
cancelable: TaskCancelable.ALWAYS,
|
|
131
139
|
cargo: [],
|
|
132
|
-
|
|
140
|
+
couplings: [
|
|
141
|
+
{
|
|
142
|
+
counterpart: {entity_type: 'plot', entity_id: 55},
|
|
143
|
+
hold: 0,
|
|
144
|
+
kind: HOLD_BUILD,
|
|
145
|
+
},
|
|
146
|
+
],
|
|
133
147
|
}),
|
|
134
148
|
])
|
|
135
149
|
const plan = cancelEligibility(e, 0, 0, {now})
|
|
@@ -152,6 +166,7 @@ describe('cancelEligibility — effects', () => {
|
|
|
152
166
|
lanes: [{lane_key: 0, schedule: {started: T0, tasks: [lt]}}],
|
|
153
167
|
gatherer_lanes: [],
|
|
154
168
|
crafter_lanes: [],
|
|
169
|
+
builder_lanes: [],
|
|
155
170
|
loader_lanes: [],
|
|
156
171
|
holds: [
|
|
157
172
|
{
|
|
@@ -184,6 +199,7 @@ describe('cancelEligibility — effects', () => {
|
|
|
184
199
|
lanes: [{lane_key: 0, schedule: {started: T0, tasks: [lt]}}],
|
|
185
200
|
gatherer_lanes: [],
|
|
186
201
|
crafter_lanes: [],
|
|
202
|
+
builder_lanes: [],
|
|
187
203
|
loader_lanes: [],
|
|
188
204
|
holds: [
|
|
189
205
|
{
|
|
@@ -200,8 +216,7 @@ describe('cancelEligibility — effects', () => {
|
|
|
200
216
|
expect(plan.effects.releasedHolds[0]?.counterpart.entity_id.toNumber()).toBe(6)
|
|
201
217
|
})
|
|
202
218
|
|
|
203
|
-
test('
|
|
204
|
-
const lt = loadTask('warehouse', 6, 99, 4)
|
|
219
|
+
test('uncoupled task emits no releasedHolds entry', () => {
|
|
205
220
|
const upcoming = new Date('2026-06-18T23:59:50.000Z')
|
|
206
221
|
const e = ServerContract.Types.entity_info.from({
|
|
207
222
|
type: 'ship',
|
|
@@ -213,14 +228,24 @@ describe('cancelEligibility — effects', () => {
|
|
|
213
228
|
cargomass: 0,
|
|
214
229
|
cargo: [],
|
|
215
230
|
modules: [],
|
|
216
|
-
lanes: [
|
|
231
|
+
lanes: [
|
|
232
|
+
{
|
|
233
|
+
lane_key: 0,
|
|
234
|
+
schedule: {
|
|
235
|
+
started: T0,
|
|
236
|
+
tasks: [task({cancelable: TaskCancelable.ALWAYS, type: TaskType.LOAD})],
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
],
|
|
217
240
|
gatherer_lanes: [],
|
|
218
241
|
crafter_lanes: [],
|
|
242
|
+
builder_lanes: [],
|
|
219
243
|
loader_lanes: [],
|
|
220
244
|
holds: [],
|
|
221
245
|
})
|
|
222
246
|
const plan = cancelEligibility(e, 0, 0, {now: upcoming})
|
|
223
247
|
expect(plan.effects.releasedHolds).toHaveLength(0)
|
|
248
|
+
expect(plan.effects.refunds).toHaveLength(0)
|
|
224
249
|
})
|
|
225
250
|
})
|
|
226
251
|
|
|
@@ -233,6 +258,7 @@ describe('cancelEligibility — feasibility', () => {
|
|
|
233
258
|
duration: 50,
|
|
234
259
|
cancelable: TaskCancelable.ALWAYS,
|
|
235
260
|
cargo: [{item_id: 7, stats: 0, modules: [], quantity: 2}],
|
|
261
|
+
couplings: [],
|
|
236
262
|
})
|
|
237
263
|
const consumer = ServerContract.Types.task.from({
|
|
238
264
|
type: TaskType.CRAFT,
|
|
@@ -242,6 +268,7 @@ describe('cancelEligibility — feasibility', () => {
|
|
|
242
268
|
{item_id: 7, stats: 0, modules: [], quantity: 2},
|
|
243
269
|
{item_id: 9, stats: 0, modules: [], quantity: 1},
|
|
244
270
|
],
|
|
271
|
+
couplings: [],
|
|
245
272
|
})
|
|
246
273
|
const e = ServerContract.Types.entity_info.from({
|
|
247
274
|
type: 'ship',
|
|
@@ -265,6 +292,7 @@ describe('cancelEligibility — feasibility', () => {
|
|
|
265
292
|
],
|
|
266
293
|
gatherer_lanes: [],
|
|
267
294
|
crafter_lanes: [],
|
|
295
|
+
builder_lanes: [],
|
|
268
296
|
loader_lanes: [],
|
|
269
297
|
holds: [],
|
|
270
298
|
})
|
|
@@ -279,12 +307,14 @@ describe('cancelEligibility — feasibility', () => {
|
|
|
279
307
|
duration: 50,
|
|
280
308
|
cancelable: TaskCancelable.ALWAYS,
|
|
281
309
|
cargo: [{item_id: 7, stats: 0, modules: [], quantity: 2}],
|
|
310
|
+
couplings: [],
|
|
282
311
|
})
|
|
283
312
|
const independent = ServerContract.Types.task.from({
|
|
284
313
|
type: TaskType.TRAVEL,
|
|
285
314
|
duration: 50,
|
|
286
315
|
cancelable: TaskCancelable.ALWAYS,
|
|
287
316
|
cargo: [],
|
|
317
|
+
couplings: [],
|
|
288
318
|
})
|
|
289
319
|
const e = ServerContract.Types.entity_info.from({
|
|
290
320
|
type: 'ship',
|
|
@@ -302,6 +332,7 @@ describe('cancelEligibility — feasibility', () => {
|
|
|
302
332
|
],
|
|
303
333
|
gatherer_lanes: [],
|
|
304
334
|
crafter_lanes: [],
|
|
335
|
+
builder_lanes: [],
|
|
305
336
|
loader_lanes: [],
|
|
306
337
|
holds: [],
|
|
307
338
|
})
|
|
@@ -315,12 +346,14 @@ describe('cancelEligibility — feasibility', () => {
|
|
|
315
346
|
duration: 50,
|
|
316
347
|
cancelable: TaskCancelable.ALWAYS,
|
|
317
348
|
cargo: [moduledCargo],
|
|
349
|
+
couplings: [],
|
|
318
350
|
})
|
|
319
351
|
const consumer = ServerContract.Types.task.from({
|
|
320
352
|
type: TaskType.CRAFT,
|
|
321
353
|
duration: 50,
|
|
322
354
|
cancelable: TaskCancelable.ALWAYS,
|
|
323
355
|
cargo: [moduledCargo, {item_id: 9, stats: 0, modules: [], quantity: 1}],
|
|
356
|
+
couplings: [],
|
|
324
357
|
})
|
|
325
358
|
const e = ServerContract.Types.entity_info.from({
|
|
326
359
|
type: 'ship',
|
|
@@ -338,6 +371,7 @@ describe('cancelEligibility — feasibility', () => {
|
|
|
338
371
|
],
|
|
339
372
|
gatherer_lanes: [],
|
|
340
373
|
crafter_lanes: [],
|
|
374
|
+
builder_lanes: [],
|
|
341
375
|
loader_lanes: [],
|
|
342
376
|
holds: [],
|
|
343
377
|
})
|
package/src/scheduling/cancel.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type {ServerContract} from '../contracts'
|
|
2
|
-
import {TaskCancelable, TaskType} from '../types'
|
|
2
|
+
import {HoldKind, TaskCancelable, TaskType} from '../types'
|
|
3
3
|
import {calcCargoItemMass} from '../capabilities/storage'
|
|
4
4
|
import {taskCargoEffect, cargoKey} from './availability'
|
|
5
5
|
import * as schedule from './schedule'
|
|
@@ -169,34 +169,26 @@ export function cancelEligibility(
|
|
|
169
169
|
if (timing[i].running && t.cancelable.equals(TaskCancelable.ALWAYS))
|
|
170
170
|
effects.abandonsRunning = true
|
|
171
171
|
if (t.energy_cost) energyForfeited += t.energy_cost.toNumber()
|
|
172
|
-
if (t.type.equals(TaskType.BUILDPLOT) && t.
|
|
173
|
-
effects.keepsPlotDeposits = {plot: t.
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
: Number.MAX_SAFE_INTEGER
|
|
193
|
-
if (giver.cargomass.toNumber() + returned > cap) {
|
|
194
|
-
return {
|
|
195
|
-
ok: false,
|
|
196
|
-
blockedReason: CancelBlockReason.WOULD_OVERFILL,
|
|
197
|
-
range,
|
|
198
|
-
effects: {...EMPTY_EFFECTS},
|
|
199
|
-
}
|
|
172
|
+
if (t.type.equals(TaskType.BUILDPLOT) && t.couplings.length > 0)
|
|
173
|
+
effects.keepsPlotDeposits = {plot: t.couplings[0].counterpart}
|
|
174
|
+
for (const c of t.couplings) {
|
|
175
|
+
const kind = c.kind.toNumber()
|
|
176
|
+
effects.releasedHolds.push({counterpart: c.counterpart, kind})
|
|
177
|
+
if (kind === HoldKind.PULL) {
|
|
178
|
+
effects.refunds.push({giver: c.counterpart, cargo: t.cargo})
|
|
179
|
+
const giver = input.counterparts?.get(c.counterpart.entity_id.toString())
|
|
180
|
+
if (giver) {
|
|
181
|
+
const returned = t.cargo.reduce(
|
|
182
|
+
(s, c) => s + calcCargoItemMass(c).toNumber(),
|
|
183
|
+
0
|
|
184
|
+
)
|
|
185
|
+
const cap = giver.capacity ? giver.capacity.toNumber() : Number.MAX_SAFE_INTEGER
|
|
186
|
+
if (giver.cargomass.toNumber() + returned > cap) {
|
|
187
|
+
return {
|
|
188
|
+
ok: false,
|
|
189
|
+
blockedReason: CancelBlockReason.WOULD_OVERFILL,
|
|
190
|
+
range,
|
|
191
|
+
effects: {...EMPTY_EFFECTS},
|
|
200
192
|
}
|
|
201
193
|
}
|
|
202
194
|
}
|
|
@@ -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
|
+
}
|
package/src/scheduling/lanes.ts
CHANGED
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
computeLoaderMass,
|
|
12
12
|
computeCrafterSpeed,
|
|
13
13
|
computeCrafterDrain,
|
|
14
|
+
computeBuilderSpeed,
|
|
15
|
+
computeBuilderDrain,
|
|
14
16
|
} from '../nft/description'
|
|
15
17
|
import type {ModuleType} from '../types'
|
|
16
18
|
import {getLane, LANE_MOBILITY, type ScheduleData} from './schedule'
|
|
@@ -27,6 +29,13 @@ export interface ResolvedGathererLane {
|
|
|
27
29
|
outputPct: number
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
export interface ResolvedBuilderLane {
|
|
33
|
+
slotIndex: number
|
|
34
|
+
speed: number
|
|
35
|
+
drain: number
|
|
36
|
+
outputPct: number
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
export interface ResolvedCrafterLane {
|
|
31
40
|
slotIndex: number
|
|
32
41
|
speed: number
|
|
@@ -110,6 +119,26 @@ export function resolveLaneCrafter(
|
|
|
110
119
|
return {slotIndex: idx, speed, drain, outputPct: amp}
|
|
111
120
|
}
|
|
112
121
|
|
|
122
|
+
export function resolveLaneBuilder(
|
|
123
|
+
modules: ModuleEntry[],
|
|
124
|
+
entityItemId: number,
|
|
125
|
+
laneKey: number
|
|
126
|
+
): ResolvedBuilderLane {
|
|
127
|
+
const idx = laneKey - 1
|
|
128
|
+
const installed = idx >= 0 && idx < modules.length ? modules[idx].installed : undefined
|
|
129
|
+
if (!installed) throw new Error('builder lane has no module')
|
|
130
|
+
const item = getItem(Number(installed.item_id.value ?? installed.item_id))
|
|
131
|
+
if (item.moduleType !== 'builder') throw new Error('lane module is not a builder')
|
|
132
|
+
const stats = BigInt(installed.stats.toString())
|
|
133
|
+
const res = decodeStat(stats, 0)
|
|
134
|
+
const fin = decodeStat(stats, 1)
|
|
135
|
+
const layout = getEntityLayout(entityItemId)?.slots ?? []
|
|
136
|
+
const amp = getSlotAmp(layout, idx)
|
|
137
|
+
const speed = applySlotMultiplier(computeBuilderSpeed(res), amp)
|
|
138
|
+
const drain = computeBuilderDrain(fin)
|
|
139
|
+
return {slotIndex: idx, speed, drain, outputPct: amp}
|
|
140
|
+
}
|
|
141
|
+
|
|
113
142
|
// LANE_MOBILITY or a missing module soft-returns valid=false (never throws); callers check `valid`.
|
|
114
143
|
export function resolveLaneLoader(
|
|
115
144
|
modules: ModuleEntry[],
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {Name, UInt16, UInt32, UInt64} from '@wharfkit/antelope'
|
|
1
|
+
import {Name, type UInt16, UInt32, UInt64} from '@wharfkit/antelope'
|
|
2
2
|
import {ServerContract} from '../contracts'
|
|
3
3
|
import {Coordinates, TaskType} from '../types'
|
|
4
4
|
import {
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
ENTITY_CARGO_NOT_LOADED,
|
|
17
17
|
} from '../errors'
|
|
18
18
|
import {getEntityLayout, getRecipe, type RecipeInput} from '../data/recipes-runtime'
|
|
19
|
+
import {ITEM_SHIP_T1_PACKED} from '../data/item-ids'
|
|
19
20
|
import {computeEntityCapabilities} from '../derivation/capabilities'
|
|
20
21
|
import {decodeCraftedItemStats, decodeStat} from '../derivation/crafting'
|
|
21
22
|
import {packedModulesToInstalled, type InstalledModule} from '../entities/slot-multiplier'
|
|
@@ -33,7 +34,7 @@ import type {ScheduleData} from './schedule'
|
|
|
33
34
|
|
|
34
35
|
export interface ProjectedEntity {
|
|
35
36
|
location: Coordinates
|
|
36
|
-
energy:
|
|
37
|
+
energy: UInt32
|
|
37
38
|
cargo: CargoStack[]
|
|
38
39
|
shipMass: UInt32
|
|
39
40
|
capacity?: UInt64
|
|
@@ -46,6 +47,7 @@ export interface ProjectedEntity {
|
|
|
46
47
|
readonly totalMass: UInt64
|
|
47
48
|
readonly gathererLanes: ServerContract.Types.gatherer_lane[]
|
|
48
49
|
readonly crafterLanes: ServerContract.Types.crafter_lane[]
|
|
50
|
+
readonly builderLanes: ServerContract.Types.builder_lane[]
|
|
49
51
|
|
|
50
52
|
hasMovement(): boolean
|
|
51
53
|
hasStorage(): boolean
|
|
@@ -58,13 +60,14 @@ export interface ProjectedEntity {
|
|
|
58
60
|
|
|
59
61
|
export interface Projectable extends ScheduleData {
|
|
60
62
|
coordinates: Coordinates | ServerContract.Types.coordinates
|
|
61
|
-
energy?:
|
|
63
|
+
energy?: UInt32
|
|
62
64
|
hullmass?: UInt32
|
|
63
65
|
generator?: ServerContract.Types.energy_stats
|
|
64
66
|
engines?: ServerContract.Types.movement_stats
|
|
65
67
|
loader_lanes?: ServerContract.Types.loader_lane[]
|
|
66
68
|
gatherer_lanes?: ServerContract.Types.gatherer_lane[]
|
|
67
69
|
crafter_lanes?: ServerContract.Types.crafter_lane[]
|
|
70
|
+
builder_lanes?: ServerContract.Types.builder_lane[]
|
|
68
71
|
hauler?: ServerContract.Types.hauler_stats
|
|
69
72
|
launcher?: ServerContract.Types.launcher_stats
|
|
70
73
|
capacity?: UInt32
|
|
@@ -93,6 +96,7 @@ interface ProjectedCaps {
|
|
|
93
96
|
loaderLanes: ServerContract.Types.loader_lane[]
|
|
94
97
|
gathererLanes: ServerContract.Types.gatherer_lane[]
|
|
95
98
|
crafterLanes: ServerContract.Types.crafter_lane[]
|
|
99
|
+
builderLanes: ServerContract.Types.builder_lane[]
|
|
96
100
|
hauler?: ServerContract.Types.hauler_stats
|
|
97
101
|
launcher?: ServerContract.Types.launcher_stats
|
|
98
102
|
}
|
|
@@ -112,6 +116,14 @@ function recomputeCaps(entity: Projectable): ProjectedCaps | undefined {
|
|
|
112
116
|
const hullStats = decodeCraftedItemStats(itemId, entity.stats)
|
|
113
117
|
if (hullStats.strength === undefined) hullStats.strength = decodeStat(entity.stats, 0)
|
|
114
118
|
if (hullStats.hardness === undefined) hullStats.hardness = decodeStat(entity.stats, 2)
|
|
119
|
+
// The retired Ship T1 recipe only assigned names to its first two stat slots.
|
|
120
|
+
// Its on-chain entity row still carries five packed channels, and the ship hull
|
|
121
|
+
// formula deliberately consumes all five. Keep the unlabelled channels internal
|
|
122
|
+
// to projection so replay remains contract-parity until deployment replaces it.
|
|
123
|
+
if (itemId === ITEM_SHIP_T1_PACKED) {
|
|
124
|
+
hullStats.__legacyShipChannel3 = decodeStat(entity.stats, 3)
|
|
125
|
+
hullStats.__legacyShipChannel4 = decodeStat(entity.stats, 4)
|
|
126
|
+
}
|
|
115
127
|
const layout = getEntityLayout(itemId)?.slots ?? []
|
|
116
128
|
const installed = toInstalledModules(entity.modules)
|
|
117
129
|
const caps = computeEntityCapabilities(hullStats, itemId, installed, layout)
|
|
@@ -157,6 +169,19 @@ function recomputeCaps(entity: Projectable): ProjectedCaps | undefined {
|
|
|
157
169
|
output_pct: l.outputPct,
|
|
158
170
|
})
|
|
159
171
|
|
|
172
|
+
const toBuilderLane = (l: {
|
|
173
|
+
slotIndex: number
|
|
174
|
+
speed: number
|
|
175
|
+
drain: number
|
|
176
|
+
outputPct: number
|
|
177
|
+
}): ServerContract.Types.builder_lane =>
|
|
178
|
+
ServerContract.Types.builder_lane.from({
|
|
179
|
+
slot_index: l.slotIndex,
|
|
180
|
+
speed: l.speed,
|
|
181
|
+
drain: l.drain,
|
|
182
|
+
output_pct: l.outputPct,
|
|
183
|
+
})
|
|
184
|
+
|
|
160
185
|
return {
|
|
161
186
|
hullmass: UInt32.from(caps.hullmass),
|
|
162
187
|
capacity: UInt32.from(caps.capacity),
|
|
@@ -167,6 +192,7 @@ function recomputeCaps(entity: Projectable): ProjectedCaps | undefined {
|
|
|
167
192
|
loaderLanes: (caps.loaderLanes ?? []).map(toLoaderLane),
|
|
168
193
|
gathererLanes: (caps.gathererLanes ?? []).map(toGathererLane),
|
|
169
194
|
crafterLanes: (caps.crafterLanes ?? []).map(toCrafterLane),
|
|
195
|
+
builderLanes: (caps.builderLanes ?? []).map(toBuilderLane),
|
|
170
196
|
hauler: caps.hauler
|
|
171
197
|
? ServerContract.Types.hauler_stats.from({
|
|
172
198
|
capacity: caps.hauler.capacity,
|
|
@@ -206,6 +232,7 @@ export function createProjectedEntity(entity: Projectable): ProjectedEntity {
|
|
|
206
232
|
const loaderLanes = entity.loader_lanes ?? caps?.loaderLanes ?? []
|
|
207
233
|
const gathererLanes = entity.gatherer_lanes ?? caps?.gathererLanes ?? []
|
|
208
234
|
const crafterLanes = entity.crafter_lanes ?? caps?.crafterLanes ?? []
|
|
235
|
+
const builderLanes = entity.builder_lanes ?? caps?.builderLanes ?? []
|
|
209
236
|
const engines = entity.engines ?? caps?.engines
|
|
210
237
|
const generator = entity.generator ?? caps?.generator
|
|
211
238
|
const hauler = entity.hauler ?? caps?.hauler
|
|
@@ -216,7 +243,7 @@ export function createProjectedEntity(entity: Projectable): ProjectedEntity {
|
|
|
216
243
|
|
|
217
244
|
const projected: ProjectedEntity = {
|
|
218
245
|
location: Coordinates.from(entity.coordinates),
|
|
219
|
-
energy:
|
|
246
|
+
energy: UInt32.from(entity.energy ?? 0),
|
|
220
247
|
cargo,
|
|
221
248
|
shipMass,
|
|
222
249
|
capacity: capacity ? UInt64.from(capacity) : undefined,
|
|
@@ -227,6 +254,7 @@ export function createProjectedEntity(entity: Projectable): ProjectedEntity {
|
|
|
227
254
|
loaderLanes,
|
|
228
255
|
gathererLanes,
|
|
229
256
|
crafterLanes,
|
|
257
|
+
builderLanes,
|
|
230
258
|
|
|
231
259
|
get cargoMass() {
|
|
232
260
|
return calcStacksMass(this.cargo)
|
|
@@ -286,12 +314,12 @@ function applyRechargeTask(
|
|
|
286
314
|
if (!projected.generator) return
|
|
287
315
|
|
|
288
316
|
if (options.complete) {
|
|
289
|
-
projected.energy =
|
|
317
|
+
projected.energy = UInt32.from(projected.generator.capacity)
|
|
290
318
|
} else if (options.progress !== undefined) {
|
|
291
319
|
const capacity = Number(projected.generator.capacity)
|
|
292
320
|
const currentEnergy = Number(projected.energy)
|
|
293
321
|
const rechargeAmount = (capacity - currentEnergy) * options.progress
|
|
294
|
-
projected.energy =
|
|
322
|
+
projected.energy = UInt32.from(Math.min(capacity, currentEnergy + rechargeAmount))
|
|
295
323
|
}
|
|
296
324
|
}
|
|
297
325
|
|
|
@@ -317,8 +345,8 @@ function applyFlightTask(
|
|
|
317
345
|
Math.floor(Number(task.energy_cost ?? 0) * options.progress)
|
|
318
346
|
)
|
|
319
347
|
projected.energy = projected.energy.gt(partialEnergy)
|
|
320
|
-
?
|
|
321
|
-
:
|
|
348
|
+
? UInt32.from(projected.energy.subtracting(partialEnergy))
|
|
349
|
+
: UInt32.from(0)
|
|
322
350
|
}
|
|
323
351
|
}
|
|
324
352
|
|
|
@@ -344,10 +372,10 @@ function applyRemoveCargoTask(projected: ProjectedEntity, task: ServerContract.T
|
|
|
344
372
|
|
|
345
373
|
function applyEnergyCost(projected: ProjectedEntity, task: ServerContract.Types.task): void {
|
|
346
374
|
if (!task.energy_cost) return
|
|
347
|
-
const energyCost =
|
|
375
|
+
const energyCost = UInt32.from(task.energy_cost)
|
|
348
376
|
projected.energy = projected.energy.gt(energyCost)
|
|
349
|
-
?
|
|
350
|
-
:
|
|
377
|
+
? UInt32.from(projected.energy.subtracting(energyCost))
|
|
378
|
+
: UInt32.from(0)
|
|
351
379
|
}
|
|
352
380
|
|
|
353
381
|
function applyGatherTask(
|
|
@@ -357,7 +385,7 @@ function applyGatherTask(
|
|
|
357
385
|
): void {
|
|
358
386
|
if (!options.complete) return
|
|
359
387
|
applyEnergyCost(projected, task)
|
|
360
|
-
if (
|
|
388
|
+
if (task.couplings.length === 0) {
|
|
361
389
|
applyAddCargoTask(projected, task)
|
|
362
390
|
}
|
|
363
391
|
}
|
|
@@ -369,13 +397,8 @@ function applyCraftTask(projected: ProjectedEntity, task: ServerContract.Types.t
|
|
|
369
397
|
for (let i = 0; i < task.cargo.length - 1; i++) {
|
|
370
398
|
removeCargoItem(projected, task.cargo[i])
|
|
371
399
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
function applyDeployTask(projected: ProjectedEntity, task: ServerContract.Types.task): void {
|
|
376
|
-
applyEnergyCost(projected, task)
|
|
377
|
-
if (task.cargo.length > 0) {
|
|
378
|
-
removeCargoItem(projected, task.cargo[0])
|
|
400
|
+
if (task.couplings.length === 0) {
|
|
401
|
+
addCargoItem(projected, task.cargo[task.cargo.length - 1])
|
|
379
402
|
}
|
|
380
403
|
}
|
|
381
404
|
|
|
@@ -402,9 +425,6 @@ function applyTask(projected: ProjectedEntity, task: ServerContract.Types.task):
|
|
|
402
425
|
case TaskType.CRAFT:
|
|
403
426
|
applyCraftTask(projected, task)
|
|
404
427
|
break
|
|
405
|
-
case TaskType.DEPLOY:
|
|
406
|
-
applyDeployTask(projected, task)
|
|
407
|
-
break
|
|
408
428
|
case TaskType.UNDEPLOY:
|
|
409
429
|
case TaskType.DEMOLISH:
|
|
410
430
|
break
|
|
@@ -561,9 +581,6 @@ export function projectEntityAt(entity: Projectable, now: Date): ProjectedEntity
|
|
|
561
581
|
case TaskType.CRAFT:
|
|
562
582
|
if (taskComplete) applyCraftTask(projected, task)
|
|
563
583
|
break
|
|
564
|
-
case TaskType.DEPLOY:
|
|
565
|
-
if (taskComplete) applyDeployTask(projected, task)
|
|
566
|
-
break
|
|
567
584
|
case TaskType.UNDEPLOY:
|
|
568
585
|
case TaskType.DEMOLISH:
|
|
569
586
|
break
|
|
@@ -32,7 +32,7 @@ export function taskCargoChanges(task: ServerContract.Types.task): TaskCargoChan
|
|
|
32
32
|
case TaskType.UNWRAP:
|
|
33
33
|
return items.map((i) => toChange(i, 'in'))
|
|
34
34
|
case TaskType.GATHER:
|
|
35
|
-
return task.
|
|
35
|
+
return task.couplings.length > 0 ? [] : items.map((i) => toChange(i, 'in'))
|
|
36
36
|
case TaskType.UNLOAD:
|
|
37
37
|
return items.map((i) => toChange(i, 'out'))
|
|
38
38
|
case TaskType.CRAFT:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {UInt32} from '@wharfkit/antelope'
|
|
2
2
|
import type {ServerContract} from '../contracts'
|
|
3
3
|
import type {ProjectedEntity} from '../scheduling/projection'
|
|
4
4
|
import {type CargoStack, cargoItemToStack, mergeStacks} from '../capabilities/storage'
|
|
@@ -6,7 +6,7 @@ import {type CargoStack, cargoItemToStack, mergeStacks} from '../capabilities/st
|
|
|
6
6
|
export interface ContractProjectedState {
|
|
7
7
|
owner: {toString(): string}
|
|
8
8
|
coordinates: ServerContract.Types.coordinates
|
|
9
|
-
energy?:
|
|
9
|
+
energy?: UInt32
|
|
10
10
|
cargomass: UInt32
|
|
11
11
|
cargo: ServerContract.Types.cargo_view[]
|
|
12
12
|
hullmass?: UInt32
|
package/src/travel/reach.ts
CHANGED
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
export interface ReachStats {
|
|
2
2
|
generator?: {capacity: bigint}
|
|
3
3
|
engines?: {drain: bigint}
|
|
4
|
-
hauler?: {drain: bigint}
|
|
5
4
|
}
|
|
6
5
|
|
|
7
|
-
export function computePerLegReach(s: ReachStats
|
|
6
|
+
export function computePerLegReach(s: ReachStats): number {
|
|
8
7
|
const capacity = s.generator?.capacity
|
|
9
8
|
const drain = s.engines?.drain
|
|
10
9
|
if (capacity === undefined || drain === undefined || drain === 0n) {
|
|
11
10
|
throw new Error('entity has no usable engine/generator (cannot compute per-leg reach)')
|
|
12
11
|
}
|
|
13
|
-
|
|
14
|
-
return Number(capacity) / Number(drain + haulDrain)
|
|
12
|
+
return Number(capacity) / Number(drain)
|
|
15
13
|
}
|
|
16
14
|
|
|
17
|
-
export function computeGroupPerLegReach(participants: ReachStats[]
|
|
15
|
+
export function computeGroupPerLegReach(participants: ReachStats[]): number {
|
|
18
16
|
const movers = participants.filter((p) => p.engines !== undefined && p.engines.drain !== 0n)
|
|
19
17
|
if (movers.length === 0) {
|
|
20
18
|
throw new Error('group has no moving entity (cannot compute per-leg reach)')
|
|
21
19
|
}
|
|
22
|
-
return Math.min(...movers.map(
|
|
20
|
+
return Math.min(...movers.map(computePerLegReach))
|
|
23
21
|
}
|