@shipload/sdk 1.0.0-next.69 → 1.0.0-next.70
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 +102 -26
- package/lib/shipload.js +131 -69
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +124 -70
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +13 -9
- package/lib/testing.js +42 -28
- package/lib/testing.js.map +1 -1
- package/lib/testing.m.js +42 -28
- package/lib/testing.m.js.map +1 -1
- package/package.json +1 -1
- package/src/contracts/server.ts +67 -29
- package/src/index-module.ts +13 -0
- package/src/influence/duration.ts +29 -0
- package/src/influence/index.ts +2 -0
- package/src/influence/influence.test.ts +67 -1
- package/src/managers/actions.ts +0 -8
- package/src/managers/jobs.test.ts +20 -2
- package/src/managers/jobs.ts +5 -3
- package/src/receipts.ts +22 -0
- package/src/scheduling/availability.test.ts +2 -2
- package/src/scheduling/jobs.test.ts +25 -8
- package/src/scheduling/jobs.ts +34 -5
- package/src/scheduling/projection.ts +15 -24
- package/src/scheduling/schedule.test.ts +44 -1
- package/src/scheduling/schedule.ts +26 -0
- package/src/scheduling/task-cargo.test.ts +11 -6
- package/src/scheduling/task-effects.test.ts +2 -2
- package/src/scheduling/task-effects.ts +2 -2
- package/src/types.ts +4 -0
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
popcount5,
|
|
21
21
|
} from './demand'
|
|
22
22
|
import {findDecomp, DECOMP_REGISTRY} from './decomp'
|
|
23
|
-
import {depotTransferDuration, contributeDuration} from './duration'
|
|
23
|
+
import {civicDropoffDuration, depotTransferDuration, contributeDuration} from './duration'
|
|
24
24
|
import {getStatCount, statsSumSq} from './quality'
|
|
25
25
|
import {pricingFromWeights, valueCargoItem, valueContribution} from './valuation'
|
|
26
26
|
import {citizenryName, citizenryPatternCount} from './citizenry'
|
|
@@ -382,3 +382,69 @@ describe('depot transfer duration', () => {
|
|
|
382
382
|
expect(depotTransferDuration(params({cargoMass: 1}))).toBeGreaterThanOrEqual(1)
|
|
383
383
|
})
|
|
384
384
|
})
|
|
385
|
+
|
|
386
|
+
describe('civic drop-off duration', () => {
|
|
387
|
+
type ModuleEntry = ServerContract.Types.module_entry
|
|
388
|
+
const DEPOT_ITEM = 10219
|
|
389
|
+
const WORKSHOP_ITEM = 10208
|
|
390
|
+
const LOADER_T1 = 10103
|
|
391
|
+
|
|
392
|
+
function makeModuleEntry(itemId: number, stats: bigint): ModuleEntry {
|
|
393
|
+
return {
|
|
394
|
+
type: UInt8.from(0),
|
|
395
|
+
installed: {item_id: UInt16.from(itemId), stats: UInt64.from(stats)},
|
|
396
|
+
} as unknown as ModuleEntry
|
|
397
|
+
}
|
|
398
|
+
function emptySlot(): ModuleEntry {
|
|
399
|
+
return {type: UInt8.from(0)} as unknown as ModuleEntry
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const withLoader = () => ({
|
|
403
|
+
buildingModules: [makeModuleEntry(LOADER_T1, packStats(500)), emptySlot()],
|
|
404
|
+
buildingItemId: DEPOT_ITEM,
|
|
405
|
+
buildingKind: 'depot',
|
|
406
|
+
buildingZ: 0,
|
|
407
|
+
shipKind: 'ship',
|
|
408
|
+
shipZ: 400,
|
|
409
|
+
cargoMass: 100,
|
|
410
|
+
})
|
|
411
|
+
const withoutLoader = () => ({
|
|
412
|
+
buildingModules: [] as ModuleEntry[],
|
|
413
|
+
buildingItemId: WORKSHOP_ITEM,
|
|
414
|
+
buildingKind: 'workshop',
|
|
415
|
+
buildingZ: 0,
|
|
416
|
+
shipKind: 'ship',
|
|
417
|
+
shipZ: 400,
|
|
418
|
+
cargoMass: 100,
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
test('a building with a slot-0 loader prices off that loader', () => {
|
|
422
|
+
const p = withLoader()
|
|
423
|
+
expect(civicDropoffDuration(p)).toBe(
|
|
424
|
+
depotTransferDuration({
|
|
425
|
+
depotModules: p.buildingModules,
|
|
426
|
+
depotItemId: p.buildingItemId,
|
|
427
|
+
depotKind: p.buildingKind,
|
|
428
|
+
depotZ: p.buildingZ,
|
|
429
|
+
shipKind: p.shipKind,
|
|
430
|
+
shipZ: p.shipZ,
|
|
431
|
+
cargoMass: p.cargoMass,
|
|
432
|
+
})
|
|
433
|
+
)
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
test('a building with no loader falls back to the civic loader', () => {
|
|
437
|
+
const p = withoutLoader()
|
|
438
|
+
expect(civicDropoffDuration(p)).toBe(contributeDuration(p.cargoMass, p.shipZ))
|
|
439
|
+
expect(civicDropoffDuration(p)).toBeGreaterThan(0)
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
test('grows with cargo mass on both paths', () => {
|
|
443
|
+
expect(civicDropoffDuration({...withLoader(), cargoMass: 10_000})).toBeGreaterThan(
|
|
444
|
+
civicDropoffDuration(withLoader())
|
|
445
|
+
)
|
|
446
|
+
expect(civicDropoffDuration({...withoutLoader(), cargoMass: 10_000})).toBeGreaterThan(
|
|
447
|
+
civicDropoffDuration(withoutLoader())
|
|
448
|
+
)
|
|
449
|
+
})
|
|
450
|
+
})
|
package/src/managers/actions.ts
CHANGED
|
@@ -562,14 +562,6 @@ export class ActionsManager extends BaseManager {
|
|
|
562
562
|
return this.server.action('claimcraft', params)
|
|
563
563
|
}
|
|
564
564
|
|
|
565
|
-
cancelcraft(jobId: UInt64Type, shipId: UInt64Type): Action {
|
|
566
|
-
const params: ServerContract.ActionParams.cancelcraft = {
|
|
567
|
-
job_id: UInt64.from(jobId),
|
|
568
|
-
ship_id: UInt64.from(shipId),
|
|
569
|
-
}
|
|
570
|
-
return this.server.action('cancelcraft', params)
|
|
571
|
-
}
|
|
572
|
-
|
|
573
565
|
buildjob(
|
|
574
566
|
targetId: UInt64Type,
|
|
575
567
|
dockId: UInt64Type,
|
|
@@ -15,7 +15,7 @@ const row = (over: Record<string, unknown> = {}) => ({
|
|
|
15
15
|
recipe_id: {toNumber: () => 10001},
|
|
16
16
|
quantity: {toNumber: () => 5},
|
|
17
17
|
energy_paid: {toNumber: () => 0},
|
|
18
|
-
cargo: [{item: '
|
|
18
|
+
cargo: [{item: 'out'}],
|
|
19
19
|
...over,
|
|
20
20
|
})
|
|
21
21
|
|
|
@@ -26,7 +26,7 @@ function managerWith(queryImpl: () => Promise<unknown[]>, allImpl: () => Promise
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
describe('JobsManager.getOwnedJobs', () => {
|
|
29
|
-
it('parses
|
|
29
|
+
it('parses a landed row: cargo is the output, no inputs, status derived', async () => {
|
|
30
30
|
const m = managerWith(
|
|
31
31
|
async () => [row()],
|
|
32
32
|
async () => []
|
|
@@ -36,6 +36,24 @@ describe('JobsManager.getOwnedJobs', () => {
|
|
|
36
36
|
expect(jobs[0]).toMatchObject({id: 7, building: 42, quantity: 5, status: 'ready'})
|
|
37
37
|
expect(jobs[0].coords).toEqual({x: 12, y: 34})
|
|
38
38
|
expect(jobs[0].output).toEqual({item: 'out'} as never)
|
|
39
|
+
expect(jobs[0].inputs).toEqual([] as never)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('reports a job In Line rather than ready, whatever its zero window says', async () => {
|
|
43
|
+
const inLine = row({
|
|
44
|
+
starts_at: {toDate: () => new Date(0)},
|
|
45
|
+
completes_at: {toDate: () => new Date(0)},
|
|
46
|
+
deposited: false,
|
|
47
|
+
cargo: [{item: 'in'}],
|
|
48
|
+
})
|
|
49
|
+
const m = managerWith(
|
|
50
|
+
async () => [inLine],
|
|
51
|
+
async () => []
|
|
52
|
+
)
|
|
53
|
+
const jobs = await m.getOwnedJobs(OWNER, {now: new Date('2026-07-26T11:30:00Z')})
|
|
54
|
+
expect(jobs[0].status).toBe('inline')
|
|
55
|
+
expect(jobs[0].deposited).toBe(false)
|
|
56
|
+
expect(jobs[0].output).toBeNull()
|
|
39
57
|
expect(jobs[0].inputs).toEqual([{item: 'in'}] as never)
|
|
40
58
|
})
|
|
41
59
|
|
package/src/managers/jobs.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {Name, type NameType} from '@wharfkit/antelope'
|
|
2
2
|
import {BaseManager} from './base'
|
|
3
|
-
import {jobStatus, splitJobCargo, type OwnedJob} from '../scheduling/jobs'
|
|
3
|
+
import {jobDeposited, jobStatus, splitJobCargo, type OwnedJob} from '../scheduling/jobs'
|
|
4
4
|
import type {ServerContract} from '../contracts'
|
|
5
5
|
|
|
6
6
|
type JobRow = ServerContract.Types.craftjob_row
|
|
@@ -32,7 +32,8 @@ export class JobsManager extends BaseManager {
|
|
|
32
32
|
private parseOwnedJob(r: JobRow, now: Date): OwnedJob {
|
|
33
33
|
const startsAt = r.starts_at.toDate()
|
|
34
34
|
const completesAt = r.completes_at.toDate()
|
|
35
|
-
const
|
|
35
|
+
const deposited = jobDeposited(r.deposited)
|
|
36
|
+
const {output, inputs} = splitJobCargo(r.cargo, deposited)
|
|
36
37
|
return {
|
|
37
38
|
id: r.id.toNumber(),
|
|
38
39
|
building: r.building.toNumber(),
|
|
@@ -43,7 +44,8 @@ export class JobsManager extends BaseManager {
|
|
|
43
44
|
completesAt,
|
|
44
45
|
recipeId: r.recipe_id.toNumber(),
|
|
45
46
|
quantity: r.quantity.toNumber(),
|
|
46
|
-
status: jobStatus({startsAt, completesAt}, now),
|
|
47
|
+
status: jobStatus({startsAt, completesAt, deposited}, now),
|
|
48
|
+
deposited,
|
|
47
49
|
output,
|
|
48
50
|
inputs,
|
|
49
51
|
outputStats: output?.stats === undefined ? undefined : BigInt(output.stats.toString()),
|
package/src/receipts.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {Serializer} from '@wharfkit/antelope'
|
|
2
|
+
import {ServerContract} from './contracts'
|
|
3
|
+
|
|
4
|
+
export type BookingReceipt = ServerContract.Types.booking_receipt
|
|
5
|
+
|
|
6
|
+
export function decodeBookingReceipt(data: unknown): BookingReceipt {
|
|
7
|
+
if (data === null || data === undefined) {
|
|
8
|
+
throw new Error('decodeBookingReceipt: no return value')
|
|
9
|
+
}
|
|
10
|
+
if (data instanceof Uint8Array || typeof data === 'string') {
|
|
11
|
+
return Serializer.decode({data, type: ServerContract.Types.booking_receipt})
|
|
12
|
+
}
|
|
13
|
+
return ServerContract.Types.booking_receipt.from(data as ServerContract.Types.booking_receipt)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function bookingReceiptLandsAtMs(receipt: BookingReceipt): number {
|
|
17
|
+
return receipt.lands_at.toMilliseconds()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function bookingReceiptRuntimeSeconds(receipt: BookingReceipt): number {
|
|
21
|
+
return Number(receipt.runtime)
|
|
22
|
+
}
|
|
@@ -223,13 +223,13 @@ describe('taskCargoEffect task coverage', () => {
|
|
|
223
223
|
})
|
|
224
224
|
|
|
225
225
|
test('a depot store removes its cargo', () => {
|
|
226
|
-
const effect = taskCargoEffect(simpleTask(TaskType.
|
|
226
|
+
const effect = taskCargoEffect(simpleTask(TaskType.CIVIC_DEPOSIT))
|
|
227
227
|
expect(effect.removed).toHaveLength(1)
|
|
228
228
|
expect(effect.added).toHaveLength(0)
|
|
229
229
|
})
|
|
230
230
|
|
|
231
231
|
test('a depot take adds its cargo', () => {
|
|
232
|
-
const effect = taskCargoEffect(simpleTask(TaskType.
|
|
232
|
+
const effect = taskCargoEffect(simpleTask(TaskType.CIVIC_WITHDRAW))
|
|
233
233
|
expect(effect.added).toHaveLength(1)
|
|
234
234
|
expect(effect.removed).toHaveLength(0)
|
|
235
235
|
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {describe, expect, it} from 'bun:test'
|
|
2
|
-
import {jobStatus, splitJobCargo} from './jobs'
|
|
2
|
+
import {jobStatus, jobStatusLabel, splitJobCargo} from './jobs'
|
|
3
3
|
|
|
4
4
|
const at = (s: string) => new Date(s)
|
|
5
5
|
|
|
@@ -15,18 +15,35 @@ describe('jobStatus', () => {
|
|
|
15
15
|
expect(jobStatus(job, at('2026-07-26T11:00:00Z'))).toBe('ready')
|
|
16
16
|
expect(jobStatus(job, at('2026-07-26T12:00:00Z'))).toBe('ready')
|
|
17
17
|
})
|
|
18
|
+
it('is inline while the drop-off is still in the air', () => {
|
|
19
|
+
const booked = {
|
|
20
|
+
startsAt: at('1970-01-01T00:00:00Z'),
|
|
21
|
+
completesAt: at('1970-01-01T00:00:00Z'),
|
|
22
|
+
}
|
|
23
|
+
expect(jobStatus({...booked, deposited: false}, at('2026-07-26T10:00:00Z'))).toBe('inline')
|
|
24
|
+
expect(jobStatus({...job, deposited: true}, at('2026-07-26T10:30:00Z'))).toBe('crafting')
|
|
25
|
+
})
|
|
26
|
+
it('labels every state', () => {
|
|
27
|
+
expect(jobStatusLabel('inline')).toBe('In Line')
|
|
28
|
+
expect(jobStatusLabel('ready')).toBe('Ready for Pickup')
|
|
29
|
+
})
|
|
18
30
|
})
|
|
19
31
|
|
|
20
32
|
describe('splitJobCargo', () => {
|
|
21
|
-
it('
|
|
22
|
-
const cargo = [{n: 'a'}, {n: 'b'}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const {output, inputs} = splitJobCargo(cargo)
|
|
26
|
-
expect(output).toEqual({n: 'out'} as never)
|
|
33
|
+
it('reads an In Line row as inputs with no output', () => {
|
|
34
|
+
const cargo = [{n: 'a'}, {n: 'b'}] as unknown as Parameters<typeof splitJobCargo>[0]
|
|
35
|
+
const {output, inputs} = splitJobCargo(cargo, false)
|
|
36
|
+
expect(output).toBeNull()
|
|
27
37
|
expect(inputs).toEqual([{n: 'a'}, {n: 'b'}] as never)
|
|
28
38
|
})
|
|
39
|
+
it('reads a landed row as the output with no inputs', () => {
|
|
40
|
+
const cargo = [{n: 'out'}] as unknown as Parameters<typeof splitJobCargo>[0]
|
|
41
|
+
const {output, inputs} = splitJobCargo(cargo, true)
|
|
42
|
+
expect(output).toEqual({n: 'out'} as never)
|
|
43
|
+
expect(inputs).toEqual([] as never)
|
|
44
|
+
})
|
|
29
45
|
it('returns null output for empty cargo', () => {
|
|
30
|
-
expect(splitJobCargo([])).toEqual({output: null, inputs: []})
|
|
46
|
+
expect(splitJobCargo([], true)).toEqual({output: null, inputs: []})
|
|
47
|
+
expect(splitJobCargo([], false)).toEqual({output: null, inputs: []})
|
|
31
48
|
})
|
|
32
49
|
})
|
package/src/scheduling/jobs.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface JobWindow {
|
|
|
10
10
|
completesAt: Date
|
|
11
11
|
recipeId: number
|
|
12
12
|
quantity: number
|
|
13
|
+
/** False while the job is In Line: booked, with its inputs still in transit to the building. */
|
|
14
|
+
deposited: boolean
|
|
13
15
|
/** Packed stat roll of the job's output, when the source carried the job's cargo. */
|
|
14
16
|
outputStats?: bigint
|
|
15
17
|
}
|
|
@@ -76,18 +78,44 @@ export function pickFabricator(
|
|
|
76
78
|
return best
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
export type JobStatus = 'waiting' | 'crafting' | 'ready'
|
|
81
|
+
export type JobStatus = 'inline' | 'waiting' | 'crafting' | 'ready'
|
|
80
82
|
|
|
81
|
-
|
|
83
|
+
// A job In Line has no window yet, so the window alone would read it as long since ready.
|
|
84
|
+
export function jobStatus(
|
|
85
|
+
job: {startsAt: Date; completesAt: Date; deposited?: boolean},
|
|
86
|
+
now: Date
|
|
87
|
+
): JobStatus {
|
|
88
|
+
if (job.deposited === false) return 'inline'
|
|
82
89
|
if (now < job.startsAt) return 'waiting'
|
|
83
90
|
if (now < job.completesAt) return 'crafting'
|
|
84
91
|
return 'ready'
|
|
85
92
|
}
|
|
86
93
|
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
const JOB_STATUS_LABELS: Record<JobStatus, string> = {
|
|
95
|
+
inline: 'In Line',
|
|
96
|
+
waiting: 'Waiting',
|
|
97
|
+
crafting: 'Crafting',
|
|
98
|
+
ready: 'Ready for Pickup',
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function jobStatusLabel(status: JobStatus): string {
|
|
102
|
+
return JOB_STATUS_LABELS[status]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// A row written before the deposited flag existed reads as undefined; those jobs were all deposited.
|
|
106
|
+
export function jobDeposited(value: unknown): boolean {
|
|
107
|
+
if (value === undefined || value === null) return true
|
|
108
|
+
return Boolean(value)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// A job row holds the inputs while In Line and the output once its drop-off has landed.
|
|
112
|
+
export function splitJobCargo<T>(
|
|
113
|
+
cargo: readonly T[],
|
|
114
|
+
deposited: boolean
|
|
115
|
+
): {output: T | null; inputs: T[]} {
|
|
89
116
|
if (cargo.length === 0) return {output: null, inputs: []}
|
|
90
|
-
return {output:
|
|
117
|
+
if (!deposited) return {output: null, inputs: [...cargo]}
|
|
118
|
+
return {output: cargo[0], inputs: []}
|
|
91
119
|
}
|
|
92
120
|
|
|
93
121
|
export interface OwnedJob {
|
|
@@ -101,6 +129,7 @@ export interface OwnedJob {
|
|
|
101
129
|
recipeId: number
|
|
102
130
|
quantity: number
|
|
103
131
|
status: JobStatus
|
|
132
|
+
deposited: boolean
|
|
104
133
|
output: CargoItem | null
|
|
105
134
|
inputs: CargoItem[]
|
|
106
135
|
/** Packed stat roll of `output`, so every job shape answers this the same way. */
|
|
@@ -405,9 +405,19 @@ function applyTask(projected: ProjectedEntity, task: ServerContract.Types.task):
|
|
|
405
405
|
break
|
|
406
406
|
case TaskType.LOAD:
|
|
407
407
|
case TaskType.UNWRAP:
|
|
408
|
+
case TaskType.CIVIC_WITHDRAW:
|
|
408
409
|
applyAddCargoTask(projected, task)
|
|
409
410
|
break
|
|
410
411
|
case TaskType.UNLOAD:
|
|
412
|
+
case TaskType.CONTRIBUTE:
|
|
413
|
+
applyRemoveCargoTask(projected, task)
|
|
414
|
+
break
|
|
415
|
+
case TaskType.CIVIC_DEPOSIT:
|
|
416
|
+
applyEnergyCost(projected, task)
|
|
417
|
+
applyRemoveCargoTask(projected, task)
|
|
418
|
+
break
|
|
419
|
+
case TaskType.UPGRADE:
|
|
420
|
+
applyEnergyCost(projected, task)
|
|
411
421
|
applyRemoveCargoTask(projected, task)
|
|
412
422
|
break
|
|
413
423
|
case TaskType.GATHER:
|
|
@@ -417,6 +427,8 @@ function applyTask(projected: ProjectedEntity, task: ServerContract.Types.task):
|
|
|
417
427
|
applyCraftTask(projected, task)
|
|
418
428
|
break
|
|
419
429
|
case TaskType.UNDEPLOY:
|
|
430
|
+
applyAddCargoTask(projected, task)
|
|
431
|
+
break
|
|
420
432
|
case TaskType.DEMOLISH:
|
|
421
433
|
break
|
|
422
434
|
}
|
|
@@ -426,16 +438,8 @@ export interface ProjectionOptions {
|
|
|
426
438
|
upToTaskIndex?: number
|
|
427
439
|
}
|
|
428
440
|
|
|
429
|
-
// Mirrors the contract's completed_task_count_at: inclusive at the anchor.
|
|
430
441
|
function anchoredSkipCount(entity: Projectable, ordered: readonly schedule.OrderedTask[]): number {
|
|
431
|
-
|
|
432
|
-
if (anchor === undefined) return 0
|
|
433
|
-
const anchorMs = Number(anchor.toMilliseconds())
|
|
434
|
-
let count = 0
|
|
435
|
-
for (const {completesAt} of ordered) {
|
|
436
|
-
if (completesAt.getTime() <= anchorMs) count++
|
|
437
|
-
}
|
|
438
|
-
return count
|
|
442
|
+
return schedule.appliedTaskCount(entity, ordered)
|
|
439
443
|
}
|
|
440
444
|
|
|
441
445
|
export function projectEntity(entity: Projectable, options?: ProjectionOptions): ProjectedEntity {
|
|
@@ -573,21 +577,8 @@ export function projectEntityAt(entity: Projectable, now: Date): ProjectedEntity
|
|
|
573
577
|
case TaskType.TRANSIT:
|
|
574
578
|
applyFlightTask(projected, task, {complete: taskComplete, progress})
|
|
575
579
|
break
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
if (taskComplete) applyAddCargoTask(projected, task)
|
|
579
|
-
break
|
|
580
|
-
case TaskType.UNLOAD:
|
|
581
|
-
if (taskComplete) applyRemoveCargoTask(projected, task)
|
|
582
|
-
break
|
|
583
|
-
case TaskType.GATHER:
|
|
584
|
-
if (taskComplete) applyGatherTask(projected, task, {complete: true})
|
|
585
|
-
break
|
|
586
|
-
case TaskType.CRAFT:
|
|
587
|
-
if (taskComplete) applyCraftTask(projected, task)
|
|
588
|
-
break
|
|
589
|
-
case TaskType.UNDEPLOY:
|
|
590
|
-
case TaskType.DEMOLISH:
|
|
580
|
+
default:
|
|
581
|
+
if (taskComplete) applyTask(projected, task)
|
|
591
582
|
break
|
|
592
583
|
}
|
|
593
584
|
}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import {describe, expect, test} from 'bun:test'
|
|
2
2
|
import {ServerContract, TaskType} from '../index-module'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
appliedTaskCount,
|
|
5
|
+
hasPendingCapper,
|
|
6
|
+
hasResolvable,
|
|
7
|
+
isCapperTaskType,
|
|
8
|
+
orderedTasks,
|
|
9
|
+
unappliedTasks,
|
|
10
|
+
} from './schedule'
|
|
4
11
|
|
|
5
12
|
const T0 = '2026-06-19T00:00:00'
|
|
6
13
|
const NOW = new Date('2026-06-19T00:01:00.000Z')
|
|
@@ -118,3 +125,39 @@ describe('hasResolvable — capper gating', () => {
|
|
|
118
125
|
expect(hasResolvable(e, NOW)).toBe(false)
|
|
119
126
|
})
|
|
120
127
|
})
|
|
128
|
+
|
|
129
|
+
function anchoredEntity(anchorISO: string) {
|
|
130
|
+
const e = entity([
|
|
131
|
+
task({type: TaskType.TRAVEL, duration: 30}),
|
|
132
|
+
task({type: TaskType.TRAVEL, duration: 30}),
|
|
133
|
+
task({type: TaskType.TRAVEL, duration: 30}),
|
|
134
|
+
])
|
|
135
|
+
return ServerContract.Types.entity_info.from({
|
|
136
|
+
...ServerContract.Types.entity_info.from(e),
|
|
137
|
+
projected_at: anchorISO,
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
describe('appliedTaskCount / unappliedTasks', () => {
|
|
142
|
+
test('no anchor leaves every task unapplied', () => {
|
|
143
|
+
const e = entity([task({duration: 30}), task({duration: 30})])
|
|
144
|
+
expect(appliedTaskCount({lanes: e.lanes}, orderedTasks(e))).toBe(0)
|
|
145
|
+
expect(unappliedTasks({lanes: e.lanes})).toHaveLength(2)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('tasks completed at or before the anchor are already applied', () => {
|
|
149
|
+
const e = anchoredEntity('2026-06-19T00:01:00')
|
|
150
|
+
expect(appliedTaskCount(e, orderedTasks(e))).toBe(2)
|
|
151
|
+
expect(unappliedTasks(e)).toHaveLength(1)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('the anchor is inclusive, mirroring completed_task_count_at', () => {
|
|
155
|
+
const e = anchoredEntity('2026-06-19T00:00:30')
|
|
156
|
+
expect(appliedTaskCount(e, orderedTasks(e))).toBe(1)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('a whole schedule finished before the anchor leaves nothing unapplied', () => {
|
|
160
|
+
const e = anchoredEntity('2026-06-19T12:00:00')
|
|
161
|
+
expect(unappliedTasks(e)).toHaveLength(0)
|
|
162
|
+
})
|
|
163
|
+
})
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type {TimePoint} from '@wharfkit/antelope'
|
|
1
2
|
import type {ServerContract} from '../contracts'
|
|
2
3
|
import {TaskType} from '../types'
|
|
3
4
|
import * as core from './lane-core'
|
|
@@ -15,6 +16,10 @@ export interface ScheduleData {
|
|
|
15
16
|
holds?: Hold[]
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
export interface AnchoredScheduleData extends ScheduleData {
|
|
20
|
+
projected_at?: TimePoint
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
export interface LaneView {
|
|
19
24
|
laneKey: number
|
|
20
25
|
schedule: Schedule
|
|
@@ -240,6 +245,27 @@ export function orderedTasks(entity: ScheduleData): OrderedTask[] {
|
|
|
240
245
|
return out
|
|
241
246
|
}
|
|
242
247
|
|
|
248
|
+
// Mirrors the contract's completed_task_count_at: inclusive at the anchor.
|
|
249
|
+
export function appliedTaskCount(
|
|
250
|
+
entity: AnchoredScheduleData,
|
|
251
|
+
ordered: readonly OrderedTask[]
|
|
252
|
+
): number {
|
|
253
|
+
const anchor = entity.projected_at
|
|
254
|
+
if (anchor === undefined) return 0
|
|
255
|
+
const anchorMs = Number(anchor.toMilliseconds())
|
|
256
|
+
let count = 0
|
|
257
|
+
for (const {completesAt} of ordered) {
|
|
258
|
+
if (completesAt.getTime() <= anchorMs) count++
|
|
259
|
+
}
|
|
260
|
+
return count
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Tasks an entity_info snapshot has NOT yet folded into its cargo and state.
|
|
264
|
+
export function unappliedTasks(entity: AnchoredScheduleData): OrderedTask[] {
|
|
265
|
+
const ordered = orderedTasks(entity)
|
|
266
|
+
return ordered.slice(appliedTaskCount(entity, ordered))
|
|
267
|
+
}
|
|
268
|
+
|
|
243
269
|
export function laneRemainingOf(entity: ScheduleData, laneKey: number, now: Date): number {
|
|
244
270
|
const lane = getLane(entity, laneKey)
|
|
245
271
|
return lane ? core.laneRemaining(lane.schedule, now) : 0
|
|
@@ -44,9 +44,14 @@ describe('taskCargoChanges', () => {
|
|
|
44
44
|
})
|
|
45
45
|
|
|
46
46
|
describe('TaskType', () => {
|
|
47
|
-
test('covers the
|
|
48
|
-
expect(TaskType.
|
|
49
|
-
expect(TaskType.
|
|
47
|
+
test('covers the civic transfer task types the contract defines', () => {
|
|
48
|
+
expect(TaskType.CIVIC_DEPOSIT).toBe(21)
|
|
49
|
+
expect(TaskType.CIVIC_WITHDRAW).toBe(22)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('keeps the former depot names as aliases of the civic transfer types', () => {
|
|
53
|
+
expect(TaskType.DEPOT_STORE).toBe(TaskType.CIVIC_DEPOSIT)
|
|
54
|
+
expect(TaskType.DEPOT_TAKE).toBe(TaskType.CIVIC_WITHDRAW)
|
|
50
55
|
})
|
|
51
56
|
})
|
|
52
57
|
|
|
@@ -62,13 +67,13 @@ function simpleTask(type: number) {
|
|
|
62
67
|
|
|
63
68
|
describe('taskCargoChanges task coverage', () => {
|
|
64
69
|
test('a depot store moves cargo out of the entity', () => {
|
|
65
|
-
const changes = taskCargoChanges(simpleTask(TaskType.
|
|
70
|
+
const changes = taskCargoChanges(simpleTask(TaskType.CIVIC_DEPOSIT))
|
|
66
71
|
expect(changes).toHaveLength(1)
|
|
67
72
|
expect(changes[0].direction).toBe('out')
|
|
68
73
|
})
|
|
69
74
|
|
|
70
75
|
test('a depot take moves cargo into the entity', () => {
|
|
71
|
-
const changes = taskCargoChanges(simpleTask(TaskType.
|
|
76
|
+
const changes = taskCargoChanges(simpleTask(TaskType.CIVIC_WITHDRAW))
|
|
72
77
|
expect(changes).toHaveLength(1)
|
|
73
78
|
expect(changes[0].direction).toBe('in')
|
|
74
79
|
})
|
|
@@ -88,7 +93,7 @@ describe('taskCargoChanges task coverage', () => {
|
|
|
88
93
|
|
|
89
94
|
describe('taskCargoChangesChecked', () => {
|
|
90
95
|
test('reports a known task type with its changes', () => {
|
|
91
|
-
const result = taskCargoChangesChecked(simpleTask(TaskType.
|
|
96
|
+
const result = taskCargoChangesChecked(simpleTask(TaskType.CIVIC_WITHDRAW))
|
|
92
97
|
expect(result.known).toBe(true)
|
|
93
98
|
if (result.known) expect(result.changes[0].direction).toBe('in')
|
|
94
99
|
})
|
|
@@ -7,13 +7,13 @@ describe('taskCargoRule', () => {
|
|
|
7
7
|
expect(taskCargoRule(TaskType.UNLOAD)).toBe('all-out')
|
|
8
8
|
expect(taskCargoRule(TaskType.UPGRADE)).toBe('all-out')
|
|
9
9
|
expect(taskCargoRule(TaskType.CONTRIBUTE)).toBe('all-out')
|
|
10
|
-
expect(taskCargoRule(TaskType.
|
|
10
|
+
expect(taskCargoRule(TaskType.CIVIC_DEPOSIT)).toBe('all-out')
|
|
11
11
|
})
|
|
12
12
|
|
|
13
13
|
test('maps the task types that move cargo into the entity', () => {
|
|
14
14
|
expect(taskCargoRule(TaskType.LOAD)).toBe('all-in')
|
|
15
15
|
expect(taskCargoRule(TaskType.UNWRAP)).toBe('all-in')
|
|
16
|
-
expect(taskCargoRule(TaskType.
|
|
16
|
+
expect(taskCargoRule(TaskType.CIVIC_WITHDRAW)).toBe('all-in')
|
|
17
17
|
})
|
|
18
18
|
|
|
19
19
|
test('maps the conditional task types to their own rules', () => {
|
|
@@ -19,11 +19,11 @@ export const TASK_CARGO_EFFECTS: ReadonlyMap<number, TaskCargoRule> = new Map<
|
|
|
19
19
|
[TaskType.SHUTTLE, 'none'],
|
|
20
20
|
[TaskType.LOAD, 'all-in'],
|
|
21
21
|
[TaskType.UNWRAP, 'all-in'],
|
|
22
|
-
[TaskType.
|
|
22
|
+
[TaskType.CIVIC_WITHDRAW, 'all-in'],
|
|
23
23
|
[TaskType.UNLOAD, 'all-out'],
|
|
24
24
|
[TaskType.UPGRADE, 'all-out'],
|
|
25
25
|
[TaskType.CONTRIBUTE, 'all-out'],
|
|
26
|
-
[TaskType.
|
|
26
|
+
[TaskType.CIVIC_DEPOSIT, 'all-out'],
|
|
27
27
|
[TaskType.GATHER, 'gather'],
|
|
28
28
|
[TaskType.CRAFT, 'craft'],
|
|
29
29
|
[TaskType.UNDEPLOY, 'undeploy'],
|
package/src/types.ts
CHANGED
|
@@ -74,8 +74,12 @@ export enum TaskType {
|
|
|
74
74
|
SHUTTLE = 18,
|
|
75
75
|
REFIT = 19,
|
|
76
76
|
CONTRIBUTE = 20,
|
|
77
|
+
/** @deprecated Renamed to CIVIC_DEPOSIT. */
|
|
77
78
|
DEPOT_STORE = 21,
|
|
79
|
+
/** @deprecated Renamed to CIVIC_WITHDRAW. */
|
|
78
80
|
DEPOT_TAKE = 22,
|
|
81
|
+
CIVIC_DEPOSIT = 21,
|
|
82
|
+
CIVIC_WITHDRAW = 22,
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
export enum RefitOp {
|