@shipload/sdk 1.0.0-next.73 → 1.0.0-next.74
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 +502 -127
- package/lib/shipload.js +5871 -4906
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +5858 -4900
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +174 -47
- package/lib/testing.js +524 -142
- package/lib/testing.js.map +1 -1
- package/lib/testing.m.js +524 -142
- package/lib/testing.m.js.map +1 -1
- package/package.json +1 -1
- package/src/contracts/server.ts +457 -134
- package/src/data/entities.json +2 -2
- package/src/index-module.ts +22 -1
- package/src/influence/civic-shuttle.test.ts +58 -0
- package/src/influence/civic-shuttle.ts +52 -6
- package/src/influence/duration.ts +0 -65
- package/src/influence/index.ts +4 -5
- package/src/influence/influence.test.ts +51 -136
- package/src/influence/valuation.ts +75 -0
- package/src/managers/actions.ts +37 -10
- package/src/managers/balances.test.ts +33 -0
- package/src/managers/balances.ts +15 -0
- package/src/managers/context.ts +50 -11
- package/src/managers/index.ts +14 -0
- package/src/managers/jobs.test.ts +82 -1
- package/src/managers/jobs.ts +41 -2
- package/src/managers/nft.test.ts +173 -1
- package/src/managers/nft.ts +35 -2
- package/src/managers/shuttle.test.ts +562 -0
- package/src/managers/shuttle.ts +397 -0
- package/src/scheduling/availability.ts +10 -0
- package/src/scheduling/energy.ts +55 -1
- package/src/scheduling/jobs.test.ts +171 -1
- package/src/scheduling/jobs.ts +110 -13
- package/src/scheduling/projection.ts +2 -1
- package/src/scheduling/task-effects.ts +38 -0
- package/src/shipload.ts +23 -4
package/src/managers/actions.ts
CHANGED
|
@@ -332,25 +332,35 @@ export class ActionsManager extends BaseManager {
|
|
|
332
332
|
depotstore(
|
|
333
333
|
shipId: UInt64Type,
|
|
334
334
|
depotId: UInt64Type,
|
|
335
|
-
items: ServerContract.ActionParams.Type.cargo_item[]
|
|
335
|
+
items: ServerContract.ActionParams.Type.cargo_item[],
|
|
336
|
+
shuttledBy?: UInt64Type
|
|
336
337
|
): Action {
|
|
337
|
-
|
|
338
|
+
const params: ServerContract.ActionParams.depotstore = {
|
|
338
339
|
ship_id: UInt64.from(shipId),
|
|
339
340
|
depot_id: UInt64.from(depotId),
|
|
340
341
|
items,
|
|
341
|
-
}
|
|
342
|
+
}
|
|
343
|
+
if (shuttledBy !== undefined) {
|
|
344
|
+
params.shuttled_by = UInt64.from(shuttledBy)
|
|
345
|
+
}
|
|
346
|
+
return this.server.action('depotstore', params)
|
|
342
347
|
}
|
|
343
348
|
|
|
344
349
|
depottake(
|
|
345
350
|
shipId: UInt64Type,
|
|
346
351
|
depotId: UInt64Type,
|
|
347
|
-
items: ServerContract.ActionParams.Type.cargo_item[]
|
|
352
|
+
items: ServerContract.ActionParams.Type.cargo_item[],
|
|
353
|
+
shuttledBy?: UInt64Type
|
|
348
354
|
): Action {
|
|
349
|
-
|
|
355
|
+
const params: ServerContract.ActionParams.depottake = {
|
|
350
356
|
ship_id: UInt64.from(shipId),
|
|
351
357
|
depot_id: UInt64.from(depotId),
|
|
352
358
|
items,
|
|
353
|
-
}
|
|
359
|
+
}
|
|
360
|
+
if (shuttledBy !== undefined) {
|
|
361
|
+
params.shuttled_by = UInt64.from(shuttledBy)
|
|
362
|
+
}
|
|
363
|
+
return this.server.action('depottake', params)
|
|
354
364
|
}
|
|
355
365
|
|
|
356
366
|
shuttle(
|
|
@@ -518,7 +528,8 @@ export class ActionsManager extends BaseManager {
|
|
|
518
528
|
workshopId: UInt64Type,
|
|
519
529
|
recipeId: UInt16Type,
|
|
520
530
|
quantity: UInt32Type,
|
|
521
|
-
inputs: ServerContract.ActionParams.Type.cargo_item[]
|
|
531
|
+
inputs: ServerContract.ActionParams.Type.cargo_item[],
|
|
532
|
+
shuttledBy?: UInt64Type
|
|
522
533
|
): Action {
|
|
523
534
|
const params: ServerContract.ActionParams.craftjob = {
|
|
524
535
|
ship_id: UInt64.from(shipId),
|
|
@@ -527,6 +538,9 @@ export class ActionsManager extends BaseManager {
|
|
|
527
538
|
quantity: UInt32.from(quantity),
|
|
528
539
|
inputs,
|
|
529
540
|
}
|
|
541
|
+
if (shuttledBy !== undefined) {
|
|
542
|
+
params.shuttled_by = UInt64.from(shuttledBy)
|
|
543
|
+
}
|
|
530
544
|
return this.server.action('craftjob', params)
|
|
531
545
|
}
|
|
532
546
|
|
|
@@ -540,6 +554,9 @@ export class ActionsManager extends BaseManager {
|
|
|
540
554
|
canceljob(route: JobCancelRoute): Action {
|
|
541
555
|
if (route.kind === 'dropoff') return this.cancel(route.shipId, route.laneKey, route.count)
|
|
542
556
|
if (route.kind === 'build') return this.cancelbuild(route.jobId)
|
|
557
|
+
if (route.kind === 'civic') {
|
|
558
|
+
return this.cancelcivic(route.buildingId, route.laneKey, route.fromId)
|
|
559
|
+
}
|
|
543
560
|
return this.cancelcraft(route.jobId)
|
|
544
561
|
}
|
|
545
562
|
|
|
@@ -566,11 +583,14 @@ export class ActionsManager extends BaseManager {
|
|
|
566
583
|
return this.server.action('clustercraft', params)
|
|
567
584
|
}
|
|
568
585
|
|
|
569
|
-
claimcraft(jobId: UInt64Type, shipId: UInt64Type): Action {
|
|
586
|
+
claimcraft(jobId: UInt64Type, shipId: UInt64Type, shuttledBy?: UInt64Type): Action {
|
|
570
587
|
const params: ServerContract.ActionParams.claimcraft = {
|
|
571
588
|
job_id: UInt64.from(jobId),
|
|
572
589
|
ship_id: UInt64.from(shipId),
|
|
573
590
|
}
|
|
591
|
+
if (shuttledBy !== undefined) {
|
|
592
|
+
params.shuttled_by = UInt64.from(shuttledBy)
|
|
593
|
+
}
|
|
574
594
|
return this.server.action('claimcraft', params)
|
|
575
595
|
}
|
|
576
596
|
|
|
@@ -578,7 +598,8 @@ export class ActionsManager extends BaseManager {
|
|
|
578
598
|
targetId: UInt64Type,
|
|
579
599
|
dockId: UInt64Type,
|
|
580
600
|
targetItemId: UInt16Type,
|
|
581
|
-
inputs: ServerContract.ActionParams.Type.cargo_item[]
|
|
601
|
+
inputs: ServerContract.ActionParams.Type.cargo_item[],
|
|
602
|
+
shuttledBy?: UInt64Type
|
|
582
603
|
): Action {
|
|
583
604
|
const params: ServerContract.ActionParams.buildjob = {
|
|
584
605
|
target_id: UInt64.from(targetId),
|
|
@@ -586,6 +607,9 @@ export class ActionsManager extends BaseManager {
|
|
|
586
607
|
target_item_id: UInt16.from(targetItemId),
|
|
587
608
|
inputs,
|
|
588
609
|
}
|
|
610
|
+
if (shuttledBy !== undefined) {
|
|
611
|
+
params.shuttled_by = UInt64.from(shuttledBy)
|
|
612
|
+
}
|
|
589
613
|
return this.server.action('buildjob', params)
|
|
590
614
|
}
|
|
591
615
|
|
|
@@ -596,10 +620,13 @@ export class ActionsManager extends BaseManager {
|
|
|
596
620
|
return this.server.action('claimbuild', params)
|
|
597
621
|
}
|
|
598
622
|
|
|
599
|
-
cancelbuild(jobId: UInt64Type): Action {
|
|
623
|
+
cancelbuild(jobId: UInt64Type, shuttledBy?: UInt64Type): Action {
|
|
600
624
|
const params: ServerContract.ActionParams.cancelbuild = {
|
|
601
625
|
job_id: UInt64.from(jobId),
|
|
602
626
|
}
|
|
627
|
+
if (shuttledBy !== undefined) {
|
|
628
|
+
params.shuttled_by = UInt64.from(shuttledBy)
|
|
629
|
+
}
|
|
603
630
|
return this.server.action('cancelbuild', params)
|
|
604
631
|
}
|
|
605
632
|
|
|
@@ -78,6 +78,39 @@ describe('getDepositConfig', () => {
|
|
|
78
78
|
})
|
|
79
79
|
})
|
|
80
80
|
|
|
81
|
+
describe('setDepositConfig', () => {
|
|
82
|
+
test('a seeded config keeps every balance read off the chain', async () => {
|
|
83
|
+
const {manager, counts} = stubContext({
|
|
84
|
+
depositConfig: SCRAP,
|
|
85
|
+
balances: [{token_contract: 'scrap.gm', balance: '5 SCRAP'}],
|
|
86
|
+
wallet: ['7 SCRAP'],
|
|
87
|
+
})
|
|
88
|
+
manager.setDepositConfig({tokenContract: 'scrap.gm', symbol: '0,SCRAP'})
|
|
89
|
+
|
|
90
|
+
const config = await manager.getDepositConfig()
|
|
91
|
+
expect(String(config?.tokenContract)).toBe('scrap.gm')
|
|
92
|
+
expect(String(config?.symbol)).toBe('0,SCRAP')
|
|
93
|
+
await manager.getPlatformBalance('alice')
|
|
94
|
+
await manager.getWalletBalance('alice')
|
|
95
|
+
|
|
96
|
+
expect(counts.depositcfg).toBe(0)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('a reload still reaches the chain', async () => {
|
|
100
|
+
const {manager, counts} = stubContext({depositConfig: SCRAP})
|
|
101
|
+
manager.setDepositConfig({tokenContract: 'scrap.gm', symbol: '0,SCRAP'})
|
|
102
|
+
await manager.getDepositConfig(true)
|
|
103
|
+
expect(counts.depositcfg).toBe(1)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('seeding null records a chain with no deposit token', async () => {
|
|
107
|
+
const {manager, counts} = stubContext({depositConfig: SCRAP})
|
|
108
|
+
manager.setDepositConfig(null)
|
|
109
|
+
expect(await manager.getDepositConfig()).toBeNull()
|
|
110
|
+
expect(counts.depositcfg).toBe(0)
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
81
114
|
describe('getPlatformBalance', () => {
|
|
82
115
|
test('returns null when the owner has never opened a row', async () => {
|
|
83
116
|
const {manager} = stubContext({depositConfig: SCRAP, balances: []})
|
package/src/managers/balances.ts
CHANGED
|
@@ -7,6 +7,11 @@ export interface DepositConfig {
|
|
|
7
7
|
symbol: Asset.Symbol
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
export interface DepositConfigInput {
|
|
11
|
+
tokenContract: NameType
|
|
12
|
+
symbol: Asset.SymbolType
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
export interface PlatformBalance {
|
|
11
16
|
owner: Name
|
|
12
17
|
tokenContract: Name
|
|
@@ -22,6 +27,16 @@ export interface TokenBalance {
|
|
|
22
27
|
export class BalancesManager extends BaseManager {
|
|
23
28
|
private depositConfig?: DepositConfig | null
|
|
24
29
|
|
|
30
|
+
// A caller holding the config already, from the indexer or its own cache, seeds it here so no balance read reaches the chain for it.
|
|
31
|
+
setDepositConfig(config: DepositConfigInput | null): void {
|
|
32
|
+
this.depositConfig = config
|
|
33
|
+
? {
|
|
34
|
+
tokenContract: Name.from(config.tokenContract),
|
|
35
|
+
symbol: Asset.Symbol.from(config.symbol),
|
|
36
|
+
}
|
|
37
|
+
: null
|
|
38
|
+
}
|
|
39
|
+
|
|
25
40
|
async getDepositConfig(reload = false): Promise<DepositConfig | null> {
|
|
26
41
|
if (!reload && this.depositConfig !== undefined) {
|
|
27
42
|
return this.depositConfig
|
package/src/managers/context.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {NftManager} from './nft'
|
|
|
15
15
|
import {BalancesManager} from './balances'
|
|
16
16
|
import {FundManager} from './fund'
|
|
17
17
|
import {JobsManager} from './jobs'
|
|
18
|
+
import {ShuttleManager} from './shuttle'
|
|
18
19
|
import {InfluenceManager} from './influence'
|
|
19
20
|
import {SubscriptionsManager} from '../subscriptions/manager'
|
|
20
21
|
|
|
@@ -30,12 +31,15 @@ export class GameContext {
|
|
|
30
31
|
private _balances?: BalancesManager
|
|
31
32
|
private _funds?: FundManager
|
|
32
33
|
private _jobs?: JobsManager
|
|
34
|
+
private _shuttle?: ShuttleManager
|
|
33
35
|
private _influence?: InfluenceManager
|
|
34
36
|
private _subscriptions?: SubscriptionsManager
|
|
35
37
|
private _subscriptionsUrl?: string
|
|
36
38
|
|
|
37
39
|
private _gameCache?: PlatformContract.Types.game_row
|
|
40
|
+
private _gameInflight?: Promise<PlatformContract.Types.game_row>
|
|
38
41
|
private _stateCache?: GameState
|
|
42
|
+
private _stateInflight?: Promise<GameState>
|
|
39
43
|
|
|
40
44
|
constructor(
|
|
41
45
|
public readonly client: APIClient,
|
|
@@ -122,6 +126,13 @@ export class GameContext {
|
|
|
122
126
|
return this._jobs
|
|
123
127
|
}
|
|
124
128
|
|
|
129
|
+
get shuttle(): ShuttleManager {
|
|
130
|
+
if (!this._shuttle) {
|
|
131
|
+
this._shuttle = new ShuttleManager(this)
|
|
132
|
+
}
|
|
133
|
+
return this._shuttle
|
|
134
|
+
}
|
|
135
|
+
|
|
125
136
|
get influence(): InfluenceManager {
|
|
126
137
|
if (!this._influence) {
|
|
127
138
|
this._influence = new InfluenceManager(this)
|
|
@@ -149,25 +160,53 @@ export class GameContext {
|
|
|
149
160
|
if (!reload && this._gameCache) {
|
|
150
161
|
return this._gameCache
|
|
151
162
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
163
|
+
if (!reload && this._gameInflight) {
|
|
164
|
+
return this._gameInflight
|
|
165
|
+
}
|
|
166
|
+
const inflight = (async () => {
|
|
167
|
+
const game = await this.platform.table('games').get()
|
|
168
|
+
if (!game) {
|
|
169
|
+
throw new Error('Game not initialized')
|
|
170
|
+
}
|
|
171
|
+
this._gameCache = game
|
|
172
|
+
return game
|
|
173
|
+
})()
|
|
174
|
+
this._gameInflight = inflight
|
|
175
|
+
try {
|
|
176
|
+
return await inflight
|
|
177
|
+
} finally {
|
|
178
|
+
if (this._gameInflight === inflight) {
|
|
179
|
+
this._gameInflight = undefined
|
|
180
|
+
}
|
|
155
181
|
}
|
|
156
|
-
this._gameCache = game
|
|
157
|
-
return game
|
|
158
182
|
}
|
|
159
183
|
|
|
160
184
|
async getState(reload = false): Promise<GameState> {
|
|
161
185
|
if (!reload && this._stateCache) {
|
|
162
186
|
return this._stateCache
|
|
163
187
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
188
|
+
if (!reload && this._stateInflight) {
|
|
189
|
+
return this._stateInflight
|
|
190
|
+
}
|
|
191
|
+
const inflight = (async () => {
|
|
192
|
+
const [state, game] = await Promise.all([
|
|
193
|
+
this.server.table('state').get(),
|
|
194
|
+
this.getGame(),
|
|
195
|
+
])
|
|
196
|
+
if (!state) {
|
|
197
|
+
throw new Error('Game state not initialized')
|
|
198
|
+
}
|
|
199
|
+
this._stateCache = GameState.from(state, game)
|
|
200
|
+
return this._stateCache
|
|
201
|
+
})()
|
|
202
|
+
this._stateInflight = inflight
|
|
203
|
+
try {
|
|
204
|
+
return await inflight
|
|
205
|
+
} finally {
|
|
206
|
+
if (this._stateInflight === inflight) {
|
|
207
|
+
this._stateInflight = undefined
|
|
208
|
+
}
|
|
167
209
|
}
|
|
168
|
-
const game = this._gameCache
|
|
169
|
-
this._stateCache = GameState.from(state, game)
|
|
170
|
-
return this._stateCache
|
|
171
210
|
}
|
|
172
211
|
|
|
173
212
|
get cachedGame(): PlatformContract.Types.game_row | undefined {
|
package/src/managers/index.ts
CHANGED
|
@@ -15,6 +15,20 @@ export type {NftConfigForItem, WrapConfig, WrapGate} from './nft'
|
|
|
15
15
|
export {BalancesManager} from './balances'
|
|
16
16
|
export type {DepositConfig, PlatformBalance, TokenBalance} from './balances'
|
|
17
17
|
export {JobsManager} from './jobs'
|
|
18
|
+
export {
|
|
19
|
+
ShuttleManager,
|
|
20
|
+
rankShuttleOptions,
|
|
21
|
+
shuttleReasonCode,
|
|
22
|
+
BOOKING_LEVEL_CODES,
|
|
23
|
+
OMITTED_REASONS,
|
|
24
|
+
} from './shuttle'
|
|
25
|
+
export type {
|
|
26
|
+
ShuttleMode,
|
|
27
|
+
ShuttleOption,
|
|
28
|
+
ShuttleOptions,
|
|
29
|
+
ShuttleReason,
|
|
30
|
+
ShuttleReasonCode,
|
|
31
|
+
} from './shuttle'
|
|
18
32
|
export {InfluenceManager} from './influence'
|
|
19
33
|
export type {
|
|
20
34
|
CharterProgress,
|
|
@@ -41,7 +41,7 @@ describe('JobsManager.getOwnedJobs', () => {
|
|
|
41
41
|
expect(jobs[0].inputs).toEqual([{item: 'in'}] as never)
|
|
42
42
|
})
|
|
43
43
|
|
|
44
|
-
it('reports
|
|
44
|
+
it('reports a legacy undeposited row (inputs only) as dropping off, whatever its window says', async () => {
|
|
45
45
|
const inFlight = row({
|
|
46
46
|
deposited: false,
|
|
47
47
|
cargo: [{item: 'in'}],
|
|
@@ -57,6 +57,27 @@ describe('JobsManager.getOwnedJobs', () => {
|
|
|
57
57
|
expect(jobs[0].inputs).toEqual([{item: 'in'}] as never)
|
|
58
58
|
})
|
|
59
59
|
|
|
60
|
+
it('reads a booking-time undeposited row whose input is booked as more than one stack', async () => {
|
|
61
|
+
const inFlight = row({
|
|
62
|
+
deposited: false,
|
|
63
|
+
cargo: [
|
|
64
|
+
{item_id: 101, quantity: 20},
|
|
65
|
+
{item_id: 101, quantity: 30},
|
|
66
|
+
],
|
|
67
|
+
})
|
|
68
|
+
const m = managerWith(
|
|
69
|
+
async () => [inFlight],
|
|
70
|
+
async () => []
|
|
71
|
+
)
|
|
72
|
+
const jobs = await m.getOwnedJobs(OWNER, {now: new Date('2026-07-26T11:30:00Z')})
|
|
73
|
+
expect(jobs[0].status).toBe('dropping')
|
|
74
|
+
expect(jobs[0].inputs).toEqual([
|
|
75
|
+
{item_id: 101, quantity: 20},
|
|
76
|
+
{item_id: 101, quantity: 30},
|
|
77
|
+
] as never)
|
|
78
|
+
expect(jobs[0].output).toBeNull()
|
|
79
|
+
})
|
|
80
|
+
|
|
60
81
|
it('reports a cancelled row as ready with its inputs held', async () => {
|
|
61
82
|
const cancelled = row({
|
|
62
83
|
quantity: {toNumber: () => 0},
|
|
@@ -94,3 +115,63 @@ describe('JobsManager.getOwnedJobs', () => {
|
|
|
94
115
|
expect(jobs).toHaveLength(1)
|
|
95
116
|
})
|
|
96
117
|
})
|
|
118
|
+
|
|
119
|
+
const buildRow = (over: Record<string, unknown> = {}) => ({
|
|
120
|
+
id: {toNumber: () => 9},
|
|
121
|
+
building: {toNumber: () => 2},
|
|
122
|
+
socket: {toNumber: () => 1},
|
|
123
|
+
target: {toNumber: () => 5},
|
|
124
|
+
owner: Name.from(OWNER),
|
|
125
|
+
coords: {x: {toNumber: () => 0}, y: {toNumber: () => 0}},
|
|
126
|
+
starts_at: {toDate: () => new Date('2026-07-26T10:00:00Z')},
|
|
127
|
+
completes_at: {toDate: () => new Date('2026-07-26T11:00:00Z')},
|
|
128
|
+
arrives_at: {toDate: () => new Date('2026-07-26T09:45:00Z')},
|
|
129
|
+
target_item_id: {toNumber: () => 10212},
|
|
130
|
+
cargo: [{item: 'in'}],
|
|
131
|
+
deposited: true,
|
|
132
|
+
...over,
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
function managerWithReadonly(impl: (name: string, data: unknown) => Promise<unknown>) {
|
|
136
|
+
const ctx = {server: {readonly: impl}} as never
|
|
137
|
+
return new JobsManager(ctx)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
describe('JobsManager.getBuildJobs', () => {
|
|
141
|
+
it('reads the dock queue through getbuildjobs and parses every row', async () => {
|
|
142
|
+
let called: unknown = null
|
|
143
|
+
const m = managerWithReadonly(async (name, data) => {
|
|
144
|
+
called = {name, data}
|
|
145
|
+
return {jobs: [buildRow(), buildRow({id: {toNumber: () => 10}, deposited: false})]}
|
|
146
|
+
})
|
|
147
|
+
const jobs = await m.getBuildJobs(2, {now: new Date('2026-07-26T11:30:00Z')})
|
|
148
|
+
expect(called).toMatchObject({name: 'getbuildjobs'})
|
|
149
|
+
expect(String((called as {data: {building_id: unknown}}).data.building_id)).toBe('2')
|
|
150
|
+
expect(jobs).toHaveLength(2)
|
|
151
|
+
expect(jobs[0]).toMatchObject({
|
|
152
|
+
id: 9,
|
|
153
|
+
building: 2,
|
|
154
|
+
socket: 1,
|
|
155
|
+
targetId: 5,
|
|
156
|
+
owner: OWNER,
|
|
157
|
+
targetItemId: 10212,
|
|
158
|
+
deposited: true,
|
|
159
|
+
status: 'ready',
|
|
160
|
+
})
|
|
161
|
+
expect(jobs[0].coords).toEqual({x: 0, y: 0})
|
|
162
|
+
expect(jobs[0].arrivesAt).toEqual(new Date('2026-07-26T09:45:00Z'))
|
|
163
|
+
expect(jobs[0].inputs).toEqual([{item: 'in'}] as never)
|
|
164
|
+
expect(jobs[1].status).toBe('dropping')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('reports a booked row before its window as queued', async () => {
|
|
168
|
+
const m = managerWithReadonly(async () => ({jobs: [buildRow()]}))
|
|
169
|
+
const jobs = await m.getBuildJobs(2, {now: new Date('2026-07-26T09:50:00Z')})
|
|
170
|
+
expect(jobs[0].status).toBe('queued')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('returns an empty list when the result carries no jobs', async () => {
|
|
174
|
+
const m = managerWithReadonly(async () => ({}))
|
|
175
|
+
expect(await m.getBuildJobs(2)).toEqual([])
|
|
176
|
+
})
|
|
177
|
+
})
|
package/src/managers/jobs.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import {Name, type NameType} from '@wharfkit/antelope'
|
|
1
|
+
import {Name, UInt64, type NameType, type UInt64Type} from '@wharfkit/antelope'
|
|
2
2
|
import {BaseManager} from './base'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
jobDeposited,
|
|
5
|
+
jobStatus,
|
|
6
|
+
splitJobCargo,
|
|
7
|
+
type BuildJob,
|
|
8
|
+
type OwnedJob,
|
|
9
|
+
} from '../scheduling/jobs'
|
|
4
10
|
import type {ServerContract} from '../contracts'
|
|
5
11
|
|
|
6
12
|
type JobRow = ServerContract.Types.craftjob_row
|
|
13
|
+
type BuildJobRow = ServerContract.Types.buildjob_row
|
|
7
14
|
|
|
8
15
|
export class JobsManager extends BaseManager {
|
|
9
16
|
async getOwnedJobs(owner: NameType, opts?: {now?: Date}): Promise<OwnedJob[]> {
|
|
@@ -29,6 +36,38 @@ export class JobsManager extends BaseManager {
|
|
|
29
36
|
return rows.filter((r) => ownerName.equals(r.owner)).map((r) => this.parseOwnedJob(r, now))
|
|
30
37
|
}
|
|
31
38
|
|
|
39
|
+
async getBuildJobs(dockId: UInt64Type, opts?: {now?: Date}): Promise<BuildJob[]> {
|
|
40
|
+
const now = opts?.now ?? new Date()
|
|
41
|
+
const result = (await this.server.readonly('getbuildjobs', {
|
|
42
|
+
building_id: UInt64.from(dockId),
|
|
43
|
+
})) as ServerContract.Types.buildjobs_result | undefined
|
|
44
|
+
return (result?.jobs ?? []).map((r) => this.parseBuildJob(r, now))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private parseBuildJob(r: BuildJobRow, now: Date): BuildJob {
|
|
48
|
+
const startsAt = r.starts_at.toDate()
|
|
49
|
+
const completesAt = r.completes_at.toDate()
|
|
50
|
+
const arrivesAt = r.arrives_at.toDate()
|
|
51
|
+
const deposited = jobDeposited(r.deposited)
|
|
52
|
+
const building = r.building.toNumber()
|
|
53
|
+
const inputs = [...r.cargo]
|
|
54
|
+
return {
|
|
55
|
+
id: r.id.toNumber(),
|
|
56
|
+
building,
|
|
57
|
+
socket: r.socket.toNumber(),
|
|
58
|
+
targetId: r.target.toNumber(),
|
|
59
|
+
owner: r.owner.toString(),
|
|
60
|
+
coords: {x: r.coords.x.toNumber(), y: r.coords.y.toNumber()},
|
|
61
|
+
startsAt,
|
|
62
|
+
completesAt,
|
|
63
|
+
arrivesAt,
|
|
64
|
+
targetItemId: r.target_item_id.toNumber(),
|
|
65
|
+
status: jobStatus({startsAt, completesAt, arrivesAt, deposited, building, inputs}, now),
|
|
66
|
+
deposited,
|
|
67
|
+
inputs,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
32
71
|
private parseOwnedJob(r: JobRow, now: Date): OwnedJob {
|
|
33
72
|
const startsAt = r.starts_at.toDate()
|
|
34
73
|
const completesAt = r.completes_at.toDate()
|
package/src/managers/nft.test.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import {describe, expect, test} from 'bun:test'
|
|
2
|
-
import {
|
|
2
|
+
import {Asset, Name} from '@wharfkit/antelope'
|
|
3
|
+
import {NftManager, resolveLockedAmount, wrapCostKey} from './nft'
|
|
4
|
+
import type {GameContext} from './context'
|
|
5
|
+
import {ServerContract} from '../contracts'
|
|
3
6
|
|
|
4
7
|
describe('resolveLockedAmount', () => {
|
|
5
8
|
test('no fee refunds the full cost', () => {
|
|
@@ -19,3 +22,172 @@ describe('wrapCostKey', () => {
|
|
|
19
22
|
expect(wrapCostKey(3, 2).toString()).toBe('770')
|
|
20
23
|
})
|
|
21
24
|
})
|
|
25
|
+
|
|
26
|
+
interface StubOptions {
|
|
27
|
+
wrapConfig?: {fee_pct: number; fee_account: string; min_asset_id?: number} | null
|
|
28
|
+
wrapCosts?: {item_type: number; tier: number; amount: number}[]
|
|
29
|
+
depositConfig?: {tokenContract: string; symbol: string} | null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function stubContext(options: StubOptions = {}) {
|
|
33
|
+
const counts = {wrapconfig: 0, wrapcost: 0, depositcfg: 0}
|
|
34
|
+
const costRows = (options.wrapCosts ?? []).map((row) =>
|
|
35
|
+
ServerContract.Types.wrapcost_row.from(row)
|
|
36
|
+
)
|
|
37
|
+
const server = {
|
|
38
|
+
table(name: string) {
|
|
39
|
+
if (name === 'wrapconfig') {
|
|
40
|
+
return {
|
|
41
|
+
get: async () => {
|
|
42
|
+
counts.wrapconfig++
|
|
43
|
+
return options.wrapConfig
|
|
44
|
+
? ServerContract.Types.wrapconfig_row.from({
|
|
45
|
+
...options.wrapConfig,
|
|
46
|
+
min_asset_id: options.wrapConfig.min_asset_id ?? 0,
|
|
47
|
+
})
|
|
48
|
+
: undefined
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (name === 'wrapcost') {
|
|
53
|
+
return {
|
|
54
|
+
get: async (key: {toString(): string}) => {
|
|
55
|
+
counts.wrapcost++
|
|
56
|
+
return costRows.find(
|
|
57
|
+
(row) =>
|
|
58
|
+
wrapCostKey(Number(row.item_type), Number(row.tier)).toString() ===
|
|
59
|
+
key.toString()
|
|
60
|
+
)
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
throw new Error(`unexpected table: ${name}`)
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
const balances = {
|
|
68
|
+
getDepositConfig: async () => {
|
|
69
|
+
counts.depositcfg++
|
|
70
|
+
return options.depositConfig
|
|
71
|
+
? {
|
|
72
|
+
tokenContract: Name.from(options.depositConfig.tokenContract),
|
|
73
|
+
symbol: Asset.Symbol.from(options.depositConfig.symbol),
|
|
74
|
+
}
|
|
75
|
+
: null
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
const context = {server, balances} as unknown as GameContext
|
|
79
|
+
return {manager: new NftManager(context), counts}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const DEPOSIT = {tokenContract: 'scrap.gm', symbol: '0,SCRAP'}
|
|
83
|
+
|
|
84
|
+
describe('setWrapConfig', () => {
|
|
85
|
+
test('a seeded config keeps getWrapConfig off the chain', async () => {
|
|
86
|
+
const {manager, counts} = stubContext({wrapConfig: {fee_pct: 200, fee_account: 'fee.gm'}})
|
|
87
|
+
manager.setWrapConfig({feePctBasisPoints: 200, feeAccount: 'fee.gm'})
|
|
88
|
+
|
|
89
|
+
const config = await manager.getWrapConfig()
|
|
90
|
+
expect(config?.feePctBasisPoints).toBe(200)
|
|
91
|
+
expect(String(config?.feeAccount)).toBe('fee.gm')
|
|
92
|
+
expect(counts.wrapconfig).toBe(0)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('a reload still reaches the chain', async () => {
|
|
96
|
+
const {manager, counts} = stubContext({wrapConfig: {fee_pct: 200, fee_account: 'fee.gm'}})
|
|
97
|
+
manager.setWrapConfig({feePctBasisPoints: 200, feeAccount: 'fee.gm'})
|
|
98
|
+
await manager.getWrapConfig(true)
|
|
99
|
+
expect(counts.wrapconfig).toBe(1)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('seeding null records a chain with no wrap fee configured', async () => {
|
|
103
|
+
const {manager, counts} = stubContext({wrapConfig: {fee_pct: 200, fee_account: 'fee.gm'}})
|
|
104
|
+
manager.setWrapConfig(null)
|
|
105
|
+
expect(await manager.getWrapConfig()).toBeNull()
|
|
106
|
+
expect(counts.wrapconfig).toBe(0)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe('setWrapCosts', () => {
|
|
111
|
+
test('a seeded cost keeps getWrapCost off the chain', async () => {
|
|
112
|
+
const {manager, counts} = stubContext({
|
|
113
|
+
wrapCosts: [{item_type: 3, tier: 2, amount: 500}],
|
|
114
|
+
})
|
|
115
|
+
manager.setWrapCosts([{itemType: 3, tier: 2, units: 500n}])
|
|
116
|
+
|
|
117
|
+
expect(await manager.getWrapCost(3, 2)).toBe(500n)
|
|
118
|
+
expect(counts.wrapcost).toBe(0)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('an item missing from the seed falls back to chain', async () => {
|
|
122
|
+
const {manager, counts} = stubContext({
|
|
123
|
+
wrapCosts: [{item_type: 3, tier: 2, amount: 500}],
|
|
124
|
+
})
|
|
125
|
+
manager.setWrapCosts([{itemType: 3, tier: 2, units: 500n}])
|
|
126
|
+
|
|
127
|
+
expect(await manager.getWrapCost(9, 1)).toBe(0n)
|
|
128
|
+
expect(counts.wrapcost).toBe(1)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('a reload still reaches the chain', async () => {
|
|
132
|
+
const {manager, counts} = stubContext({
|
|
133
|
+
wrapCosts: [{item_type: 3, tier: 2, amount: 500}],
|
|
134
|
+
})
|
|
135
|
+
manager.setWrapCosts([{itemType: 3, tier: 2, units: 500n}])
|
|
136
|
+
await manager.getWrapCost(3, 2, true)
|
|
137
|
+
expect(counts.wrapcost).toBe(1)
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe('getWrapDeposit', () => {
|
|
142
|
+
test('with every seed present, makes no chain reads', async () => {
|
|
143
|
+
const {manager, counts} = stubContext({
|
|
144
|
+
wrapCosts: [{item_type: 3, tier: 2, amount: 500}],
|
|
145
|
+
wrapConfig: {fee_pct: 200, fee_account: 'fee.gm'},
|
|
146
|
+
depositConfig: DEPOSIT,
|
|
147
|
+
})
|
|
148
|
+
manager.setWrapCosts([{itemType: 3, tier: 2, units: 500n}])
|
|
149
|
+
manager.setWrapConfig({feePctBasisPoints: 200, feeAccount: 'fee.gm'})
|
|
150
|
+
|
|
151
|
+
const deposit = await manager.getWrapDeposit(3, 2)
|
|
152
|
+
expect(deposit).toEqual({
|
|
153
|
+
cost: 500n,
|
|
154
|
+
refund: 490n,
|
|
155
|
+
feePct: 2,
|
|
156
|
+
symbol: 'SCRAP',
|
|
157
|
+
precision: 0,
|
|
158
|
+
tokenContract: 'scrap.gm',
|
|
159
|
+
})
|
|
160
|
+
expect(counts.wrapcost).toBe(0)
|
|
161
|
+
expect(counts.wrapconfig).toBe(0)
|
|
162
|
+
// getDepositConfig is a BalancesManager concern seeded separately; this stub always calls through.
|
|
163
|
+
expect(counts.depositcfg).toBe(1)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
test('with nothing seeded, falls back to chain for config and cost', async () => {
|
|
167
|
+
const {manager, counts} = stubContext({
|
|
168
|
+
wrapCosts: [{item_type: 3, tier: 2, amount: 500}],
|
|
169
|
+
wrapConfig: {fee_pct: 200, fee_account: 'fee.gm'},
|
|
170
|
+
depositConfig: DEPOSIT,
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
const deposit = await manager.getWrapDeposit(3, 2)
|
|
174
|
+
expect(deposit?.cost).toBe(500n)
|
|
175
|
+
expect(counts.wrapcost).toBe(1)
|
|
176
|
+
expect(counts.wrapconfig).toBe(1)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('reload true bypasses seeds and reaches chain', async () => {
|
|
180
|
+
const {manager, counts} = stubContext({
|
|
181
|
+
wrapCosts: [{item_type: 3, tier: 2, amount: 500}],
|
|
182
|
+
wrapConfig: {fee_pct: 200, fee_account: 'fee.gm'},
|
|
183
|
+
depositConfig: DEPOSIT,
|
|
184
|
+
})
|
|
185
|
+
manager.setWrapCosts([{itemType: 3, tier: 2, units: 500n}])
|
|
186
|
+
manager.setWrapConfig({feePctBasisPoints: 200, feeAccount: 'fee.gm'})
|
|
187
|
+
|
|
188
|
+
await manager.getWrapDeposit(3, 2, {reload: true})
|
|
189
|
+
expect(counts.wrapcost).toBe(1)
|
|
190
|
+
expect(counts.wrapconfig).toBe(1)
|
|
191
|
+
expect(counts.depositcfg).toBe(1)
|
|
192
|
+
})
|
|
193
|
+
})
|