@shipload/sdk 1.0.0-next.72 → 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 +576 -133
- package/lib/shipload.js +6019 -5114
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +6002 -5108
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +194 -52
- package/lib/testing.js +767 -468
- package/lib/testing.js.map +1 -1
- package/lib/testing.m.js +767 -468
- package/lib/testing.m.js.map +1 -1
- package/package.json +1 -1
- package/src/contracts/server.ts +523 -143
- package/src/data/catalog.test.ts +81 -1
- package/src/data/catalog.ts +6 -4
- package/src/data/entities.json +2 -2
- package/src/data/metadata.ts +197 -363
- package/src/data/task-types.json +184 -0
- package/src/entities/makers.ts +2 -2
- package/src/index-module.ts +31 -2
- 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 +49 -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/resolution/resolve-item.ts +2 -2
- 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/scheduling/unwrap.test.ts +60 -0
- package/src/scheduling/unwrap.ts +24 -8
- package/src/shipload.ts +23 -4
- package/src/types.ts +1 -0
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import {describe, expect, it} from 'bun:test'
|
|
2
|
-
import
|
|
2
|
+
import {UInt8, UInt32, UInt64} from '@wharfkit/antelope'
|
|
3
|
+
import {ServerContract} from '../contracts'
|
|
4
|
+
import {HoldKind} from '../types'
|
|
5
|
+
import type {OrderedTask, ScheduleData} from './schedule'
|
|
3
6
|
import {
|
|
4
7
|
buildJobCancelRoute,
|
|
8
|
+
hostedDropoff,
|
|
5
9
|
jobCancelRoute,
|
|
6
10
|
jobCancellationBlockReason,
|
|
7
11
|
jobCancellable,
|
|
@@ -15,6 +19,7 @@ import {
|
|
|
15
19
|
} from './jobs'
|
|
16
20
|
|
|
17
21
|
const at = (s: string) => new Date(s)
|
|
22
|
+
const WITH_MODULES = [{item_id: 101, stats: '413333752', quantity: 10, modules: []}] as never
|
|
18
23
|
|
|
19
24
|
const INPUTS = [{item_id: 101, stats: '413333752', quantity: 10}] as never
|
|
20
25
|
const FULL_INPUT = {
|
|
@@ -191,6 +196,28 @@ describe('jobStatus', () => {
|
|
|
191
196
|
it('reads an undeposited row as dropping off when no schedule is given', () => {
|
|
192
197
|
expect(jobStatus(inFlight, at('2026-07-26T09:00:00Z'))).toBe('dropping')
|
|
193
198
|
})
|
|
199
|
+
it('finds the Drop-off of a booking-time undeposited job from its inputs-only cargo', () => {
|
|
200
|
+
const input = {item_id: 1, stats: '0', quantity: 5, modules: [], entity_id: undefined}
|
|
201
|
+
const {inputs} = splitJobCargo([input], false, 1)
|
|
202
|
+
const dropoff = task({
|
|
203
|
+
type: 21,
|
|
204
|
+
building: 7,
|
|
205
|
+
cargo: [input],
|
|
206
|
+
startsAt: at('2026-07-26T00:00:00Z'),
|
|
207
|
+
completesAt: at('2026-07-26T00:00:01Z'),
|
|
208
|
+
})
|
|
209
|
+
expect(
|
|
210
|
+
jobDropoffTask(
|
|
211
|
+
{
|
|
212
|
+
startsAt: new Date(0),
|
|
213
|
+
completesAt: new Date(0),
|
|
214
|
+
building: 7,
|
|
215
|
+
inputs: inputs as never,
|
|
216
|
+
},
|
|
217
|
+
[dropoff]
|
|
218
|
+
)
|
|
219
|
+
).toBe(dropoff)
|
|
220
|
+
})
|
|
194
221
|
it('labels every phase', () => {
|
|
195
222
|
expect(jobStatusLabel('booked')).toBe('Booked')
|
|
196
223
|
expect(jobStatusLabel('dropping')).toBe('Dropping off')
|
|
@@ -262,6 +289,37 @@ describe('jobCancelRoute', () => {
|
|
|
262
289
|
it('cancels a Queued job through cancelcraft', () => {
|
|
263
290
|
expect(jobCancelRoute(job, at('2026-07-26T09:59:59Z'))).toEqual({kind: 'craft', jobId: 7})
|
|
264
291
|
})
|
|
292
|
+
it('routes a bay-hosted drop-off cancel through cancelcivic when the task is the lane tail and not yet completed', () => {
|
|
293
|
+
const route = jobCancelRoute(inFlight, at('2026-07-26T09:00:00Z'), [], {
|
|
294
|
+
hosted: {hostId: 90000, laneKey: 1, taskIndex: 0, civic: true},
|
|
295
|
+
hostedTask: dropoff,
|
|
296
|
+
hostLaneLength: 1,
|
|
297
|
+
})
|
|
298
|
+
expect(route).toEqual({kind: 'civic', buildingId: 90000, laneKey: 1, fromId: 5})
|
|
299
|
+
})
|
|
300
|
+
it('refuses a bay-hosted civic cancel once the hosted task has completed', () => {
|
|
301
|
+
const route = jobCancelRoute(inFlight, at('2026-07-26T10:00:00Z'), [], {
|
|
302
|
+
hosted: {hostId: 90000, laneKey: 1, taskIndex: 0, civic: true},
|
|
303
|
+
hostedTask: dropoff,
|
|
304
|
+
hostLaneLength: 1,
|
|
305
|
+
})
|
|
306
|
+
expect(route).toBeNull()
|
|
307
|
+
})
|
|
308
|
+
it('refuses a bay-hosted civic cancel when the hosted task is not the lane tail', () => {
|
|
309
|
+
const route = jobCancelRoute(inFlight, at('2026-07-26T09:00:00Z'), [], {
|
|
310
|
+
hosted: {hostId: 90000, laneKey: 1, taskIndex: 0, civic: true},
|
|
311
|
+
hostedTask: dropoff,
|
|
312
|
+
hostLaneLength: 2,
|
|
313
|
+
})
|
|
314
|
+
expect(route).toBeNull()
|
|
315
|
+
})
|
|
316
|
+
it('routes a third-party-hosted drop-off cancel through the carrier ship', () => {
|
|
317
|
+
const route = jobCancelRoute(inFlight, at('2026-07-26T09:00:00Z'), [], {
|
|
318
|
+
hosted: {hostId: 8, laneKey: 2, taskIndex: 0, civic: false},
|
|
319
|
+
hostLaneLength: 3,
|
|
320
|
+
})
|
|
321
|
+
expect(route).toEqual({kind: 'dropoff', shipId: 8, laneKey: 2, count: 3})
|
|
322
|
+
})
|
|
265
323
|
it('refuses only deposited positive-quantity output-only legacy jobs', () => {
|
|
266
324
|
const legacy = {...job, inputs: []}
|
|
267
325
|
expect(jobCancellationBlockReason(legacy)).toBe('legacy-inputs-unavailable')
|
|
@@ -412,6 +470,15 @@ describe('splitJobCargo', () => {
|
|
|
412
470
|
expect(splitJobCargo([], true, 1)).toEqual({output: null, inputs: []})
|
|
413
471
|
expect(splitJobCargo([], false, 1)).toEqual({output: null, inputs: []})
|
|
414
472
|
})
|
|
473
|
+
it('reads an undeposited row whose input is booked as more than one stack per recipe slot', () => {
|
|
474
|
+
const stackA1 = {item_id: 1, quantity: 2}
|
|
475
|
+
const stackA2 = {item_id: 1, quantity: 1}
|
|
476
|
+
const stackB = {item_id: 2, quantity: 4}
|
|
477
|
+
const cargo = [stackA1, stackA2, stackB] as unknown as Parameters<typeof splitJobCargo>[0]
|
|
478
|
+
const {output, inputs} = splitJobCargo(cargo, false, 1)
|
|
479
|
+
expect(output).toBeNull()
|
|
480
|
+
expect(inputs).toEqual([stackA1, stackA2, stackB] as never)
|
|
481
|
+
})
|
|
415
482
|
})
|
|
416
483
|
|
|
417
484
|
describe('workshopAvailability', () => {
|
|
@@ -446,3 +513,106 @@ describe('workshopAvailability', () => {
|
|
|
446
513
|
expect(workshopAvailability(jobs, 1, now)).toEqual({open: 1, nextOpeningAt: null})
|
|
447
514
|
})
|
|
448
515
|
})
|
|
516
|
+
|
|
517
|
+
describe('hostedDropoff', () => {
|
|
518
|
+
function hostWithDeposit(
|
|
519
|
+
entityType: string,
|
|
520
|
+
building: number,
|
|
521
|
+
holdId: number,
|
|
522
|
+
owner = 'nex.shipload'
|
|
523
|
+
): ScheduleData & {owner: string} {
|
|
524
|
+
return {
|
|
525
|
+
owner,
|
|
526
|
+
lanes: [
|
|
527
|
+
ServerContract.Types.lane.from({
|
|
528
|
+
lane_key: UInt8.from(1),
|
|
529
|
+
schedule: {
|
|
530
|
+
started: '2026-07-26T09:00:00.000',
|
|
531
|
+
tasks: [
|
|
532
|
+
{
|
|
533
|
+
type: UInt8.from(21),
|
|
534
|
+
duration: UInt32.from(600),
|
|
535
|
+
cancelable: UInt8.from(2),
|
|
536
|
+
cargo: WITH_MODULES,
|
|
537
|
+
couplings: [
|
|
538
|
+
{
|
|
539
|
+
counterpart: {entity_type: 'ship', entity_id: 5},
|
|
540
|
+
hold: holdId,
|
|
541
|
+
kind: HoldKind.PULL,
|
|
542
|
+
},
|
|
543
|
+
],
|
|
544
|
+
subject: {entity_type: entityType, entity_id: building},
|
|
545
|
+
},
|
|
546
|
+
],
|
|
547
|
+
},
|
|
548
|
+
}),
|
|
549
|
+
],
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function sourceWithHold(hostId: number, hostType: string, holdId: number): ScheduleData {
|
|
554
|
+
return {
|
|
555
|
+
holds: [
|
|
556
|
+
ServerContract.Types.hold.from({
|
|
557
|
+
id: holdId,
|
|
558
|
+
kind: HoldKind.PULL,
|
|
559
|
+
counterpart: {entity_type: hostType, entity_id: hostId},
|
|
560
|
+
until: '2026-07-26T09:50:00.000',
|
|
561
|
+
incoming_mass: 0,
|
|
562
|
+
}),
|
|
563
|
+
],
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const job = {
|
|
568
|
+
building: 42,
|
|
569
|
+
inputs: WITH_MODULES,
|
|
570
|
+
startsAt: at('2026-07-26T10:00:00Z'),
|
|
571
|
+
completesAt: at('2026-07-26T11:00:00Z'),
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
it('finds the carrier hosting a bay-hosted Drop-off and flags it civic', () => {
|
|
575
|
+
const host = hostWithDeposit('depot', 42, 3, 'nex.shipload')
|
|
576
|
+
const source = sourceWithHold(90000, 'depot', 3)
|
|
577
|
+
const lookup = (id: string) => (id === '90000' ? host : undefined)
|
|
578
|
+
const result = hostedDropoff(source, lookup, job, 'nex.shipload')
|
|
579
|
+
expect(result).toMatchObject({hostId: 90000, laneKey: 1, taskIndex: 0, civic: true})
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
it('does not flag a player-owned Depot civic', () => {
|
|
583
|
+
const host = hostWithDeposit('depot', 42, 3, 'alice')
|
|
584
|
+
const source = sourceWithHold(90000, 'depot', 3)
|
|
585
|
+
const lookup = (id: string) => (id === '90000' ? host : undefined)
|
|
586
|
+
const result = hostedDropoff(source, lookup, job, 'nex.shipload')
|
|
587
|
+
expect(result).toMatchObject({hostId: 90000, laneKey: 1, taskIndex: 0, civic: false})
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
it('finds a third-party carrier and does not flag it civic', () => {
|
|
591
|
+
const host = hostWithDeposit('ship', 42, 3, 'alice')
|
|
592
|
+
const source = sourceWithHold(8, 'ship', 3)
|
|
593
|
+
const lookup = (id: string) => (id === '8' ? host : undefined)
|
|
594
|
+
const result = hostedDropoff(source, lookup, job, 'nex.shipload')
|
|
595
|
+
expect(result).toMatchObject({hostId: 8, laneKey: 1, taskIndex: 0, civic: false})
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
it('returns undefined when no PULL hold names a matching host task', () => {
|
|
599
|
+
expect(hostedDropoff({holds: []}, () => undefined, job)).toBeUndefined()
|
|
600
|
+
const host = hostWithDeposit('depot', 99, 3)
|
|
601
|
+
const source = sourceWithHold(90000, 'depot', 3)
|
|
602
|
+
const lookup = (id: string) => (id === '90000' ? host : undefined)
|
|
603
|
+
expect(hostedDropoff(source, lookup, job)).toBeUndefined()
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
it('does not match a Drop-off whose arrival differs from the job', () => {
|
|
607
|
+
const host = hostWithDeposit('depot', 42, 3, 'nex.shipload')
|
|
608
|
+
const source = sourceWithHold(90000, 'depot', 3)
|
|
609
|
+
const lookup = (id: string) => (id === '90000' ? host : undefined)
|
|
610
|
+
const result = hostedDropoff(
|
|
611
|
+
source,
|
|
612
|
+
lookup,
|
|
613
|
+
{...job, arrivesAt: at('2026-07-26T12:00:00Z')},
|
|
614
|
+
'nex.shipload'
|
|
615
|
+
)
|
|
616
|
+
expect(result).toBeUndefined()
|
|
617
|
+
})
|
|
618
|
+
})
|
package/src/scheduling/jobs.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type {NameType} from '@wharfkit/antelope'
|
|
1
2
|
import type {ServerContract} from '../contracts'
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
3
|
+
import {isCivicEntity} from '../influence/civic'
|
|
4
|
+
import {HoldKind, TaskType} from '../types'
|
|
5
|
+
import {orderedTasks, type OrderedTask, type ScheduleData} from './schedule'
|
|
4
6
|
|
|
5
7
|
type CargoItem = ServerContract.Types.cargo_item
|
|
6
8
|
|
|
@@ -180,13 +182,31 @@ export function jobDropoffTask(
|
|
|
180
182
|
return matches?.length === 1 ? matches[0] : undefined
|
|
181
183
|
}
|
|
182
184
|
|
|
185
|
+
export interface HostedLeg {
|
|
186
|
+
hostId: number
|
|
187
|
+
laneKey: number
|
|
188
|
+
taskIndex: number
|
|
189
|
+
civic: boolean
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface JobRouteOptions {
|
|
193
|
+
hosted?: HostedLeg
|
|
194
|
+
hostedTask?: OrderedTask
|
|
195
|
+
hostLaneLength?: number
|
|
196
|
+
}
|
|
197
|
+
|
|
183
198
|
// Without the ship's schedule an undeposited row reads as Dropping off: the transfer usually starts at once.
|
|
184
199
|
export function jobStatus(
|
|
185
200
|
job: JobStatusInput,
|
|
186
201
|
now: Date,
|
|
187
|
-
tasks?: readonly OrderedTask[]
|
|
202
|
+
tasks?: readonly OrderedTask[],
|
|
203
|
+
options?: JobRouteOptions
|
|
188
204
|
): JobStatus {
|
|
189
205
|
if (job.deposited === false) {
|
|
206
|
+
if (options?.hosted) {
|
|
207
|
+
const start = options.hostedTask?.startsAt
|
|
208
|
+
return start && now < start ? 'booked' : 'dropping'
|
|
209
|
+
}
|
|
190
210
|
const dropoff = jobDropoffTask(job, tasks)
|
|
191
211
|
return dropoff && now < dropoff.startsAt ? 'booked' : 'dropping'
|
|
192
212
|
}
|
|
@@ -231,18 +251,37 @@ export type JobCancelRoute =
|
|
|
231
251
|
| {kind: 'dropoff'; shipId: number; laneKey: number; count: number}
|
|
232
252
|
| {kind: 'craft'; jobId: number}
|
|
233
253
|
| {kind: 'build'; jobId: number}
|
|
254
|
+
| {kind: 'civic'; buildingId: number; laneKey: number; fromId: number}
|
|
234
255
|
|
|
235
256
|
// The ship's cancel pops from the lane tail, so the count reaches from the Drop-off through every task queued behind it.
|
|
236
257
|
function cancelRoute(
|
|
237
258
|
job: JobStatusInput & {id: number; shipId?: number},
|
|
238
259
|
queued: 'craft' | 'build',
|
|
239
260
|
now: Date,
|
|
240
|
-
tasks?: readonly OrderedTask[]
|
|
261
|
+
tasks?: readonly OrderedTask[],
|
|
262
|
+
options?: JobRouteOptions
|
|
241
263
|
): JobCancelRoute | null {
|
|
242
|
-
if (!jobCancellable(jobStatus(job, now, tasks))) return null
|
|
264
|
+
if (!jobCancellable(jobStatus(job, now, tasks, options))) return null
|
|
243
265
|
if (queued === 'craft' && jobCancellationBlockReason(job, tasks)) return null
|
|
244
266
|
if (job.deposited !== false) return {kind: queued, jobId: job.id}
|
|
245
|
-
if (job.shipId === undefined
|
|
267
|
+
if (job.shipId === undefined) return null
|
|
268
|
+
if (options?.hosted) {
|
|
269
|
+
const {hostId, laneKey, taskIndex, civic} = options.hosted
|
|
270
|
+
const laneLength = options.hostLaneLength ?? taskIndex + 1
|
|
271
|
+
if (civic) {
|
|
272
|
+
const isTail = taskIndex === laneLength - 1
|
|
273
|
+
const notCompleted = options.hostedTask ? options.hostedTask.completesAt > now : false
|
|
274
|
+
if (!isTail || !notCompleted) return null
|
|
275
|
+
return {kind: 'civic', buildingId: hostId, laneKey, fromId: job.shipId}
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
kind: 'dropoff',
|
|
279
|
+
shipId: hostId,
|
|
280
|
+
laneKey,
|
|
281
|
+
count: laneLength - taskIndex,
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (!tasks) return null
|
|
246
285
|
const dropoff = jobDropoffTask(job, tasks)
|
|
247
286
|
if (!dropoff || dropoff.completesAt <= now) return null
|
|
248
287
|
const laneLength = tasks.filter((t) => t.laneKey === dropoff.laneKey).length
|
|
@@ -257,18 +296,59 @@ function cancelRoute(
|
|
|
257
296
|
export function jobCancelRoute(
|
|
258
297
|
job: JobStatusInput & {id: number; shipId?: number},
|
|
259
298
|
now: Date,
|
|
260
|
-
tasks?: readonly OrderedTask[]
|
|
299
|
+
tasks?: readonly OrderedTask[],
|
|
300
|
+
options?: JobRouteOptions
|
|
261
301
|
): JobCancelRoute | null {
|
|
262
|
-
return cancelRoute(job, 'craft', now, tasks)
|
|
302
|
+
return cancelRoute(job, 'craft', now, tasks, options)
|
|
263
303
|
}
|
|
264
304
|
|
|
265
305
|
// A Build Job's Drop-off rides on the upgrade target itself, so the target plays the ship's part.
|
|
266
306
|
export function buildJobCancelRoute(
|
|
267
307
|
job: JobStatusInput & {id: number; targetId?: number},
|
|
268
308
|
now: Date,
|
|
269
|
-
tasks?: readonly OrderedTask[]
|
|
309
|
+
tasks?: readonly OrderedTask[],
|
|
310
|
+
options?: JobRouteOptions
|
|
270
311
|
): JobCancelRoute | null {
|
|
271
|
-
return cancelRoute({...job, shipId: job.targetId}, 'build', now, tasks)
|
|
312
|
+
return cancelRoute({...job, shipId: job.targetId}, 'build', now, tasks, options)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export interface HostedDropoffEntity extends ScheduleData {
|
|
316
|
+
owner?: NameType
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// The source entity's HOLD_PULL names the carrier; the carrier's schedule carries the Drop-off task.
|
|
320
|
+
export function hostedDropoff(
|
|
321
|
+
source: ScheduleData,
|
|
322
|
+
lookup: (id: string) => HostedDropoffEntity | undefined,
|
|
323
|
+
job: JobStatusInput,
|
|
324
|
+
civicOwner?: NameType
|
|
325
|
+
): (HostedLeg & {task: OrderedTask}) | undefined {
|
|
326
|
+
for (const hold of source.holds ?? []) {
|
|
327
|
+
if (hold.kind.toNumber() !== HoldKind.PULL) continue
|
|
328
|
+
const host = lookup(hold.counterpart.entity_id.toString())
|
|
329
|
+
if (!host) continue
|
|
330
|
+
for (const entry of orderedTasks(host)) {
|
|
331
|
+
if (Number(entry.task.type) !== TaskType.CIVIC_DEPOSIT) continue
|
|
332
|
+
if (!entry.task.couplings.some((c) => c.hold.equals(hold.id))) continue
|
|
333
|
+
if (!matchesJob(entry, job)) continue
|
|
334
|
+
if (
|
|
335
|
+
job.arrivesAt !== undefined &&
|
|
336
|
+
entry.completesAt.getTime() !== job.arrivesAt.getTime()
|
|
337
|
+
)
|
|
338
|
+
continue
|
|
339
|
+
return {
|
|
340
|
+
hostId: Number(hold.counterpart.entity_id),
|
|
341
|
+
laneKey: entry.laneKey,
|
|
342
|
+
taskIndex: entry.taskIndex,
|
|
343
|
+
civic:
|
|
344
|
+
civicOwner !== undefined &&
|
|
345
|
+
host.owner !== undefined &&
|
|
346
|
+
isCivicEntity({owner: host.owner}, civicOwner),
|
|
347
|
+
task: entry,
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return undefined
|
|
272
352
|
}
|
|
273
353
|
|
|
274
354
|
export interface PickupInFlight {
|
|
@@ -312,15 +392,32 @@ export function jobDeposited(value: unknown): boolean {
|
|
|
312
392
|
return Boolean(value)
|
|
313
393
|
}
|
|
314
394
|
|
|
315
|
-
// A deposited row's
|
|
395
|
+
// A deposited row's cargo is its inputs plus the output tail; an undeposited row carries inputs only.
|
|
316
396
|
export function splitJobCargo<T>(
|
|
317
397
|
cargo: readonly T[],
|
|
318
398
|
deposited: boolean,
|
|
319
399
|
quantity: number
|
|
320
400
|
): {output: T | null; inputs: T[]} {
|
|
321
401
|
if (cargo.length === 0) return {output: null, inputs: []}
|
|
322
|
-
if (
|
|
323
|
-
|
|
402
|
+
if (deposited && quantity > 0)
|
|
403
|
+
return {output: cargo[cargo.length - 1], inputs: cargo.slice(0, -1)}
|
|
404
|
+
return {output: null, inputs: [...cargo]}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export interface BuildJob {
|
|
408
|
+
id: number
|
|
409
|
+
building: number
|
|
410
|
+
socket: number
|
|
411
|
+
targetId: number
|
|
412
|
+
owner: string
|
|
413
|
+
coords: {x: number; y: number}
|
|
414
|
+
startsAt: Date
|
|
415
|
+
completesAt: Date
|
|
416
|
+
arrivesAt: Date
|
|
417
|
+
targetItemId: number
|
|
418
|
+
status: JobStatus
|
|
419
|
+
deposited: boolean
|
|
420
|
+
inputs: CargoItem[]
|
|
324
421
|
}
|
|
325
422
|
|
|
326
423
|
export interface OwnedJob {
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
stackToCargoItem,
|
|
31
31
|
} from '../capabilities/storage'
|
|
32
32
|
import {hydrateEntityLanes} from '../entity/hydrate'
|
|
33
|
-
import {craftCargoOwnership} from './availability'
|
|
33
|
+
import {craftCargoOwnership, isHostedCivicLeg} from './availability'
|
|
34
34
|
import * as schedule from './schedule'
|
|
35
35
|
import type {ScheduleData} from './schedule'
|
|
36
36
|
|
|
@@ -394,6 +394,7 @@ function applyCraftTask(projected: ProjectedEntity, task: ServerContract.Types.t
|
|
|
394
394
|
}
|
|
395
395
|
|
|
396
396
|
function applyTask(projected: ProjectedEntity, task: ServerContract.Types.task): void {
|
|
397
|
+
if (isHostedCivicLeg(task)) return
|
|
397
398
|
switch (task.type.toNumber()) {
|
|
398
399
|
case TaskType.RECHARGE:
|
|
399
400
|
applyRechargeTask(projected, task, {complete: true})
|
|
@@ -32,3 +32,41 @@ export const TASK_CARGO_EFFECTS: ReadonlyMap<number, TaskCargoRule> = new Map<
|
|
|
32
32
|
export function taskCargoRule(taskType: number): TaskCargoRule | undefined {
|
|
33
33
|
return TASK_CARGO_EFFECTS.get(taskType)
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
// Transcribed from TASK_TRAITS in contracts/src/server/include/server/task_traits.hpp.
|
|
37
|
+
export type TaskEnergyEffect = 'none' | 'fills' | 'draws' | 'zeroes'
|
|
38
|
+
|
|
39
|
+
export const TASK_ENERGY_EFFECTS: ReadonlyMap<number, TaskEnergyEffect> = new Map<
|
|
40
|
+
number,
|
|
41
|
+
TaskEnergyEffect
|
|
42
|
+
>([
|
|
43
|
+
[TaskType.IDLE, 'none'],
|
|
44
|
+
[TaskType.TRAVEL, 'draws'],
|
|
45
|
+
[TaskType.RECHARGE, 'fills'],
|
|
46
|
+
[TaskType.LOAD, 'none'],
|
|
47
|
+
[TaskType.UNLOAD, 'none'],
|
|
48
|
+
[TaskType.GATHER, 'draws'],
|
|
49
|
+
[TaskType.WARP, 'zeroes'],
|
|
50
|
+
[TaskType.CRAFT, 'draws'],
|
|
51
|
+
[TaskType.TRANSIT, 'none'],
|
|
52
|
+
[TaskType.UNWRAP, 'none'],
|
|
53
|
+
[TaskType.UNDEPLOY, 'none'],
|
|
54
|
+
[TaskType.LAUNCH, 'none'],
|
|
55
|
+
[TaskType.DEMOLISH, 'none'],
|
|
56
|
+
[TaskType.BUILDPLOT, 'draws'],
|
|
57
|
+
[TaskType.CHARGE, 'draws'],
|
|
58
|
+
[TaskType.UPGRADE, 'draws'],
|
|
59
|
+
[TaskType.SHUTTLE, 'none'],
|
|
60
|
+
[TaskType.CONTRIBUTE, 'none'],
|
|
61
|
+
[TaskType.CIVIC_DEPOSIT, 'draws'],
|
|
62
|
+
[TaskType.CIVIC_WITHDRAW, 'none'],
|
|
63
|
+
])
|
|
64
|
+
|
|
65
|
+
export function taskEnergyEffect(taskType: number): TaskEnergyEffect {
|
|
66
|
+
return TASK_ENERGY_EFFECTS.get(taskType) ?? 'none'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Only TRAVEL is both 'draws' and mobility-trait; WARP's 'zeroes' branch never checks this.
|
|
70
|
+
export function taskEnergyDrawNeedsCoordinates(taskType: number): boolean {
|
|
71
|
+
return taskType === TaskType.TRAVEL
|
|
72
|
+
}
|
|
@@ -5,8 +5,14 @@ import {
|
|
|
5
5
|
unwrapLoadDuration,
|
|
6
6
|
estimateUnwrapDuration,
|
|
7
7
|
incomingHoldMass,
|
|
8
|
+
projectCargomass,
|
|
8
9
|
projectedPeakCargomass,
|
|
9
10
|
} from './unwrap'
|
|
11
|
+
import {TimePoint, UInt8, UInt16, UInt32, UInt64} from '@wharfkit/antelope'
|
|
12
|
+
import {ServerContract} from '../contracts'
|
|
13
|
+
import {getItem} from '../data/catalog'
|
|
14
|
+
import {ITEM_BEAM} from '../data/item-ids'
|
|
15
|
+
import {TaskType} from '../types'
|
|
10
16
|
|
|
11
17
|
describe('unwrap duration mirror', () => {
|
|
12
18
|
test('derivedLoaders aggregates lanes like derived_loaders()', () => {
|
|
@@ -84,3 +90,57 @@ test('projectedPeakCargomass tracks the running peak from cargomass', () => {
|
|
|
84
90
|
// No pending tasks: peak = base + candidate add.
|
|
85
91
|
expect(projectedPeakCargomass(entity, new Date(0), 500)).toBe(1500)
|
|
86
92
|
})
|
|
93
|
+
|
|
94
|
+
const DEPOSIT_START = new Date('2026-09-20T07:48:37.000Z')
|
|
95
|
+
const BEAM_QTY = 2_500
|
|
96
|
+
const BEAM_MASS = getItem(ITEM_BEAM).mass
|
|
97
|
+
|
|
98
|
+
function unloadingShip(projectedAtMs?: number) {
|
|
99
|
+
const cargo = ServerContract.Types.cargo_item.from({
|
|
100
|
+
item_id: UInt16.from(ITEM_BEAM),
|
|
101
|
+
quantity: UInt32.from(BEAM_QTY),
|
|
102
|
+
stats: UInt64.from(0),
|
|
103
|
+
modules: [],
|
|
104
|
+
})
|
|
105
|
+
const lane = ServerContract.Types.lane.from({
|
|
106
|
+
lane_key: UInt8.from(0),
|
|
107
|
+
schedule: ServerContract.Types.schedule.from({
|
|
108
|
+
started: TimePoint.fromMilliseconds(DEPOSIT_START.getTime()),
|
|
109
|
+
tasks: [
|
|
110
|
+
ServerContract.Types.task.from({
|
|
111
|
+
type: UInt8.from(TaskType.CIVIC_DEPOSIT),
|
|
112
|
+
duration: UInt32.from(1_309),
|
|
113
|
+
cancelable: UInt8.from(2),
|
|
114
|
+
cargo: [cargo],
|
|
115
|
+
couplings: [],
|
|
116
|
+
}),
|
|
117
|
+
],
|
|
118
|
+
}),
|
|
119
|
+
})
|
|
120
|
+
return {
|
|
121
|
+
cargomass: BEAM_QTY * BEAM_MASS,
|
|
122
|
+
lanes: [lane],
|
|
123
|
+
holds: [],
|
|
124
|
+
projected_at:
|
|
125
|
+
projectedAtMs === undefined ? undefined : TimePoint.fromMilliseconds(projectedAtMs),
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const UNLOAD_ENDS = new Date(DEPOSIT_START.getTime() + 1_309_000)
|
|
130
|
+
const AFTER_UNLOAD = new Date(UNLOAD_ENDS.getTime() + 60_000)
|
|
131
|
+
|
|
132
|
+
test('projectCargomass reports the mass held at the candidate and the peak around it', () => {
|
|
133
|
+
const plan = projectCargomass(unloadingShip(), AFTER_UNLOAD, 400 * BEAM_MASS)
|
|
134
|
+
// The queued unload empties the hold before the candidate lands.
|
|
135
|
+
expect(plan.used).toBe(400 * BEAM_MASS)
|
|
136
|
+
expect(plan.peak).toBe(BEAM_QTY * BEAM_MASS)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('a task already applied to a projected row is not replayed', () => {
|
|
140
|
+
const applied = unloadingShip(UNLOAD_ENDS.getTime())
|
|
141
|
+
applied.cargomass = 0
|
|
142
|
+
const plan = projectCargomass(applied, AFTER_UNLOAD, 400 * BEAM_MASS)
|
|
143
|
+
expect(plan.used).toBe(400 * BEAM_MASS)
|
|
144
|
+
expect(plan.peak).toBe(400 * BEAM_MASS)
|
|
145
|
+
expect(projectedPeakCargomass(applied, AFTER_UNLOAD, 400 * BEAM_MASS)).toBe(plan.peak)
|
|
146
|
+
})
|
package/src/scheduling/unwrap.ts
CHANGED
|
@@ -133,28 +133,44 @@ function cargoListMass(items: CargoItem[]): number {
|
|
|
133
133
|
return m
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
export
|
|
137
|
-
|
|
136
|
+
export interface CargomassProjection {
|
|
137
|
+
used: number
|
|
138
|
+
peak: number
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function projectCargomass(
|
|
142
|
+
entity: sched.AnchoredScheduleData & {cargomass: number | {toNumber(): number}},
|
|
138
143
|
at: Date,
|
|
139
144
|
addMass: number,
|
|
140
145
|
removeMass = 0
|
|
141
|
-
):
|
|
142
|
-
const events: {t: number; delta: number}[] = []
|
|
143
|
-
for (const ordered of sched.
|
|
146
|
+
): CargomassProjection {
|
|
147
|
+
const events: {t: number; delta: number; candidate: boolean}[] = []
|
|
148
|
+
for (const ordered of sched.unappliedTasks(entity)) {
|
|
144
149
|
const eff = taskCargoEffect(ordered.task)
|
|
145
150
|
const delta = cargoListMass(eff.added) - cargoListMass(eff.removed)
|
|
146
|
-
events.push({t: ordered.completesAt.getTime(), delta})
|
|
151
|
+
events.push({t: ordered.completesAt.getTime(), delta, candidate: false})
|
|
147
152
|
}
|
|
148
|
-
events.push({t: at.getTime(), delta: addMass - removeMass})
|
|
153
|
+
events.push({t: at.getTime(), delta: addMass - removeMass, candidate: true})
|
|
149
154
|
events.sort((a, b) => (a.t !== b.t ? a.t - b.t : b.delta - a.delta))
|
|
150
155
|
let running = Number(entity.cargomass)
|
|
151
156
|
let peak = running
|
|
157
|
+
let used = running
|
|
152
158
|
for (const e of events) {
|
|
153
159
|
running += e.delta
|
|
154
160
|
if (running < 0) running = 0
|
|
155
161
|
if (running > peak) peak = running
|
|
162
|
+
if (e.candidate) used = running
|
|
156
163
|
}
|
|
157
|
-
return Math.min(peak, 0xffff_ffff)
|
|
164
|
+
return {used: Math.min(used, 0xffff_ffff), peak: Math.min(peak, 0xffff_ffff)}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function projectedPeakCargomass(
|
|
168
|
+
entity: sched.AnchoredScheduleData & {cargomass: number | {toNumber(): number}},
|
|
169
|
+
at: Date,
|
|
170
|
+
addMass: number,
|
|
171
|
+
removeMass = 0
|
|
172
|
+
): number {
|
|
173
|
+
return projectCargomass(entity, at, addMass, removeMass).peak
|
|
158
174
|
}
|
|
159
175
|
|
|
160
176
|
export function receiveFits(
|
package/src/shipload.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {NftManager} from './managers/nft'
|
|
|
15
15
|
import type {BalancesManager} from './managers/balances'
|
|
16
16
|
import type {FundManager} from './managers/fund'
|
|
17
17
|
import type {JobsManager} from './managers/jobs'
|
|
18
|
+
import type {ShuttleManager} from './managers/shuttle'
|
|
18
19
|
import type {InfluenceManager} from './managers/influence'
|
|
19
20
|
import type {SubscriptionsManager} from './subscriptions/manager'
|
|
20
21
|
import type {GameState} from './entities/gamestate'
|
|
@@ -23,6 +24,9 @@ interface ShiploadOptions {
|
|
|
23
24
|
platformContractName?: string
|
|
24
25
|
serverContractName?: string
|
|
25
26
|
fundContractName?: string
|
|
27
|
+
platformContractAccount?: string
|
|
28
|
+
serverContractAccount?: string
|
|
29
|
+
fundContractAccount?: string
|
|
26
30
|
client?: APIClient
|
|
27
31
|
subscriptionsUrl?: string
|
|
28
32
|
atomicAssetsAccount?: string
|
|
@@ -43,13 +47,24 @@ export class Shipload {
|
|
|
43
47
|
|
|
44
48
|
const platform = platformContract
|
|
45
49
|
? platformContract
|
|
46
|
-
: new PlatformContract.Contract({
|
|
50
|
+
: new PlatformContract.Contract({
|
|
51
|
+
client: apiClient,
|
|
52
|
+
account: constructorOptions?.platformContractAccount,
|
|
53
|
+
})
|
|
47
54
|
|
|
48
55
|
const server = serverContract
|
|
49
56
|
? serverContract
|
|
50
|
-
: new ServerContract.Contract({
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
: new ServerContract.Contract({
|
|
58
|
+
client: apiClient,
|
|
59
|
+
account: constructorOptions?.serverContractAccount,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const fund = fundContract
|
|
63
|
+
? fundContract
|
|
64
|
+
: new FundContract.Contract({
|
|
65
|
+
client: apiClient,
|
|
66
|
+
account: constructorOptions?.fundContractAccount,
|
|
67
|
+
})
|
|
53
68
|
|
|
54
69
|
this._context = new GameContext(
|
|
55
70
|
apiClient,
|
|
@@ -167,6 +182,10 @@ export class Shipload {
|
|
|
167
182
|
return this._context.jobs
|
|
168
183
|
}
|
|
169
184
|
|
|
185
|
+
get shuttle(): ShuttleManager {
|
|
186
|
+
return this._context.shuttle
|
|
187
|
+
}
|
|
188
|
+
|
|
170
189
|
get influence(): InfluenceManager {
|
|
171
190
|
return this._context.influence
|
|
172
191
|
}
|