@shipload/sdk 1.0.0-next.4 → 1.0.0-next.41
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 +34 -0
- package/lib/scan.js +136 -0
- package/lib/scan.js.map +1 -0
- package/lib/scan.m.js +129 -0
- package/lib/scan.m.js.map +1 -0
- package/lib/shipload.d.ts +2473 -973
- package/lib/shipload.js +11529 -5211
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +11338 -5162
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +970 -0
- package/lib/testing.js +4013 -0
- package/lib/testing.js.map +1 -0
- package/lib/testing.m.js +4007 -0
- package/lib/testing.m.js.map +1 -0
- package/package.json +20 -2
- package/src/capabilities/craftable.ts +51 -0
- package/src/capabilities/crafting.test.ts +7 -0
- package/src/capabilities/crafting.ts +5 -6
- package/src/capabilities/gathering.test.ts +16 -0
- package/src/capabilities/gathering.ts +35 -18
- package/src/capabilities/index.ts +0 -1
- package/src/capabilities/modules.ts +9 -0
- package/src/capabilities/storage.ts +16 -1
- package/src/contracts/platform.ts +231 -3
- package/src/contracts/server.ts +1021 -481
- 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 +12 -5
- package/src/data/capability-formulas.ts +14 -7
- package/src/data/catalog.ts +0 -5
- package/src/data/colors.ts +14 -47
- package/src/data/entities.json +76 -10
- package/src/data/item-ids.ts +18 -12
- package/src/data/items.json +321 -38
- package/src/data/kind-registry.json +109 -0
- package/src/data/kind-registry.ts +165 -0
- package/src/data/metadata.ts +119 -33
- package/src/data/recipes-runtime.ts +3 -23
- package/src/data/recipes.json +238 -117
- package/src/derivation/build-methods.ts +45 -0
- package/src/derivation/capabilities.test.ts +151 -0
- package/src/derivation/capabilities.ts +512 -0
- package/src/derivation/capability-mappings.ts +9 -12
- package/src/derivation/crafting.ts +23 -24
- package/src/derivation/index.ts +25 -2
- package/src/derivation/recipe-usage.test.ts +78 -0
- package/src/derivation/recipe-usage.ts +141 -0
- package/src/derivation/reserve-regen.ts +34 -0
- package/src/derivation/resources.ts +125 -38
- package/src/derivation/rollups.test.ts +55 -0
- package/src/derivation/rollups.ts +56 -0
- package/src/derivation/stars.test.ts +51 -0
- package/src/derivation/stars.ts +15 -0
- package/src/derivation/stats.ts +6 -6
- package/src/derivation/stratum.ts +17 -20
- package/src/derivation/tiers.ts +40 -7
- package/src/derivation/wormhole.ts +136 -0
- package/src/entities/entity.ts +98 -0
- package/src/entities/gamestate.ts +3 -28
- package/src/entities/makers.ts +124 -134
- 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 +267 -47
- package/src/managers/actions.ts +528 -95
- package/src/managers/base.ts +6 -2
- package/src/managers/construction-types.ts +80 -0
- package/src/managers/construction.ts +412 -0
- package/src/managers/context.ts +20 -1
- package/src/managers/coordinates.ts +14 -0
- package/src/managers/entities.ts +18 -66
- package/src/managers/epochs.ts +40 -0
- package/src/managers/index.ts +17 -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/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 +338 -0
- package/src/nft/description.ts +98 -24
- package/src/nft/index.ts +3 -0
- package/src/planner/index.ts +127 -0
- package/src/planner/planner.test.ts +319 -0
- package/src/resolution/describe-module.ts +18 -13
- package/src/resolution/display-name.ts +38 -10
- package/src/resolution/resolve-item.test.ts +37 -0
- package/src/resolution/resolve-item.ts +55 -24
- package/src/scan/index.ts +180 -0
- package/src/scan/scan-wasm.base64.ts +2 -0
- package/src/scheduling/accessor.ts +68 -22
- package/src/scheduling/availability.ts +108 -0
- package/src/scheduling/cancel.test.ts +348 -0
- package/src/scheduling/cancel.ts +209 -0
- package/src/scheduling/energy.ts +47 -0
- package/src/scheduling/idle-resolve.ts +45 -0
- package/src/scheduling/lane-core.ts +128 -0
- package/src/scheduling/lanes.test.ts +249 -0
- package/src/scheduling/lanes.ts +198 -0
- package/src/scheduling/projection.ts +209 -105
- package/src/scheduling/schedule.ts +241 -104
- package/src/scheduling/task-cargo.ts +46 -0
- package/src/shipload.ts +21 -1
- package/src/subscriptions/manager.ts +229 -142
- package/src/subscriptions/mappers.ts +5 -8
- package/src/subscriptions/types.ts +11 -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 +23 -0
- package/src/travel/route-planner.ts +196 -0
- package/src/travel/travel.ts +200 -112
- package/src/types/capabilities.ts +29 -6
- package/src/types/entity.ts +3 -3
- package/src/types/index.ts +0 -1
- package/src/types.ts +28 -13
- package/src/utils/cargo.ts +27 -0
- package/src/utils/display-name.ts +70 -0
- package/src/utils/system.ts +36 -24
- package/src/capabilities/loading.ts +0 -8
- package/src/entities/container.ts +0 -108
- package/src/entities/ship-deploy.ts +0 -259
- package/src/entities/ship.ts +0 -204
- package/src/entities/warehouse.ts +0 -119
- package/src/types/entity-traits.ts +0 -69
|
@@ -1,82 +1,128 @@
|
|
|
1
1
|
import type {ServerContract} from '../contracts'
|
|
2
2
|
import type {TaskType} from '../types'
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import * as core from './lane-core'
|
|
4
|
+
import {
|
|
5
|
+
activeTasks,
|
|
6
|
+
getLane,
|
|
7
|
+
getLanes,
|
|
8
|
+
hasSchedule,
|
|
9
|
+
isIdle,
|
|
10
|
+
LANE_MOBILITY,
|
|
11
|
+
type LaneView,
|
|
12
|
+
type ScheduleData,
|
|
13
|
+
} from './schedule'
|
|
5
14
|
|
|
6
15
|
type Task = ServerContract.Types.task
|
|
7
16
|
|
|
8
17
|
export class ScheduleAccessor {
|
|
9
|
-
|
|
18
|
+
private _laneResolved = false
|
|
19
|
+
private _lane: LaneView | undefined
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
private entity: ScheduleData,
|
|
23
|
+
private laneKey: number = LANE_MOBILITY
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
private get lane(): LaneView | undefined {
|
|
27
|
+
if (!this._laneResolved) {
|
|
28
|
+
this._lane = getLane(this.entity, this.laneKey)
|
|
29
|
+
this._laneResolved = true
|
|
30
|
+
}
|
|
31
|
+
return this._lane
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
forLane(laneKey: number): ScheduleAccessor {
|
|
35
|
+
return new ScheduleAccessor(this.entity, laneKey)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get lanes(): LaneView[] {
|
|
39
|
+
return getLanes(this.entity)
|
|
40
|
+
}
|
|
10
41
|
|
|
11
42
|
get hasSchedule(): boolean {
|
|
12
|
-
return
|
|
43
|
+
return hasSchedule(this.entity)
|
|
13
44
|
}
|
|
14
45
|
|
|
15
46
|
get isIdle(): boolean {
|
|
16
|
-
return
|
|
47
|
+
return isIdle(this.entity)
|
|
17
48
|
}
|
|
18
49
|
|
|
19
50
|
get tasks(): Task[] {
|
|
20
|
-
return schedule.
|
|
51
|
+
return this.lane?.schedule.tasks ?? []
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
activeTasks(now: Date): Task[] {
|
|
55
|
+
return activeTasks(this.entity, now)
|
|
21
56
|
}
|
|
22
57
|
|
|
23
58
|
duration(): number {
|
|
24
|
-
return
|
|
59
|
+
return this.lane ? core.laneDuration(this.lane.schedule) : 0
|
|
25
60
|
}
|
|
26
61
|
|
|
27
62
|
elapsed(now: Date): number {
|
|
28
|
-
return
|
|
63
|
+
return this.lane ? core.laneElapsed(this.lane.schedule, now) : 0
|
|
29
64
|
}
|
|
30
65
|
|
|
31
66
|
remaining(now: Date): number {
|
|
32
|
-
return
|
|
67
|
+
return this.lane ? core.laneRemaining(this.lane.schedule, now) : 0
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
startsIn(now: Date): number {
|
|
71
|
+
return this.lane ? core.laneStartsIn(this.lane.schedule, now) : 0
|
|
33
72
|
}
|
|
34
73
|
|
|
35
74
|
complete(now: Date): boolean {
|
|
36
|
-
return
|
|
75
|
+
return this.lane ? core.laneComplete(this.lane.schedule, now) : false
|
|
37
76
|
}
|
|
38
77
|
|
|
39
78
|
currentTaskIndex(now: Date): number {
|
|
40
|
-
return
|
|
79
|
+
return this.lane ? core.currentTaskIndexForLane(this.lane.schedule, now) : -1
|
|
41
80
|
}
|
|
42
81
|
|
|
43
82
|
currentTask(now: Date): Task | undefined {
|
|
44
|
-
return
|
|
83
|
+
return this.lane ? core.currentTask(this.lane.schedule, now) : undefined
|
|
45
84
|
}
|
|
46
85
|
|
|
47
86
|
currentTaskType(now: Date): TaskType | undefined {
|
|
48
|
-
return
|
|
87
|
+
return this.lane ? core.currentTaskType(this.lane.schedule, now) : undefined
|
|
49
88
|
}
|
|
50
89
|
|
|
51
90
|
taskStartTime(index: number): number {
|
|
52
|
-
return
|
|
91
|
+
return this.lane ? core.laneTaskStartTime(this.lane.schedule, index) : 0
|
|
53
92
|
}
|
|
54
93
|
|
|
55
94
|
taskElapsed(index: number, now: Date): number {
|
|
56
|
-
return
|
|
95
|
+
return this.lane ? core.laneTaskElapsed(this.lane.schedule, index, now) : 0
|
|
57
96
|
}
|
|
58
97
|
|
|
59
98
|
taskRemaining(index: number, now: Date): number {
|
|
60
|
-
return
|
|
99
|
+
return this.lane ? core.laneTaskRemaining(this.lane.schedule, index, now) : 0
|
|
61
100
|
}
|
|
62
101
|
|
|
63
102
|
taskComplete(index: number, now: Date): boolean {
|
|
64
|
-
return
|
|
103
|
+
return this.lane ? core.laneTaskComplete(this.lane.schedule, index, now) : false
|
|
65
104
|
}
|
|
66
105
|
|
|
67
106
|
taskInProgress(index: number, now: Date): boolean {
|
|
68
|
-
return
|
|
107
|
+
return this.lane ? core.laneTaskInProgress(this.lane.schedule, index, now) : false
|
|
69
108
|
}
|
|
70
109
|
|
|
71
110
|
currentTaskProgress(now: Date): number {
|
|
72
|
-
return
|
|
111
|
+
return this.lane ? core.currentTaskProgress(this.lane.schedule, now) : 0
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
currentTaskProgressFloat(now: Date): number {
|
|
115
|
+
return this.lane ? core.currentTaskProgressFloatForLane(this.lane.schedule, now) : 0
|
|
73
116
|
}
|
|
74
117
|
|
|
75
118
|
progress(now: Date): number {
|
|
76
|
-
return
|
|
119
|
+
return this.lane ? core.laneProgress(this.lane.schedule, now) : 0
|
|
77
120
|
}
|
|
78
121
|
}
|
|
79
122
|
|
|
80
|
-
export function createScheduleAccessor(
|
|
81
|
-
|
|
123
|
+
export function createScheduleAccessor(
|
|
124
|
+
entity: ScheduleData,
|
|
125
|
+
laneKey: number = LANE_MOBILITY
|
|
126
|
+
): ScheduleAccessor {
|
|
127
|
+
return new ScheduleAccessor(entity, laneKey)
|
|
82
128
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type {ServerContract} from '../contracts'
|
|
2
|
+
import {TaskType} from '../types'
|
|
3
|
+
import * as schedule from './schedule'
|
|
4
|
+
|
|
5
|
+
type Task = ServerContract.Types.task
|
|
6
|
+
type CargoItem = ServerContract.Types.cargo_item
|
|
7
|
+
|
|
8
|
+
export interface CargoEffect {
|
|
9
|
+
added: CargoItem[]
|
|
10
|
+
removed: CargoItem[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AvailabilityInput extends schedule.ScheduleData {
|
|
14
|
+
cargo: CargoItem[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function taskCargoEffect(task: Task): CargoEffect {
|
|
18
|
+
switch (task.type.toNumber()) {
|
|
19
|
+
case TaskType.LOAD:
|
|
20
|
+
case TaskType.UNWRAP:
|
|
21
|
+
case TaskType.UNDEPLOY:
|
|
22
|
+
return {added: task.cargo, removed: []}
|
|
23
|
+
case TaskType.UNLOAD:
|
|
24
|
+
return {added: [], removed: task.cargo}
|
|
25
|
+
case TaskType.GATHER:
|
|
26
|
+
return task.entitytarget ? {added: [], removed: []} : {added: task.cargo, removed: []}
|
|
27
|
+
case TaskType.CRAFT:
|
|
28
|
+
if (task.cargo.length === 0) return {added: [], removed: []}
|
|
29
|
+
return {added: [task.cargo[task.cargo.length - 1]], removed: task.cargo.slice(0, -1)}
|
|
30
|
+
case TaskType.DEPLOY:
|
|
31
|
+
return task.cargo.length > 0
|
|
32
|
+
? {added: [], removed: [task.cargo[0]]}
|
|
33
|
+
: {added: [], removed: []}
|
|
34
|
+
default:
|
|
35
|
+
return {added: [], removed: []}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function cargoKey(item: CargoItem): string {
|
|
40
|
+
const base = `${item.item_id.toNumber()}:${item.stats.toString()}`
|
|
41
|
+
const modules = item.modules ?? []
|
|
42
|
+
const entityId = item.entity_id?.toString()
|
|
43
|
+
const normalizedEntityId = entityId && entityId !== '0' ? entityId : ''
|
|
44
|
+
if (modules.length === 0 && normalizedEntityId === '') return base
|
|
45
|
+
return `${base}:modules=${JSON.stringify(modules)}:entity=${normalizedEntityId}`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function cargoQuantity(item: CargoItem): bigint {
|
|
49
|
+
return BigInt(item.quantity.toString())
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function projectedCargoAvailableAt(
|
|
53
|
+
entity: AvailabilityInput,
|
|
54
|
+
at: Date
|
|
55
|
+
): Map<string, bigint> {
|
|
56
|
+
const avail = new Map<string, bigint>()
|
|
57
|
+
|
|
58
|
+
for (const item of entity.cargo) {
|
|
59
|
+
const key = cargoKey(item)
|
|
60
|
+
avail.set(key, (avail.get(key) ?? 0n) + cargoQuantity(item))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Every scheduled task reserves inputs against the unsettled cargo base, even already-elapsed ones.
|
|
64
|
+
const tasks = schedule.orderedTasks(entity)
|
|
65
|
+
|
|
66
|
+
for (const ordered of tasks) {
|
|
67
|
+
if (ordered.completesAt.getTime() >= at.getTime()) continue
|
|
68
|
+
|
|
69
|
+
for (const item of taskCargoEffect(ordered.task).added) {
|
|
70
|
+
const key = cargoKey(item)
|
|
71
|
+
avail.set(key, (avail.get(key) ?? 0n) + cargoQuantity(item))
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const ordered of tasks) {
|
|
76
|
+
for (const item of taskCargoEffect(ordered.task).removed) {
|
|
77
|
+
const key = cargoKey(item)
|
|
78
|
+
const current = avail.get(key) ?? 0n
|
|
79
|
+
const quantity = cargoQuantity(item)
|
|
80
|
+
avail.set(key, current > quantity ? current - quantity : 0n)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return avail
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Latest completion among scheduled tasks producing any of the given inputs (a craft starts no earlier).
|
|
88
|
+
export function cargoReadyAt(entity: AvailabilityInput, inputItemIds: readonly number[]): Date {
|
|
89
|
+
let readyMs = 0
|
|
90
|
+
for (const ordered of schedule.orderedTasks(entity)) {
|
|
91
|
+
for (const item of taskCargoEffect(ordered.task).added) {
|
|
92
|
+
if (inputItemIds.includes(item.item_id.toNumber())) {
|
|
93
|
+
readyMs = Math.max(readyMs, ordered.completesAt.getTime())
|
|
94
|
+
break
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return new Date(readyMs)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function availableForItem(avail: Map<string, bigint>, itemId: number): bigint {
|
|
102
|
+
const prefix = `${itemId}:`
|
|
103
|
+
let total = 0n
|
|
104
|
+
for (const [key, quantity] of avail) {
|
|
105
|
+
if (key.startsWith(prefix)) total += quantity
|
|
106
|
+
}
|
|
107
|
+
return total
|
|
108
|
+
}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import {describe, expect, test} from 'bun:test'
|
|
2
|
+
import {ServerContract, TaskType, TaskCancelable} from '../index-module'
|
|
3
|
+
import {cancelEligibility, CancelBlockReason} from './cancel'
|
|
4
|
+
|
|
5
|
+
const T0 = '2026-06-19T00:00:00'
|
|
6
|
+
|
|
7
|
+
function task(over: Partial<{type: number; duration: number; cancelable: number; group: number}>) {
|
|
8
|
+
return ServerContract.Types.task.from({
|
|
9
|
+
type: over.type ?? TaskType.TRAVEL,
|
|
10
|
+
duration: over.duration ?? 100,
|
|
11
|
+
cancelable: over.cancelable ?? TaskCancelable.BEFORE_START,
|
|
12
|
+
cargo: [],
|
|
13
|
+
...(over.group ? {entitygroup: over.group} : {}),
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function entity(tasks: ReturnType<typeof task>[], startedISO = T0) {
|
|
18
|
+
return ServerContract.Types.entity_info.from({
|
|
19
|
+
type: 'ship',
|
|
20
|
+
id: 1,
|
|
21
|
+
owner: 'player.gm',
|
|
22
|
+
entity_name: 'Ship 1',
|
|
23
|
+
coordinates: {x: 0, y: 0, z: 0},
|
|
24
|
+
item_id: 1,
|
|
25
|
+
cargomass: 0,
|
|
26
|
+
cargo: [],
|
|
27
|
+
modules: [],
|
|
28
|
+
lanes: [{lane_key: 0, schedule: {started: startedISO, tasks}}],
|
|
29
|
+
gatherer_lanes: [],
|
|
30
|
+
crafter_lanes: [],
|
|
31
|
+
loader_lanes: [],
|
|
32
|
+
holds: [],
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('cancelEligibility — local gates', () => {
|
|
37
|
+
const now = new Date('2026-06-19T00:00:10.000Z') // 10s in: task 0 running, task 1 upcoming
|
|
38
|
+
|
|
39
|
+
test('cancelling the last upcoming task: ok, count 1', () => {
|
|
40
|
+
const e = entity([task({}), task({})])
|
|
41
|
+
const plan = cancelEligibility(e, 0, 1, {now})
|
|
42
|
+
expect(plan.ok).toBe(true)
|
|
43
|
+
expect(plan.range.count).toBe(1)
|
|
44
|
+
expect(plan.range.taskIndices).toEqual([1])
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('NEVER task is blocked', () => {
|
|
48
|
+
const e = entity([
|
|
49
|
+
task({}),
|
|
50
|
+
task({cancelable: TaskCancelable.NEVER, type: TaskType.GATHER}),
|
|
51
|
+
])
|
|
52
|
+
expect(cancelEligibility(e, 0, 1, {now}).blockedReason).toBe(CancelBlockReason.TASK_NEVER)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('BEFORE_START task that is running is blocked', () => {
|
|
56
|
+
const e = entity([task({cancelable: TaskCancelable.BEFORE_START, duration: 100})])
|
|
57
|
+
// task 0 is running at now=10s
|
|
58
|
+
expect(cancelEligibility(e, 0, 0, {now}).blockedReason).toBe(
|
|
59
|
+
CancelBlockReason.BEFORE_START_RUNNING
|
|
60
|
+
)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('done task is blocked', () => {
|
|
64
|
+
const e = entity([task({duration: 5})]) // completes at 5s, now=10s
|
|
65
|
+
expect(cancelEligibility(e, 0, 0, {now}).blockedReason).toBe(CancelBlockReason.DONE)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('unknown lane: count 0, ok false', () => {
|
|
69
|
+
const e = entity([task({})])
|
|
70
|
+
const plan = cancelEligibility(e, 9, 0, {now})
|
|
71
|
+
expect(plan.ok).toBe(false)
|
|
72
|
+
expect(plan.range.count).toBe(0)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('multi-task range: 4 tasks, fromIndex 1 yields count 3', () => {
|
|
76
|
+
const e = entity([
|
|
77
|
+
task({duration: 100}),
|
|
78
|
+
task({cancelable: TaskCancelable.ALWAYS, duration: 100}),
|
|
79
|
+
task({cancelable: TaskCancelable.ALWAYS, duration: 100}),
|
|
80
|
+
task({cancelable: TaskCancelable.ALWAYS, duration: 100}),
|
|
81
|
+
])
|
|
82
|
+
const plan = cancelEligibility(e, 0, 1, {now})
|
|
83
|
+
expect(plan.ok).toBe(true)
|
|
84
|
+
expect(plan.range.count).toBe(3)
|
|
85
|
+
expect(plan.range.taskIndices).toEqual([1, 2, 3])
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('cancelEligibility — linked tasks', () => {
|
|
90
|
+
const now = new Date('2026-06-19T00:00:10.000Z')
|
|
91
|
+
test('range containing a linked (entitygroup) task is blocked', () => {
|
|
92
|
+
const e = entity([task({}), task({group: 42}), task({})])
|
|
93
|
+
expect(cancelEligibility(e, 0, 0, {now}).blockedReason).toBe(
|
|
94
|
+
CancelBlockReason.CONTAINS_LINKED_TASK
|
|
95
|
+
)
|
|
96
|
+
})
|
|
97
|
+
test('task after the linked one cancels normally', () => {
|
|
98
|
+
const e = entity([task({}), task({group: 42}), task({})])
|
|
99
|
+
expect(cancelEligibility(e, 0, 2, {now}).ok).toBe(true)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const HOLD_PULL = 1
|
|
104
|
+
function loadTask(giverType: string, giverId: number, holdId: number, qty: number) {
|
|
105
|
+
return ServerContract.Types.task.from({
|
|
106
|
+
type: TaskType.LOAD,
|
|
107
|
+
duration: 100,
|
|
108
|
+
cancelable: TaskCancelable.ALWAYS,
|
|
109
|
+
cargo: [{item_id: 7, stats: 0, modules: [], quantity: qty}],
|
|
110
|
+
entitytarget: {entity_type: giverType, entity_id: giverId},
|
|
111
|
+
hold: holdId,
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
describe('cancelEligibility — effects', () => {
|
|
116
|
+
const now = new Date('2026-06-19T00:00:10.000Z')
|
|
117
|
+
|
|
118
|
+
test('abandonsRunning true when the front of range is a running ALWAYS task', () => {
|
|
119
|
+
const e = entity([
|
|
120
|
+
task({cancelable: TaskCancelable.ALWAYS, type: TaskType.RECHARGE, duration: 100}),
|
|
121
|
+
])
|
|
122
|
+
expect(cancelEligibility(e, 0, 0, {now}).effects.abandonsRunning).toBe(true)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('buildplot cancel reports keepsPlotDeposits', () => {
|
|
126
|
+
const e = entity([
|
|
127
|
+
ServerContract.Types.task.from({
|
|
128
|
+
type: TaskType.BUILDPLOT,
|
|
129
|
+
duration: 100,
|
|
130
|
+
cancelable: TaskCancelable.ALWAYS,
|
|
131
|
+
cargo: [],
|
|
132
|
+
entitytarget: {entity_type: 'plot', entity_id: 55},
|
|
133
|
+
}),
|
|
134
|
+
])
|
|
135
|
+
const plan = cancelEligibility(e, 0, 0, {now})
|
|
136
|
+
expect(plan.effects.keepsPlotDeposits?.plot.entity_id.toNumber()).toBe(55)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('PULL load cancel refunds cargo to the giver', () => {
|
|
140
|
+
const lt = loadTask('warehouse', 6, 1, 4)
|
|
141
|
+
const upcoming = new Date('2026-06-18T23:59:50.000Z')
|
|
142
|
+
const e = ServerContract.Types.entity_info.from({
|
|
143
|
+
type: 'ship',
|
|
144
|
+
id: 1,
|
|
145
|
+
owner: 'player.gm',
|
|
146
|
+
entity_name: 'Ship 1',
|
|
147
|
+
coordinates: {x: 0, y: 0, z: 0},
|
|
148
|
+
item_id: 1,
|
|
149
|
+
cargomass: 0,
|
|
150
|
+
cargo: [],
|
|
151
|
+
modules: [],
|
|
152
|
+
lanes: [{lane_key: 0, schedule: {started: T0, tasks: [lt]}}],
|
|
153
|
+
gatherer_lanes: [],
|
|
154
|
+
crafter_lanes: [],
|
|
155
|
+
loader_lanes: [],
|
|
156
|
+
holds: [
|
|
157
|
+
{
|
|
158
|
+
id: 1,
|
|
159
|
+
kind: HOLD_PULL,
|
|
160
|
+
counterpart: {entity_type: 'warehouse', entity_id: 6},
|
|
161
|
+
until: T0,
|
|
162
|
+
incoming_mass: 0,
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
})
|
|
166
|
+
const plan = cancelEligibility(e, 0, 0, {now: upcoming})
|
|
167
|
+
expect(plan.effects.refunds[0]?.giver.entity_id.toNumber()).toBe(6)
|
|
168
|
+
expect(plan.effects.refunds[0]?.cargo[0].quantity.toNumber()).toBe(4)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('PULL load cancel populates releasedHolds with kind and counterpart', () => {
|
|
172
|
+
const lt = loadTask('warehouse', 6, 1, 4)
|
|
173
|
+
const upcoming = new Date('2026-06-18T23:59:50.000Z')
|
|
174
|
+
const e = ServerContract.Types.entity_info.from({
|
|
175
|
+
type: 'ship',
|
|
176
|
+
id: 1,
|
|
177
|
+
owner: 'player.gm',
|
|
178
|
+
entity_name: 'Ship 1',
|
|
179
|
+
coordinates: {x: 0, y: 0, z: 0},
|
|
180
|
+
item_id: 1,
|
|
181
|
+
cargomass: 0,
|
|
182
|
+
cargo: [],
|
|
183
|
+
modules: [],
|
|
184
|
+
lanes: [{lane_key: 0, schedule: {started: T0, tasks: [lt]}}],
|
|
185
|
+
gatherer_lanes: [],
|
|
186
|
+
crafter_lanes: [],
|
|
187
|
+
loader_lanes: [],
|
|
188
|
+
holds: [
|
|
189
|
+
{
|
|
190
|
+
id: 1,
|
|
191
|
+
kind: HOLD_PULL,
|
|
192
|
+
counterpart: {entity_type: 'warehouse', entity_id: 6},
|
|
193
|
+
until: T0,
|
|
194
|
+
incoming_mass: 0,
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
})
|
|
198
|
+
const plan = cancelEligibility(e, 0, 0, {now: upcoming})
|
|
199
|
+
expect(plan.effects.releasedHolds[0]?.kind).toBe(1)
|
|
200
|
+
expect(plan.effects.releasedHolds[0]?.counterpart.entity_id.toNumber()).toBe(6)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test('unresolvable hold emits no releasedHolds entry', () => {
|
|
204
|
+
const lt = loadTask('warehouse', 6, 99, 4)
|
|
205
|
+
const upcoming = new Date('2026-06-18T23:59:50.000Z')
|
|
206
|
+
const e = ServerContract.Types.entity_info.from({
|
|
207
|
+
type: 'ship',
|
|
208
|
+
id: 1,
|
|
209
|
+
owner: 'player.gm',
|
|
210
|
+
entity_name: 'Ship 1',
|
|
211
|
+
coordinates: {x: 0, y: 0, z: 0},
|
|
212
|
+
item_id: 1,
|
|
213
|
+
cargomass: 0,
|
|
214
|
+
cargo: [],
|
|
215
|
+
modules: [],
|
|
216
|
+
lanes: [{lane_key: 0, schedule: {started: T0, tasks: [lt]}}],
|
|
217
|
+
gatherer_lanes: [],
|
|
218
|
+
crafter_lanes: [],
|
|
219
|
+
loader_lanes: [],
|
|
220
|
+
holds: [],
|
|
221
|
+
})
|
|
222
|
+
const plan = cancelEligibility(e, 0, 0, {now: upcoming})
|
|
223
|
+
expect(plan.effects.releasedHolds).toHaveLength(0)
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
describe('cancelEligibility — feasibility', () => {
|
|
228
|
+
const upcoming = new Date('2026-06-18T23:59:50.000Z')
|
|
229
|
+
|
|
230
|
+
test('WOULD_STRAND when cancelling a producer a later consumer needs', () => {
|
|
231
|
+
const producer = ServerContract.Types.task.from({
|
|
232
|
+
type: TaskType.LOAD,
|
|
233
|
+
duration: 50,
|
|
234
|
+
cancelable: TaskCancelable.ALWAYS,
|
|
235
|
+
cargo: [{item_id: 7, stats: 0, modules: [], quantity: 2}],
|
|
236
|
+
})
|
|
237
|
+
const consumer = ServerContract.Types.task.from({
|
|
238
|
+
type: TaskType.CRAFT,
|
|
239
|
+
duration: 50,
|
|
240
|
+
cancelable: TaskCancelable.ALWAYS,
|
|
241
|
+
cargo: [
|
|
242
|
+
{item_id: 7, stats: 0, modules: [], quantity: 2},
|
|
243
|
+
{item_id: 9, stats: 0, modules: [], quantity: 1},
|
|
244
|
+
],
|
|
245
|
+
})
|
|
246
|
+
const e = ServerContract.Types.entity_info.from({
|
|
247
|
+
type: 'ship',
|
|
248
|
+
id: 1,
|
|
249
|
+
owner: 'player.gm',
|
|
250
|
+
entity_name: 'S',
|
|
251
|
+
coordinates: {x: 0, y: 0, z: 0},
|
|
252
|
+
item_id: 1,
|
|
253
|
+
cargomass: 0,
|
|
254
|
+
cargo: [],
|
|
255
|
+
modules: [],
|
|
256
|
+
lanes: [
|
|
257
|
+
{
|
|
258
|
+
lane_key: 1,
|
|
259
|
+
schedule: {started: '2026-06-19T00:00:00', tasks: [producer]},
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
lane_key: 2,
|
|
263
|
+
schedule: {started: '2026-06-19T00:00:00', tasks: [consumer]},
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
gatherer_lanes: [],
|
|
267
|
+
crafter_lanes: [],
|
|
268
|
+
loader_lanes: [],
|
|
269
|
+
holds: [],
|
|
270
|
+
})
|
|
271
|
+
expect(cancelEligibility(e, 1, 0, {now: upcoming}).blockedReason).toBe(
|
|
272
|
+
CancelBlockReason.WOULD_STRAND
|
|
273
|
+
)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
test('benign cancel on independent lane does NOT strand', () => {
|
|
277
|
+
const producer = ServerContract.Types.task.from({
|
|
278
|
+
type: TaskType.LOAD,
|
|
279
|
+
duration: 50,
|
|
280
|
+
cancelable: TaskCancelable.ALWAYS,
|
|
281
|
+
cargo: [{item_id: 7, stats: 0, modules: [], quantity: 2}],
|
|
282
|
+
})
|
|
283
|
+
const independent = ServerContract.Types.task.from({
|
|
284
|
+
type: TaskType.TRAVEL,
|
|
285
|
+
duration: 50,
|
|
286
|
+
cancelable: TaskCancelable.ALWAYS,
|
|
287
|
+
cargo: [],
|
|
288
|
+
})
|
|
289
|
+
const e = ServerContract.Types.entity_info.from({
|
|
290
|
+
type: 'ship',
|
|
291
|
+
id: 1,
|
|
292
|
+
owner: 'player.gm',
|
|
293
|
+
entity_name: 'S',
|
|
294
|
+
coordinates: {x: 0, y: 0, z: 0},
|
|
295
|
+
item_id: 1,
|
|
296
|
+
cargomass: 0,
|
|
297
|
+
cargo: [],
|
|
298
|
+
modules: [],
|
|
299
|
+
lanes: [
|
|
300
|
+
{lane_key: 1, schedule: {started: '2026-06-19T00:00:00', tasks: [producer]}},
|
|
301
|
+
{lane_key: 2, schedule: {started: '2026-06-19T00:00:00', tasks: [independent]}},
|
|
302
|
+
],
|
|
303
|
+
gatherer_lanes: [],
|
|
304
|
+
crafter_lanes: [],
|
|
305
|
+
loader_lanes: [],
|
|
306
|
+
holds: [],
|
|
307
|
+
})
|
|
308
|
+
expect(cancelEligibility(e, 2, 0, {now: upcoming}).ok).toBe(true)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
test('WOULD_STRAND when cancelling producer of a MODULAR cargo the consumer needs', () => {
|
|
312
|
+
const moduledCargo = {item_id: 7, stats: 0, modules: [{type: 3}], quantity: 2}
|
|
313
|
+
const producer = ServerContract.Types.task.from({
|
|
314
|
+
type: TaskType.LOAD,
|
|
315
|
+
duration: 50,
|
|
316
|
+
cancelable: TaskCancelable.ALWAYS,
|
|
317
|
+
cargo: [moduledCargo],
|
|
318
|
+
})
|
|
319
|
+
const consumer = ServerContract.Types.task.from({
|
|
320
|
+
type: TaskType.CRAFT,
|
|
321
|
+
duration: 50,
|
|
322
|
+
cancelable: TaskCancelable.ALWAYS,
|
|
323
|
+
cargo: [moduledCargo, {item_id: 9, stats: 0, modules: [], quantity: 1}],
|
|
324
|
+
})
|
|
325
|
+
const e = ServerContract.Types.entity_info.from({
|
|
326
|
+
type: 'ship',
|
|
327
|
+
id: 1,
|
|
328
|
+
owner: 'player.gm',
|
|
329
|
+
entity_name: 'S',
|
|
330
|
+
coordinates: {x: 0, y: 0, z: 0},
|
|
331
|
+
item_id: 1,
|
|
332
|
+
cargomass: 0,
|
|
333
|
+
cargo: [],
|
|
334
|
+
modules: [],
|
|
335
|
+
lanes: [
|
|
336
|
+
{lane_key: 1, schedule: {started: '2026-06-19T00:00:00', tasks: [producer]}},
|
|
337
|
+
{lane_key: 2, schedule: {started: '2026-06-19T00:00:00', tasks: [consumer]}},
|
|
338
|
+
],
|
|
339
|
+
gatherer_lanes: [],
|
|
340
|
+
crafter_lanes: [],
|
|
341
|
+
loader_lanes: [],
|
|
342
|
+
holds: [],
|
|
343
|
+
})
|
|
344
|
+
expect(cancelEligibility(e, 1, 0, {now: upcoming}).blockedReason).toBe(
|
|
345
|
+
CancelBlockReason.WOULD_STRAND
|
|
346
|
+
)
|
|
347
|
+
})
|
|
348
|
+
})
|