@shipload/sdk 1.0.0-next.65 → 1.0.0-next.67
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 +786 -321
- package/lib/shipload.js +2731 -1305
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +2644 -1286
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +223 -146
- package/lib/testing.js +907 -563
- package/lib/testing.js.map +1 -1
- package/lib/testing.m.js +907 -563
- package/lib/testing.m.js.map +1 -1
- package/package.json +1 -1
- package/src/capabilities/crafting.test.ts +51 -6
- package/src/capabilities/crafting.ts +19 -4
- package/src/capabilities/gathering.ts +2 -2
- package/src/capabilities/storage.test.ts +26 -0
- package/src/capabilities/storage.ts +6 -6
- package/src/contracts/fund.ts +79 -2
- package/src/contracts/platform.ts +10 -1
- package/src/contracts/server.ts +521 -330
- package/src/data/charters.json +325 -127
- package/src/data/entities.json +29 -24
- package/src/data/items.json +114 -114
- package/src/data/kind-registry.json +3 -3
- package/src/data/kind-registry.ts +5 -0
- package/src/data/metadata.ts +9 -2
- package/src/data/recipes.json +56 -95
- package/src/derivation/build-methods.test.ts +59 -1
- package/src/derivation/build-methods.ts +2 -0
- package/src/derivation/capabilities.ts +7 -7
- package/src/derivation/strata.ts +5 -3
- package/src/derivation/tiers.ts +5 -5
- package/src/entity/hydrate.test.ts +156 -0
- package/src/entity/hydrate.ts +86 -0
- package/src/errors.ts +9 -0
- package/src/format.ts +15 -13
- package/src/index-module.ts +32 -5
- package/src/influence/ballot.test.ts +103 -0
- package/src/influence/ballot.ts +98 -0
- package/src/influence/charter-metadata.test.ts +136 -0
- package/src/influence/charter-metadata.ts +236 -0
- package/src/influence/charters.test.ts +51 -65
- package/src/influence/charters.ts +70 -40
- package/src/influence/civic-shuttle.test.ts +159 -0
- package/src/influence/civic-shuttle.ts +115 -0
- package/src/influence/civic.test.ts +287 -0
- package/src/influence/civic.ts +269 -0
- package/src/influence/constants.ts +30 -9
- package/src/influence/decomp.ts +15 -15
- package/src/influence/duration.ts +42 -6
- package/src/influence/index.ts +59 -6
- package/src/influence/influence.test.ts +76 -9
- package/src/influence/valuation.ts +17 -17
- package/src/managers/actions.ts +99 -37
- package/src/managers/base.ts +4 -0
- package/src/managers/context.ts +3 -1
- package/src/managers/epochs.ts +32 -1
- package/src/managers/index.ts +4 -2
- package/src/managers/influence.test.ts +18 -20
- package/src/managers/influence.ts +137 -84
- package/src/managers/jobs.test.ts +2 -2
- package/src/managers/jobs.ts +4 -4
- package/src/nft/description.ts +10 -11
- package/src/planner/index.ts +54 -0
- package/src/planner/planner.test.ts +127 -2
- package/src/resolution/display-name.ts +1 -1
- package/src/scheduling/availability.test.ts +113 -0
- package/src/scheduling/availability.ts +48 -8
- package/src/scheduling/epoch.ts +1 -0
- package/src/scheduling/jobs.ts +1 -1
- package/src/scheduling/projection.ts +2 -16
- package/src/scheduling/task-cargo.test.ts +62 -1
- package/src/scheduling/task-cargo.ts +25 -10
- package/src/scheduling/task-effects.test.ts +35 -0
- package/src/scheduling/task-effects.ts +34 -0
- package/src/scheduling/unwrap.test.ts +3 -3
- package/src/scheduling/unwrap.ts +3 -3
- package/src/shipload.ts +22 -3
- package/src/travel/reach.test.ts +86 -0
- package/src/travel/reach.ts +23 -10
- package/src/travel/route-build.test.ts +63 -0
- package/src/travel/route-build.ts +60 -0
- package/src/travel/travel.ts +2 -1
- package/src/types.ts +6 -2
- package/src/utils/hash.ts +18 -2
- package/src/utils/system.ts +6 -1
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import {UInt8, UInt16, UInt32, UInt64} from '@wharfkit/antelope'
|
|
2
2
|
import {expect, test, describe} from 'bun:test'
|
|
3
3
|
import {ServerContract} from '../contracts'
|
|
4
|
+
import {encodeStats} from '../derivation/crafting'
|
|
5
|
+
import {ITEM_GATHERER_T1} from '../data/item-ids'
|
|
4
6
|
import {
|
|
5
7
|
planParallelTransfer,
|
|
8
|
+
planSingleGather,
|
|
6
9
|
type GatherPlanEntity,
|
|
7
10
|
gatherEnergyCost,
|
|
8
11
|
splitCost,
|
|
@@ -10,6 +13,19 @@ import {
|
|
|
10
13
|
buildGatherPlan,
|
|
11
14
|
} from './index'
|
|
12
15
|
|
|
16
|
+
type ModuleEntry = ServerContract.Types.module_entry
|
|
17
|
+
type Lane = ServerContract.Types.lane
|
|
18
|
+
|
|
19
|
+
function makeModuleEntry(itemId: number, stats: bigint): ModuleEntry {
|
|
20
|
+
return {
|
|
21
|
+
type: UInt8.from(0),
|
|
22
|
+
installed: {
|
|
23
|
+
item_id: UInt16.from(itemId),
|
|
24
|
+
stats: UInt64.from(stats),
|
|
25
|
+
},
|
|
26
|
+
} as unknown as ModuleEntry
|
|
27
|
+
}
|
|
28
|
+
|
|
13
29
|
function gathererLane(
|
|
14
30
|
slotIndex: number,
|
|
15
31
|
yieldVal: number,
|
|
@@ -114,7 +130,7 @@ describe('planParallelTransfer', () => {
|
|
|
114
130
|
|
|
115
131
|
describe('cost helpers', () => {
|
|
116
132
|
const STRATUM = 100
|
|
117
|
-
const ITEM_MASS =
|
|
133
|
+
const ITEM_MASS = 10
|
|
118
134
|
const RICHNESS = 500
|
|
119
135
|
|
|
120
136
|
test('gatherEnergyCost is 0 for non-positive quantity and positive otherwise', () => {
|
|
@@ -152,7 +168,7 @@ describe('buildGatherPlan', () => {
|
|
|
152
168
|
const STRATUM = 100
|
|
153
169
|
const OPTS = {
|
|
154
170
|
richness: 500,
|
|
155
|
-
itemMass:
|
|
171
|
+
itemMass: 10,
|
|
156
172
|
holdRoom: 1_000_000,
|
|
157
173
|
reserveRemaining: 1_000_000,
|
|
158
174
|
now: NOW,
|
|
@@ -332,3 +348,112 @@ describe('buildGatherPlan', () => {
|
|
|
332
348
|
expect(plan.cycles[0].rechargeSeconds).toBe(0)
|
|
333
349
|
})
|
|
334
350
|
})
|
|
351
|
+
|
|
352
|
+
describe('planSingleGather', () => {
|
|
353
|
+
const STRATUM = 100
|
|
354
|
+
const ITEM_MASS = 1000
|
|
355
|
+
const RICHNESS = 500
|
|
356
|
+
const deepStats = encodeStats([100, 900, 100]) // tol=900 => depth=5000 for tier1
|
|
357
|
+
|
|
358
|
+
test('mirrors the bot hand-assembly: laneKey, quantity, energyCost match selectGatherLane + maxQtyForCharge + splitCost', () => {
|
|
359
|
+
const modules: ModuleEntry[] = [makeModuleEntry(ITEM_GATHERER_T1, deepStats)]
|
|
360
|
+
const lane = gathererLane(0, 700, 1_250_000, 5000)
|
|
361
|
+
|
|
362
|
+
const result = planSingleGather({
|
|
363
|
+
modules,
|
|
364
|
+
entityItemId: ITEM_GATHERER_T1,
|
|
365
|
+
lanes: [],
|
|
366
|
+
gathererLanes: [lane],
|
|
367
|
+
stratum: STRATUM,
|
|
368
|
+
desiredQuantity: 100,
|
|
369
|
+
itemMass: ITEM_MASS,
|
|
370
|
+
richness: RICHNESS,
|
|
371
|
+
generatorCapacity: 1_000_000,
|
|
372
|
+
currentEnergy: 1_000_000,
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
const expectedLaneKey = 1
|
|
376
|
+
const expectedQuantity = maxQtyForCharge(
|
|
377
|
+
[lane],
|
|
378
|
+
100,
|
|
379
|
+
1_000_000,
|
|
380
|
+
STRATUM,
|
|
381
|
+
ITEM_MASS,
|
|
382
|
+
RICHNESS
|
|
383
|
+
)
|
|
384
|
+
const expectedCost = splitCost([lane], expectedQuantity, STRATUM, ITEM_MASS, RICHNESS)
|
|
385
|
+
|
|
386
|
+
expect(result.laneKey).toBe(expectedLaneKey)
|
|
387
|
+
expect(result.quantity).toBe(expectedQuantity)
|
|
388
|
+
expect(result.energyCost).toBe(expectedCost)
|
|
389
|
+
expect(result.needsRecharge).toBe(false)
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
test('needsRecharge is true when current energy is below the priced cost', () => {
|
|
393
|
+
const modules: ModuleEntry[] = [makeModuleEntry(ITEM_GATHERER_T1, deepStats)]
|
|
394
|
+
const lane = gathererLane(0, 700, 1_250_000, 5000)
|
|
395
|
+
|
|
396
|
+
const result = planSingleGather({
|
|
397
|
+
modules,
|
|
398
|
+
entityItemId: ITEM_GATHERER_T1,
|
|
399
|
+
lanes: [],
|
|
400
|
+
gathererLanes: [lane],
|
|
401
|
+
stratum: STRATUM,
|
|
402
|
+
desiredQuantity: 100,
|
|
403
|
+
itemMass: ITEM_MASS,
|
|
404
|
+
richness: RICHNESS,
|
|
405
|
+
generatorCapacity: 1_000_000,
|
|
406
|
+
currentEnergy: 0,
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
expect(result.energyCost).toBeGreaterThan(0)
|
|
410
|
+
expect(result.needsRecharge).toBe(true)
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
test('caps quantity to what fits one full charge, same boundary as maxQtyForCharge', () => {
|
|
414
|
+
const modules: ModuleEntry[] = [makeModuleEntry(ITEM_GATHERER_T1, deepStats)]
|
|
415
|
+
const lane = gathererLane(0, 700, 1_250_000, 5000)
|
|
416
|
+
const capacity = 40_000
|
|
417
|
+
|
|
418
|
+
const result = planSingleGather({
|
|
419
|
+
modules,
|
|
420
|
+
entityItemId: ITEM_GATHERER_T1,
|
|
421
|
+
lanes: [],
|
|
422
|
+
gathererLanes: [lane],
|
|
423
|
+
stratum: STRATUM,
|
|
424
|
+
desiredQuantity: 100_000,
|
|
425
|
+
itemMass: ITEM_MASS,
|
|
426
|
+
richness: RICHNESS,
|
|
427
|
+
generatorCapacity: capacity,
|
|
428
|
+
currentEnergy: capacity,
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
expect(
|
|
432
|
+
splitCost([lane], result.quantity, STRATUM, ITEM_MASS, RICHNESS)
|
|
433
|
+
).toBeLessThanOrEqual(capacity)
|
|
434
|
+
expect(
|
|
435
|
+
splitCost([lane], result.quantity + 1, STRATUM, ITEM_MASS, RICHNESS)
|
|
436
|
+
).toBeGreaterThan(capacity)
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
test('throws when no gatherer lane reaches the stratum, same as selectGatherLane', () => {
|
|
440
|
+
const shallowStats = encodeStats([100, 10, 100]) // depth=550 for tier1
|
|
441
|
+
const modules: ModuleEntry[] = [makeModuleEntry(ITEM_GATHERER_T1, shallowStats)]
|
|
442
|
+
const lane = gathererLane(0, 700, 1_250_000, 550)
|
|
443
|
+
|
|
444
|
+
expect(() =>
|
|
445
|
+
planSingleGather({
|
|
446
|
+
modules,
|
|
447
|
+
entityItemId: ITEM_GATHERER_T1,
|
|
448
|
+
lanes: [] as Lane[],
|
|
449
|
+
gathererLanes: [lane],
|
|
450
|
+
stratum: 5000,
|
|
451
|
+
desiredQuantity: 100,
|
|
452
|
+
itemMass: ITEM_MASS,
|
|
453
|
+
richness: RICHNESS,
|
|
454
|
+
generatorCapacity: 1_000_000,
|
|
455
|
+
currentEnergy: 1_000_000,
|
|
456
|
+
})
|
|
457
|
+
).toThrow('no gatherer reaches this stratum')
|
|
458
|
+
})
|
|
459
|
+
})
|
|
@@ -50,7 +50,7 @@ export function displayName(item: DisplayNameInput): string {
|
|
|
50
50
|
export interface DescribeOptions {
|
|
51
51
|
translate?: (key: string) => string
|
|
52
52
|
formatNumber?: (n: number) => string
|
|
53
|
-
formatMass?: (
|
|
53
|
+
formatMass?: (mass: number) => string
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
export function describeItem(resolved: ResolvedItem, opts?: DescribeOptions): string {
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type CargoInput,
|
|
7
7
|
hasIncomingCoupling,
|
|
8
8
|
hasSourceCoupling,
|
|
9
|
+
incomingSources,
|
|
9
10
|
type IncomingSource,
|
|
10
11
|
projectedCargoAvailableAt,
|
|
11
12
|
taskCargoEffect,
|
|
@@ -202,3 +203,115 @@ describe('clustercraft projection parity', () => {
|
|
|
202
203
|
expect(eff.added).toEqual([output])
|
|
203
204
|
})
|
|
204
205
|
})
|
|
206
|
+
|
|
207
|
+
function simpleTask(type: number) {
|
|
208
|
+
return ServerContract.Types.task.from({
|
|
209
|
+
type,
|
|
210
|
+
duration: 60,
|
|
211
|
+
cancelable: 0,
|
|
212
|
+
cargo: [cargoItem(401, 0, 100)],
|
|
213
|
+
couplings: [],
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
describe('taskCargoEffect task coverage', () => {
|
|
218
|
+
test('a contribute removes its cargo', () => {
|
|
219
|
+
const effect = taskCargoEffect(simpleTask(TaskType.CONTRIBUTE))
|
|
220
|
+
expect(effect.removed).toHaveLength(1)
|
|
221
|
+
expect(effect.added).toHaveLength(0)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
test('a depot store removes its cargo', () => {
|
|
225
|
+
const effect = taskCargoEffect(simpleTask(TaskType.DEPOT_STORE))
|
|
226
|
+
expect(effect.removed).toHaveLength(1)
|
|
227
|
+
expect(effect.added).toHaveLength(0)
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
test('a depot take adds its cargo', () => {
|
|
231
|
+
const effect = taskCargoEffect(simpleTask(TaskType.DEPOT_TAKE))
|
|
232
|
+
expect(effect.added).toHaveLength(1)
|
|
233
|
+
expect(effect.removed).toHaveLength(0)
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
describe('incomingSources', () => {
|
|
238
|
+
const RECEIVER = 1
|
|
239
|
+
const SENDER = 2
|
|
240
|
+
|
|
241
|
+
function receiver(holds: {id: number; kind: number; from: number; until: string}[]) {
|
|
242
|
+
const e = entity([])
|
|
243
|
+
return ServerContract.Types.entity_info.from({
|
|
244
|
+
...JSON.parse(JSON.stringify(e)),
|
|
245
|
+
id: RECEIVER,
|
|
246
|
+
holds: holds.map((h) => ({
|
|
247
|
+
id: h.id,
|
|
248
|
+
kind: h.kind,
|
|
249
|
+
counterpart: {entity_type: 'ship', entity_id: h.from},
|
|
250
|
+
until: h.until,
|
|
251
|
+
incoming_mass: 100,
|
|
252
|
+
})),
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function sender(tasks: ReturnType<typeof craftTask>[]) {
|
|
257
|
+
const e = entity([], [])
|
|
258
|
+
return ServerContract.Types.entity_info.from({
|
|
259
|
+
...JSON.parse(JSON.stringify(e)),
|
|
260
|
+
id: SENDER,
|
|
261
|
+
lanes: [{lane_key: 0, schedule: {started: T0, tasks}}],
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function coupledTo(hold: number, entityId: number, kind: number) {
|
|
266
|
+
return ServerContract.Types.coupling.from({
|
|
267
|
+
counterpart: {entity_type: 'ship', entity_id: entityId},
|
|
268
|
+
hold,
|
|
269
|
+
kind,
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
test('a coupled craft output resolves to the receiving hold', () => {
|
|
274
|
+
const to = receiver([{id: 1, kind: HoldKind.GATHER, from: SENDER, until: T0}])
|
|
275
|
+
const from = sender([
|
|
276
|
+
craftTask(
|
|
277
|
+
[cargoItem(7, 0, 4), cargoItem(9, 0, 1)],
|
|
278
|
+
[coupledTo(1, RECEIVER, HoldKind.GATHER)]
|
|
279
|
+
),
|
|
280
|
+
])
|
|
281
|
+
const sources = incomingSources(to, () => from)
|
|
282
|
+
expect(sources).toHaveLength(1)
|
|
283
|
+
// A craft delivers its output slot alone, never its inputs.
|
|
284
|
+
expect(sources[0].items).toHaveLength(1)
|
|
285
|
+
expect(sources[0].items[0].item_id.toNumber()).toBe(9)
|
|
286
|
+
expect(sources[0].holdId).toBe('1')
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
test('a hold whose counterpart is unknown yields nothing', () => {
|
|
290
|
+
const to = receiver([{id: 1, kind: HoldKind.GATHER, from: SENDER, until: T0}])
|
|
291
|
+
expect(incomingSources(to, () => undefined)).toHaveLength(0)
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
test('an excluded hold is skipped', () => {
|
|
295
|
+
const to = receiver([{id: 1, kind: HoldKind.GATHER, from: SENDER, until: T0}])
|
|
296
|
+
const from = sender([
|
|
297
|
+
craftTask([cargoItem(9, 0, 1)], [coupledTo(1, RECEIVER, HoldKind.GATHER)]),
|
|
298
|
+
])
|
|
299
|
+
expect(incomingSources(to, () => from, ['1'])).toHaveLength(0)
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
test('a coupling naming another hold does not fill this one', () => {
|
|
303
|
+
const to = receiver([{id: 1, kind: HoldKind.GATHER, from: SENDER, until: T0}])
|
|
304
|
+
const from = sender([
|
|
305
|
+
craftTask([cargoItem(9, 0, 1)], [coupledTo(2, RECEIVER, HoldKind.GATHER)]),
|
|
306
|
+
])
|
|
307
|
+
expect(incomingSources(to, () => from)).toHaveLength(0)
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
test('an outgoing hold kind is not an incoming source', () => {
|
|
311
|
+
const to = receiver([{id: 1, kind: HoldKind.PULL, from: SENDER, until: T0}])
|
|
312
|
+
const from = sender([
|
|
313
|
+
craftTask([cargoItem(9, 0, 1)], [coupledTo(1, RECEIVER, HoldKind.GATHER)]),
|
|
314
|
+
])
|
|
315
|
+
expect(incomingSources(to, () => from)).toHaveLength(0)
|
|
316
|
+
})
|
|
317
|
+
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {ServerContract} from '../contracts'
|
|
2
2
|
import {HoldKind, TaskType} from '../types'
|
|
3
3
|
import * as schedule from './schedule'
|
|
4
|
+
import {taskCargoRule} from './task-effects'
|
|
4
5
|
|
|
5
6
|
type Task = ServerContract.Types.task
|
|
6
7
|
type Coupling = ServerContract.Types.coupling
|
|
@@ -64,20 +65,59 @@ export function calcCounterpartDelivery(task: Task, coupling: Coupling): CargoIt
|
|
|
64
65
|
return task.cargo
|
|
65
66
|
}
|
|
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
|
+
|
|
67
109
|
export function taskCargoEffect(task: Task): CargoEffect {
|
|
68
|
-
switch (task.type.toNumber()) {
|
|
69
|
-
case
|
|
70
|
-
case
|
|
71
|
-
case TaskType.UNDEPLOY:
|
|
110
|
+
switch (taskCargoRule(task.type.toNumber())) {
|
|
111
|
+
case 'all-in':
|
|
112
|
+
case 'undeploy':
|
|
72
113
|
return {added: task.cargo, removed: []}
|
|
73
|
-
case
|
|
74
|
-
case TaskType.UPGRADE:
|
|
114
|
+
case 'all-out':
|
|
75
115
|
return {added: [], removed: task.cargo}
|
|
76
|
-
case
|
|
116
|
+
case 'gather':
|
|
77
117
|
return task.couplings.length > 0
|
|
78
118
|
? {added: [], removed: []}
|
|
79
119
|
: {added: task.cargo, removed: []}
|
|
80
|
-
case
|
|
120
|
+
case 'craft': {
|
|
81
121
|
if (task.cargo.length === 0) return {added: [], removed: []}
|
|
82
122
|
const {clustered, ownOutput} = craftCargoOwnership(task)
|
|
83
123
|
return {
|
package/src/scheduling/epoch.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface EpochInfo {
|
|
|
10
10
|
export function getCurrentEpoch(game: PlatformContract.Types.game_row): UInt64 {
|
|
11
11
|
const current = new Date().getTime()
|
|
12
12
|
const difference = (current - game.config.start.toMilliseconds()) / 1000
|
|
13
|
+
if (difference < 0) return UInt64.from(0)
|
|
13
14
|
const epoch = Math.floor(difference / Number(game.config.epochtime)) + 1
|
|
14
15
|
return UInt64.from(epoch)
|
|
15
16
|
}
|
package/src/scheduling/jobs.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
subtractFromStacks,
|
|
30
30
|
stackToCargoItem,
|
|
31
31
|
} from '../capabilities/storage'
|
|
32
|
+
import {hydrateEntityLanes} from '../entity/hydrate'
|
|
32
33
|
import {craftCargoOwnership} from './availability'
|
|
33
34
|
import * as schedule from './schedule'
|
|
34
35
|
import type {ScheduleData} from './schedule'
|
|
@@ -142,21 +143,6 @@ function recomputeCaps(entity: Projectable): ProjectedCaps | undefined {
|
|
|
142
143
|
output_pct: l.outputPct,
|
|
143
144
|
})
|
|
144
145
|
|
|
145
|
-
const toGathererLane = (l: {
|
|
146
|
-
slotIndex: number
|
|
147
|
-
yield: number
|
|
148
|
-
drain: number
|
|
149
|
-
depth: number
|
|
150
|
-
outputPct: number
|
|
151
|
-
}): ServerContract.Types.gatherer_lane =>
|
|
152
|
-
ServerContract.Types.gatherer_lane.from({
|
|
153
|
-
slot_index: l.slotIndex,
|
|
154
|
-
yield: l.yield,
|
|
155
|
-
drain: l.drain,
|
|
156
|
-
depth: l.depth,
|
|
157
|
-
output_pct: l.outputPct,
|
|
158
|
-
})
|
|
159
|
-
|
|
160
146
|
const toCrafterLane = (l: {
|
|
161
147
|
slotIndex: number
|
|
162
148
|
speed: number
|
|
@@ -191,7 +177,7 @@ function recomputeCaps(entity: Projectable): ProjectedCaps | undefined {
|
|
|
191
177
|
? ServerContract.Types.energy_stats.from(caps.generator)
|
|
192
178
|
: undefined,
|
|
193
179
|
loaderLanes: (caps.loaderLanes ?? []).map(toLoaderLane),
|
|
194
|
-
gathererLanes: (caps
|
|
180
|
+
gathererLanes: hydrateEntityLanes(caps),
|
|
195
181
|
crafterLanes: (caps.crafterLanes ?? []).map(toCrafterLane),
|
|
196
182
|
builderLanes: (caps.builderLanes ?? []).map(toBuilderLane),
|
|
197
183
|
hauler: caps.hauler
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {describe, expect, test} from 'bun:test'
|
|
2
2
|
import {ServerContract, TaskType} from '../index-module'
|
|
3
|
-
import {taskCargoChanges} from './task-cargo'
|
|
3
|
+
import {taskCargoChanges, taskCargoChangesChecked} from './task-cargo'
|
|
4
4
|
|
|
5
5
|
function cargoItem(itemId: number, quantity: number) {
|
|
6
6
|
return ServerContract.Types.cargo_item.from({item_id: itemId, stats: 0, modules: [], quantity})
|
|
@@ -42,3 +42,64 @@ describe('taskCargoChanges', () => {
|
|
|
42
42
|
])
|
|
43
43
|
})
|
|
44
44
|
})
|
|
45
|
+
|
|
46
|
+
describe('TaskType', () => {
|
|
47
|
+
test('covers the depot task types the contract defines', () => {
|
|
48
|
+
expect(TaskType.DEPOT_STORE).toBe(21)
|
|
49
|
+
expect(TaskType.DEPOT_TAKE).toBe(22)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
function simpleTask(type: number) {
|
|
54
|
+
return ServerContract.Types.task.from({
|
|
55
|
+
type,
|
|
56
|
+
duration: 60,
|
|
57
|
+
cancelable: 0,
|
|
58
|
+
cargo: [cargoItem(401, 100)],
|
|
59
|
+
couplings: [],
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe('taskCargoChanges task coverage', () => {
|
|
64
|
+
test('a depot store moves cargo out of the entity', () => {
|
|
65
|
+
const changes = taskCargoChanges(simpleTask(TaskType.DEPOT_STORE))
|
|
66
|
+
expect(changes).toHaveLength(1)
|
|
67
|
+
expect(changes[0].direction).toBe('out')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('a depot take moves cargo into the entity', () => {
|
|
71
|
+
const changes = taskCargoChanges(simpleTask(TaskType.DEPOT_TAKE))
|
|
72
|
+
expect(changes).toHaveLength(1)
|
|
73
|
+
expect(changes[0].direction).toBe('in')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('an upgrade consumes its cargo', () => {
|
|
77
|
+
const changes = taskCargoChanges(simpleTask(TaskType.UPGRADE))
|
|
78
|
+
expect(changes).toHaveLength(1)
|
|
79
|
+
expect(changes[0].direction).toBe('out')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('an undeploy returns cargo to the entity', () => {
|
|
83
|
+
const changes = taskCargoChanges(simpleTask(TaskType.UNDEPLOY))
|
|
84
|
+
expect(changes).toHaveLength(1)
|
|
85
|
+
expect(changes[0].direction).toBe('in')
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('taskCargoChangesChecked', () => {
|
|
90
|
+
test('reports a known task type with its changes', () => {
|
|
91
|
+
const result = taskCargoChangesChecked(simpleTask(TaskType.DEPOT_TAKE))
|
|
92
|
+
expect(result.known).toBe(true)
|
|
93
|
+
if (result.known) expect(result.changes[0].direction).toBe('in')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('distinguishes an unrecognised task type from one that moves nothing', () => {
|
|
97
|
+
const unknown = taskCargoChangesChecked(simpleTask(99))
|
|
98
|
+
expect(unknown.known).toBe(false)
|
|
99
|
+
if (!unknown.known) expect(unknown.taskType).toBe(99)
|
|
100
|
+
|
|
101
|
+
const travel = taskCargoChangesChecked(simpleTask(TaskType.TRAVEL))
|
|
102
|
+
expect(travel.known).toBe(true)
|
|
103
|
+
if (travel.known) expect(travel.changes).toHaveLength(0)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type {ServerContract} from '../contracts'
|
|
2
|
-
import {
|
|
2
|
+
import {type TaskCargoRule, taskCargoRule} from './task-effects'
|
|
3
3
|
|
|
4
4
|
export type TaskCargoDirection = 'in' | 'out'
|
|
5
5
|
|
|
@@ -11,6 +11,10 @@ export interface TaskCargoChange {
|
|
|
11
11
|
quantity: number
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export type TaskCargoResult =
|
|
15
|
+
| {known: true; changes: TaskCargoChange[]}
|
|
16
|
+
| {known: false; taskType: number}
|
|
17
|
+
|
|
14
18
|
function toChange(
|
|
15
19
|
item: ServerContract.Types.cargo_item,
|
|
16
20
|
direction: TaskCargoDirection
|
|
@@ -24,19 +28,18 @@ function toChange(
|
|
|
24
28
|
}
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
function changesForRule(rule: TaskCargoRule, task: ServerContract.Types.task): TaskCargoChange[] {
|
|
28
32
|
const items = task.cargo ?? []
|
|
29
33
|
if (items.length === 0) return []
|
|
30
|
-
switch (
|
|
31
|
-
case
|
|
32
|
-
case
|
|
34
|
+
switch (rule) {
|
|
35
|
+
case 'all-in':
|
|
36
|
+
case 'undeploy':
|
|
33
37
|
return items.map((i) => toChange(i, 'in'))
|
|
34
|
-
case
|
|
35
|
-
return task.couplings.length > 0 ? [] : items.map((i) => toChange(i, 'in'))
|
|
36
|
-
case TaskType.UNLOAD:
|
|
37
|
-
case TaskType.CONTRIBUTE:
|
|
38
|
+
case 'all-out':
|
|
38
39
|
return items.map((i) => toChange(i, 'out'))
|
|
39
|
-
case
|
|
40
|
+
case 'gather':
|
|
41
|
+
return task.couplings.length > 0 ? [] : items.map((i) => toChange(i, 'in'))
|
|
42
|
+
case 'craft':
|
|
40
43
|
return [
|
|
41
44
|
...items.slice(0, -1).map((i) => toChange(i, 'out')),
|
|
42
45
|
...(task.couplings.length > 0 ? [] : [toChange(items[items.length - 1], 'in')]),
|
|
@@ -45,3 +48,15 @@ export function taskCargoChanges(task: ServerContract.Types.task): TaskCargoChan
|
|
|
45
48
|
return []
|
|
46
49
|
}
|
|
47
50
|
}
|
|
51
|
+
|
|
52
|
+
export function taskCargoChanges(task: ServerContract.Types.task): TaskCargoChange[] {
|
|
53
|
+
const rule = taskCargoRule(Number(task.type))
|
|
54
|
+
return rule ? changesForRule(rule, task) : []
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function taskCargoChangesChecked(task: ServerContract.Types.task): TaskCargoResult {
|
|
58
|
+
const type = Number(task.type)
|
|
59
|
+
const rule = taskCargoRule(type)
|
|
60
|
+
if (!rule) return {known: false, taskType: type}
|
|
61
|
+
return {known: true, changes: changesForRule(rule, task)}
|
|
62
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {describe, expect, test} from 'bun:test'
|
|
2
|
+
import {TaskType} from '../types'
|
|
3
|
+
import {taskCargoRule} from './task-effects'
|
|
4
|
+
|
|
5
|
+
describe('taskCargoRule', () => {
|
|
6
|
+
test('maps the task types that move cargo out of the entity', () => {
|
|
7
|
+
expect(taskCargoRule(TaskType.UNLOAD)).toBe('all-out')
|
|
8
|
+
expect(taskCargoRule(TaskType.UPGRADE)).toBe('all-out')
|
|
9
|
+
expect(taskCargoRule(TaskType.CONTRIBUTE)).toBe('all-out')
|
|
10
|
+
expect(taskCargoRule(TaskType.DEPOT_STORE)).toBe('all-out')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('maps the task types that move cargo into the entity', () => {
|
|
14
|
+
expect(taskCargoRule(TaskType.LOAD)).toBe('all-in')
|
|
15
|
+
expect(taskCargoRule(TaskType.UNWRAP)).toBe('all-in')
|
|
16
|
+
expect(taskCargoRule(TaskType.DEPOT_TAKE)).toBe('all-in')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('maps the conditional task types to their own rules', () => {
|
|
20
|
+
expect(taskCargoRule(TaskType.GATHER)).toBe('gather')
|
|
21
|
+
expect(taskCargoRule(TaskType.CRAFT)).toBe('craft')
|
|
22
|
+
expect(taskCargoRule(TaskType.UNDEPLOY)).toBe('undeploy')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('maps the task types that move no cargo', () => {
|
|
26
|
+
expect(taskCargoRule(TaskType.TRAVEL)).toBe('none')
|
|
27
|
+
expect(taskCargoRule(TaskType.RECHARGE)).toBe('none')
|
|
28
|
+
expect(taskCargoRule(TaskType.DEMOLISH)).toBe('none')
|
|
29
|
+
expect(taskCargoRule(TaskType.BUILDPLOT)).toBe('none')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('returns undefined for a task type the table does not know', () => {
|
|
33
|
+
expect(taskCargoRule(99)).toBeUndefined()
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {TaskType} from '../types'
|
|
2
|
+
|
|
3
|
+
export type TaskCargoRule = 'none' | 'all-in' | 'all-out' | 'gather' | 'craft' | 'undeploy'
|
|
4
|
+
|
|
5
|
+
// Transcribed from the switch in contracts/src/server/src/projection.cpp.
|
|
6
|
+
export const TASK_CARGO_EFFECTS: ReadonlyMap<number, TaskCargoRule> = new Map<
|
|
7
|
+
number,
|
|
8
|
+
TaskCargoRule
|
|
9
|
+
>([
|
|
10
|
+
[TaskType.IDLE, 'none'],
|
|
11
|
+
[TaskType.TRAVEL, 'none'],
|
|
12
|
+
[TaskType.RECHARGE, 'none'],
|
|
13
|
+
[TaskType.WARP, 'none'],
|
|
14
|
+
[TaskType.TRANSIT, 'none'],
|
|
15
|
+
[TaskType.LAUNCH, 'none'],
|
|
16
|
+
[TaskType.DEMOLISH, 'none'],
|
|
17
|
+
[TaskType.BUILDPLOT, 'none'],
|
|
18
|
+
[TaskType.CHARGE, 'none'],
|
|
19
|
+
[TaskType.SHUTTLE, 'none'],
|
|
20
|
+
[TaskType.LOAD, 'all-in'],
|
|
21
|
+
[TaskType.UNWRAP, 'all-in'],
|
|
22
|
+
[TaskType.DEPOT_TAKE, 'all-in'],
|
|
23
|
+
[TaskType.UNLOAD, 'all-out'],
|
|
24
|
+
[TaskType.UPGRADE, 'all-out'],
|
|
25
|
+
[TaskType.CONTRIBUTE, 'all-out'],
|
|
26
|
+
[TaskType.DEPOT_STORE, 'all-out'],
|
|
27
|
+
[TaskType.GATHER, 'gather'],
|
|
28
|
+
[TaskType.CRAFT, 'craft'],
|
|
29
|
+
[TaskType.UNDEPLOY, 'undeploy'],
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
export function taskCargoRule(taskType: number): TaskCargoRule | undefined {
|
|
33
|
+
return TASK_CARGO_EFFECTS.get(taskType)
|
|
34
|
+
}
|
|
@@ -20,9 +20,9 @@ describe('unwrap duration mirror', () => {
|
|
|
20
20
|
})
|
|
21
21
|
|
|
22
22
|
test('transit floors distance then flight time', () => {
|
|
23
|
-
// distance = floor(sqrt(3^2+4^2)*10000)=50000; accel=400/mass*10000; flight=floor(2*sqrt(d/accel))
|
|
23
|
+
// distance = floor(sqrt(3^2+4^2)*10000)=50000; accel=400/(mass*100)*10000; flight=floor(2*sqrt(d/accel))
|
|
24
24
|
const mass = 1000
|
|
25
|
-
const accel = (400 / mass) * 10000
|
|
25
|
+
const accel = (400 / (mass * 100)) * 10000
|
|
26
26
|
const expected = Math.floor(2 * Math.sqrt(50000 / accel))
|
|
27
27
|
expect(unwrapTransitDuration(mass, {x: 0, y: 0}, {x: 3, y: 4})).toBe(expected)
|
|
28
28
|
})
|
|
@@ -30,7 +30,7 @@ describe('unwrap duration mirror', () => {
|
|
|
30
30
|
test('load uses altitude z, adds loader mass, divides by quantity', () => {
|
|
31
31
|
const loaders = {mass: 1200, thrust: 30, quantity: 2}
|
|
32
32
|
const itemMass = 800
|
|
33
|
-
const accel = (30 / (itemMass + 1200)) * 10000
|
|
33
|
+
const accel = (30 / ((itemMass + 1200) * 100)) * 10000
|
|
34
34
|
const flight = Math.floor(2 * Math.sqrt(3000 / accel))
|
|
35
35
|
expect(unwrapLoadDuration(loaders, itemMass, 3000)).toBe(Math.floor(flight / 2))
|
|
36
36
|
})
|
package/src/scheduling/unwrap.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type {UInt16Type, UInt32Type} from '@wharfkit/antelope'
|
|
2
2
|
import {calcCargoItemMass} from '../capabilities/storage'
|
|
3
3
|
import type {ServerContract} from '../contracts'
|
|
4
|
-
import {PRECISION} from '../types'
|
|
4
|
+
import {MASS_STAT_SCALE, PRECISION} from '../types'
|
|
5
5
|
import * as sched from './schedule'
|
|
6
6
|
import {taskCargoEffect} from './availability'
|
|
7
7
|
import {candidateLaneCompletesAt} from './lanes'
|
|
8
8
|
|
|
9
9
|
const NFT_TRANSIT_THRUST = 400
|
|
10
|
-
const BASELINE_LOADER: DerivedLoaders = {mass:
|
|
10
|
+
const BASELINE_LOADER: DerivedLoaders = {mass: 20, thrust: 1, quantity: 1}
|
|
11
11
|
// ground-level entities (warehouses, z=0) still incur a base orbital climb of load effort
|
|
12
12
|
const MIN_LOAD_Z = 800
|
|
13
13
|
|
|
@@ -37,7 +37,7 @@ export function derivedLoaders(
|
|
|
37
37
|
|
|
38
38
|
function acceleration(thrust: number, mass: number): number {
|
|
39
39
|
if (mass <= 0) return 0
|
|
40
|
-
return (thrust / mass) * PRECISION
|
|
40
|
+
return (thrust / (mass * MASS_STAT_SCALE)) * PRECISION
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
function flightTime(distance: number, accel: number): number {
|