@shipload/sdk 1.0.0-next.26 → 1.0.0-next.28
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 +201 -86
- package/lib/shipload.js +843 -425
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +825 -423
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +13 -8
- package/lib/testing.js +41 -26
- package/lib/testing.js.map +1 -1
- package/lib/testing.m.js +41 -26
- package/lib/testing.m.js.map +1 -1
- package/package.json +1 -1
- package/src/capabilities/craftable.ts +51 -0
- package/src/contracts/server.ts +39 -18
- package/src/data/capabilities.ts +5 -0
- package/src/data/colors.ts +1 -1
- package/src/data/item-ids.ts +1 -1
- package/src/data/metadata.ts +3 -3
- package/src/data/recipes.json +10 -10
- package/src/derivation/capabilities.ts +11 -11
- package/src/derivation/index.ts +9 -0
- package/src/derivation/stars.test.ts +51 -0
- package/src/derivation/stars.ts +15 -0
- package/src/derivation/stats.ts +5 -4
- package/src/entities/entity.ts +1 -1
- package/src/entities/makers.ts +15 -6
- package/src/index-module.ts +33 -4
- package/src/managers/actions.ts +10 -1
- package/src/managers/construction.ts +67 -65
- package/src/nft/buildImmutableData.ts +13 -11
- package/src/nft/description.ts +2 -2
- package/src/resolution/resolve-item.ts +2 -2
- package/src/scheduling/accessor.ts +65 -23
- package/src/scheduling/availability.ts +108 -0
- package/src/scheduling/energy.ts +18 -11
- package/src/scheduling/lane-core.ts +130 -0
- package/src/scheduling/lanes.ts +60 -0
- package/src/scheduling/projection.ts +30 -54
- package/src/scheduling/schedule.ts +236 -121
- package/src/travel/travel.ts +21 -16
- package/src/types/capabilities.ts +1 -0
|
@@ -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
|
+
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
|
+
}
|
package/src/scheduling/energy.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {TaskType} from '../types'
|
|
2
2
|
import {createProjectedEntity, type Projectable} from './projection'
|
|
3
|
-
import {
|
|
3
|
+
import {orderedTasks} from './schedule'
|
|
4
4
|
|
|
5
5
|
export function energyAtTime(entity: Projectable, now: Date): number {
|
|
6
6
|
const projected = createProjectedEntity(entity)
|
|
@@ -13,24 +13,31 @@ export function energyAtTime(entity: Projectable, now: Date): number {
|
|
|
13
13
|
|
|
14
14
|
let running = Number(projected.energy)
|
|
15
15
|
|
|
16
|
-
const
|
|
17
|
-
if (
|
|
16
|
+
const ordered = orderedTasks(entity)
|
|
17
|
+
if (ordered.length === 0) return clamp(running)
|
|
18
18
|
|
|
19
|
-
const
|
|
20
|
-
const activeProgress = currentTaskProgressFloat(entity, now)
|
|
19
|
+
const nowMs = now.getTime()
|
|
21
20
|
|
|
22
|
-
for (
|
|
23
|
-
const
|
|
24
|
-
|
|
21
|
+
for (const {task, startsAt} of ordered) {
|
|
22
|
+
const duration = task.duration.toNumber()
|
|
23
|
+
const isReserved = task.type.toNumber() === TaskType.RESERVED
|
|
24
|
+
const elapsed = Math.min(
|
|
25
|
+
Math.max(0, Math.floor((nowMs - startsAt.getTime()) / 1000)),
|
|
26
|
+
duration
|
|
27
|
+
)
|
|
28
|
+
const complete = !isReserved && elapsed >= duration
|
|
29
|
+
const inProgress = !complete && elapsed > 0 && elapsed < duration
|
|
25
30
|
|
|
26
|
-
|
|
31
|
+
if (!complete && !inProgress) continue
|
|
27
32
|
|
|
28
|
-
|
|
33
|
+
const fraction = complete ? 1 : duration === 0 ? 1 : elapsed / duration
|
|
34
|
+
|
|
35
|
+
if (task.type.toNumber() === TaskType.RECHARGE) {
|
|
29
36
|
if (capacity !== undefined) {
|
|
30
37
|
running = complete ? capacity : running + (capacity - running) * fraction
|
|
31
38
|
}
|
|
32
39
|
} else {
|
|
33
|
-
const cost = Number(
|
|
40
|
+
const cost = Number(task.energy_cost ?? 0)
|
|
34
41
|
running -= cost * fraction
|
|
35
42
|
}
|
|
36
43
|
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type {ServerContract} from '../contracts'
|
|
2
|
+
import {TaskType} from '../types'
|
|
3
|
+
|
|
4
|
+
type Schedule = ServerContract.Types.schedule
|
|
5
|
+
type Task = ServerContract.Types.task
|
|
6
|
+
|
|
7
|
+
export function laneDuration(schedule: Schedule): number {
|
|
8
|
+
return schedule.tasks.reduce((sum, task) => sum + task.duration.toNumber(), 0)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function laneRawElapsed(schedule: Schedule, now: Date): number {
|
|
12
|
+
const started = schedule.started.toDate()
|
|
13
|
+
return Math.floor((now.getTime() - started.getTime()) / 1000)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function laneElapsed(schedule: Schedule, now: Date): number {
|
|
17
|
+
return Math.max(0, laneRawElapsed(schedule, now))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function laneStartsIn(schedule: Schedule, now: Date): number {
|
|
21
|
+
return Math.max(0, -laneRawElapsed(schedule, now))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function laneRemaining(schedule: Schedule, now: Date): number {
|
|
25
|
+
return Math.max(0, laneDuration(schedule) - laneRawElapsed(schedule, now))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function laneComplete(schedule: Schedule, now: Date): boolean {
|
|
29
|
+
if (schedule.tasks.length === 0) return false
|
|
30
|
+
if (schedule.tasks.some((t) => t.type.toNumber() === TaskType.RESERVED)) return false
|
|
31
|
+
return laneRemaining(schedule, now) === 0
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function laneProgress(schedule: Schedule, now: Date): number {
|
|
35
|
+
const duration = laneDuration(schedule)
|
|
36
|
+
if (duration === 0) return schedule.tasks.length > 0 ? 1 : 0
|
|
37
|
+
return Math.min(1, laneElapsed(schedule, now) / duration)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function currentTaskIndexForLane(schedule: Schedule, now: Date): number {
|
|
41
|
+
if (schedule.tasks.length === 0) return -1
|
|
42
|
+
if (laneRawElapsed(schedule, now) < 0) return -1
|
|
43
|
+
const elapsed = laneElapsed(schedule, now)
|
|
44
|
+
let timeAccum = 0
|
|
45
|
+
for (let i = 0; i < schedule.tasks.length; i++) {
|
|
46
|
+
const taskDuration = schedule.tasks[i].duration.toNumber()
|
|
47
|
+
if (elapsed < timeAccum + taskDuration) return i
|
|
48
|
+
timeAccum += taskDuration
|
|
49
|
+
}
|
|
50
|
+
return -1
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function currentTask(schedule: Schedule, now: Date): Task | undefined {
|
|
54
|
+
const index = currentTaskIndexForLane(schedule, now)
|
|
55
|
+
if (index < 0) return undefined
|
|
56
|
+
return schedule.tasks[index]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function currentTaskType(schedule: Schedule, now: Date): TaskType | undefined {
|
|
60
|
+
const task = currentTask(schedule, now)
|
|
61
|
+
return task ? (task.type.toNumber() as TaskType) : undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function laneTaskStartTime(schedule: Schedule, index: number): number {
|
|
65
|
+
if (index < 0 || index >= schedule.tasks.length) return 0
|
|
66
|
+
let timeAccum = 0
|
|
67
|
+
for (let i = 0; i < index; i++) {
|
|
68
|
+
timeAccum += schedule.tasks[i].duration.toNumber()
|
|
69
|
+
}
|
|
70
|
+
return timeAccum
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function laneTaskElapsed(schedule: Schedule, index: number, now: Date): number {
|
|
74
|
+
if (index < 0 || index >= schedule.tasks.length) return 0
|
|
75
|
+
const elapsed = laneElapsed(schedule, now)
|
|
76
|
+
const taskStart = laneTaskStartTime(schedule, index)
|
|
77
|
+
const taskDuration = schedule.tasks[index].duration.toNumber()
|
|
78
|
+
if (elapsed <= taskStart) return 0
|
|
79
|
+
return Math.min(elapsed - taskStart, taskDuration)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function laneTaskRemaining(schedule: Schedule, index: number, now: Date): number {
|
|
83
|
+
if (index < 0 || index >= schedule.tasks.length) return 0
|
|
84
|
+
const taskDuration = schedule.tasks[index].duration.toNumber()
|
|
85
|
+
return Math.max(0, taskDuration - laneTaskElapsed(schedule, index, now))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function laneTaskComplete(schedule: Schedule, index: number, now: Date): boolean {
|
|
89
|
+
if (index < 0 || index >= schedule.tasks.length) return false
|
|
90
|
+
if (schedule.tasks[index].type.toNumber() === TaskType.RESERVED) return false
|
|
91
|
+
const taskDuration = schedule.tasks[index].duration.toNumber()
|
|
92
|
+
return laneTaskElapsed(schedule, index, now) >= taskDuration
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function laneTaskInProgress(schedule: Schedule, index: number, now: Date): boolean {
|
|
96
|
+
if (index < 0 || index >= schedule.tasks.length) return false
|
|
97
|
+
const taskElapsed = laneTaskElapsed(schedule, index, now)
|
|
98
|
+
const taskDuration = schedule.tasks[index].duration.toNumber()
|
|
99
|
+
return taskElapsed > 0 && taskElapsed < taskDuration
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function laneCompletesAt(schedule: Schedule, index: number): Date {
|
|
103
|
+
const startedMs = schedule.started.toDate().getTime()
|
|
104
|
+
const endSec =
|
|
105
|
+
laneTaskStartTime(schedule, index) + (schedule.tasks[index]?.duration.toNumber() ?? 0)
|
|
106
|
+
return new Date(startedMs + endSec * 1000)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function currentTaskProgress(schedule: Schedule, now: Date): number {
|
|
110
|
+
const index = currentTaskIndexForLane(schedule, now)
|
|
111
|
+
if (index < 0) return 0
|
|
112
|
+
const elapsed = laneTaskElapsed(schedule, index, now)
|
|
113
|
+
const duration = schedule.tasks[index].duration.toNumber()
|
|
114
|
+
if (duration === 0) return 1
|
|
115
|
+
return Math.min(1, elapsed / duration)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function currentTaskProgressFloatForLane(schedule: Schedule, now: Date): number {
|
|
119
|
+
if (schedule.tasks.length === 0) return 0
|
|
120
|
+
const index = currentTaskIndexForLane(schedule, now)
|
|
121
|
+
if (index < 0) return 0
|
|
122
|
+
const task = schedule.tasks[index]
|
|
123
|
+
const durationMs = task.duration.toNumber() * 1000
|
|
124
|
+
if (durationMs === 0) return 1
|
|
125
|
+
const startedMs = schedule.started.toDate().getTime()
|
|
126
|
+
const taskStartMs = startedMs + laneTaskStartTime(schedule, index) * 1000
|
|
127
|
+
const elapsedMs = now.getTime() - taskStartMs
|
|
128
|
+
if (elapsedMs <= 0) return 0
|
|
129
|
+
return Math.min(1, elapsedMs / durationMs)
|
|
130
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type {ServerContract} from '../contracts'
|
|
2
|
+
import {getItem} from '../data/catalog'
|
|
3
|
+
import type {ModuleType} from '../types'
|
|
4
|
+
import {getLane, type ScheduleData} from './schedule'
|
|
5
|
+
|
|
6
|
+
type ModuleEntry = ServerContract.Types.module_entry
|
|
7
|
+
type Lane = ServerContract.Types.lane
|
|
8
|
+
type Schedule = ServerContract.Types.schedule
|
|
9
|
+
|
|
10
|
+
export function laneKeyForModule(slotIndex: number): number {
|
|
11
|
+
return slotIndex + 1
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function laneIsFree(lanes: Lane[], laneKey: number): boolean {
|
|
15
|
+
const lane = lanes.find((entry) => entry.lane_key.toNumber() === laneKey)
|
|
16
|
+
return lane ? lane.schedule.tasks.length === 0 : true
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function workerLaneKey(
|
|
20
|
+
modules: ModuleEntry[],
|
|
21
|
+
moduleSubtype: ModuleType,
|
|
22
|
+
lanes: Lane[]
|
|
23
|
+
): number {
|
|
24
|
+
const occupiedMatchingLaneKeys: number[] = []
|
|
25
|
+
|
|
26
|
+
for (let slotIndex = 0; slotIndex < modules.length; slotIndex++) {
|
|
27
|
+
const installed = modules[slotIndex].installed
|
|
28
|
+
if (!installed) continue
|
|
29
|
+
if (getItem(installed.item_id).moduleType !== moduleSubtype) continue
|
|
30
|
+
|
|
31
|
+
const laneKey = laneKeyForModule(slotIndex)
|
|
32
|
+
if (laneIsFree(lanes, laneKey)) return laneKey
|
|
33
|
+
occupiedMatchingLaneKeys.push(laneKey)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (occupiedMatchingLaneKeys.length > 0) {
|
|
37
|
+
return Math.min(...occupiedMatchingLaneKeys)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
throw new Error(`No installed ${moduleSubtype} worker module`)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function rawScheduleEnd(schedule: Schedule): Date {
|
|
44
|
+
const durationSec = schedule.tasks.reduce((sum, task) => sum + task.duration.toNumber(), 0)
|
|
45
|
+
return new Date(schedule.started.toDate().getTime() + durationSec * 1000)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function candidateLaneCompletesAt(
|
|
49
|
+
entity: ScheduleData,
|
|
50
|
+
laneKey: number,
|
|
51
|
+
durationSec: number,
|
|
52
|
+
now: Date
|
|
53
|
+
): Date {
|
|
54
|
+
const lane = getLane(entity, laneKey)
|
|
55
|
+
const startMs = lane
|
|
56
|
+
? Math.max(rawScheduleEnd(lane.schedule).getTime(), now.getTime())
|
|
57
|
+
: now.getTime()
|
|
58
|
+
|
|
59
|
+
return new Date(startMs + durationSec * 1000)
|
|
60
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {Name,
|
|
1
|
+
import {Name, UInt16, UInt32, UInt64} from '@wharfkit/antelope'
|
|
2
2
|
import {ServerContract} from '../contracts'
|
|
3
3
|
import {Coordinates, TaskType} from '../types'
|
|
4
4
|
import {
|
|
@@ -333,50 +333,27 @@ export interface ProjectionOptions {
|
|
|
333
333
|
|
|
334
334
|
export function projectEntity(entity: Projectable, options?: ProjectionOptions): ProjectedEntity {
|
|
335
335
|
const projected = createProjectedEntity(entity)
|
|
336
|
-
|
|
336
|
+
const ordered = schedule.orderedTasks(entity)
|
|
337
|
+
if (ordered.length === 0) return projected
|
|
337
338
|
|
|
338
|
-
const tasks = entity.schedule.tasks
|
|
339
339
|
const taskCount =
|
|
340
340
|
options?.upToTaskIndex !== undefined
|
|
341
|
-
? Math.max(0, Math.min(options.upToTaskIndex,
|
|
342
|
-
:
|
|
341
|
+
? Math.max(0, Math.min(options.upToTaskIndex, ordered.length))
|
|
342
|
+
: ordered.length
|
|
343
343
|
|
|
344
344
|
for (let i = 0; i < taskCount; i++) {
|
|
345
|
-
applyTask(projected,
|
|
345
|
+
applyTask(projected, ordered[i].task)
|
|
346
346
|
}
|
|
347
347
|
return projected
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
-
export
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function buildRemainingProjectable(snapshot: ProjectableSnapshot): Projectable | null {
|
|
356
|
-
if (!snapshot.schedule) return null
|
|
357
|
-
const remainingTasks: ServerContract.Types.task[] = []
|
|
358
|
-
if (snapshot.current_task) remainingTasks.push(snapshot.current_task)
|
|
359
|
-
if (snapshot.pending_tasks?.length) remainingTasks.push(...snapshot.pending_tasks)
|
|
360
|
-
if (remainingTasks.length === 0) return null
|
|
361
|
-
|
|
362
|
-
const completedCount = snapshot.schedule.tasks.length - remainingTasks.length
|
|
363
|
-
let startedMs = snapshot.schedule.started.toMilliseconds()
|
|
364
|
-
for (let i = 0; i < completedCount; i++) {
|
|
365
|
-
startedMs += snapshot.schedule.tasks[i].duration.toNumber() * 1000
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
return {
|
|
369
|
-
...snapshot,
|
|
370
|
-
schedule: ServerContract.Types.schedule.from({
|
|
371
|
-
started: TimePoint.fromMilliseconds(startedMs),
|
|
372
|
-
tasks: remainingTasks,
|
|
373
|
-
}),
|
|
350
|
+
export function projectRemainingAt(entity: Projectable, _now: Date): ProjectedEntity {
|
|
351
|
+
// Resolve is lazy/entity-global: completed tasks are unsettled until resolve, so replay all.
|
|
352
|
+
const projected = createProjectedEntity(entity)
|
|
353
|
+
for (const {task} of schedule.orderedTasks(entity)) {
|
|
354
|
+
applyTask(projected, task)
|
|
374
355
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
export function projectFromCurrentState(snapshot: ProjectableSnapshot): ProjectedEntity {
|
|
378
|
-
const projectable = buildRemainingProjectable(snapshot)
|
|
379
|
-
return projectable ? projectEntity(projectable) : createProjectedEntity(snapshot)
|
|
356
|
+
return projected
|
|
380
357
|
}
|
|
381
358
|
|
|
382
359
|
function getRecipeInputsForOutput(outputItemId: number): RecipeInput[] | undefined {
|
|
@@ -438,10 +415,11 @@ function validateCraftTask(task: ServerContract.Types.task, projected: Projected
|
|
|
438
415
|
}
|
|
439
416
|
|
|
440
417
|
export function validateSchedule(entity: Projectable): void {
|
|
441
|
-
|
|
418
|
+
const ordered = schedule.orderedTasks(entity)
|
|
419
|
+
if (ordered.length === 0) return
|
|
442
420
|
|
|
443
421
|
const projected = createProjectedEntity(entity)
|
|
444
|
-
for (const task of
|
|
422
|
+
for (const {task} of ordered) {
|
|
445
423
|
if (task.type.toNumber() === TaskType.CRAFT) {
|
|
446
424
|
validateCraftTask(task, projected)
|
|
447
425
|
}
|
|
@@ -455,22 +433,28 @@ export function validateSchedule(entity: Projectable): void {
|
|
|
455
433
|
export function projectEntityAt(entity: Projectable, now: Date): ProjectedEntity {
|
|
456
434
|
const projected = createProjectedEntity(entity)
|
|
457
435
|
|
|
458
|
-
|
|
436
|
+
const ordered = schedule.orderedTasks(entity)
|
|
437
|
+
if (ordered.length === 0) {
|
|
459
438
|
return projected
|
|
460
439
|
}
|
|
461
440
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
const
|
|
441
|
+
const nowMs = now.getTime()
|
|
442
|
+
|
|
443
|
+
for (const {task, startsAt} of ordered) {
|
|
444
|
+
const duration = task.duration.toNumber()
|
|
445
|
+
const isReserved = task.type.toNumber() === TaskType.RESERVED
|
|
446
|
+
const elapsed = Math.min(
|
|
447
|
+
Math.max(0, Math.floor((nowMs - startsAt.getTime()) / 1000)),
|
|
448
|
+
duration
|
|
449
|
+
)
|
|
450
|
+
const taskComplete = !isReserved && elapsed >= duration
|
|
451
|
+
const taskInProgress = elapsed > 0 && elapsed < duration
|
|
466
452
|
|
|
467
453
|
if (!taskComplete && !taskInProgress) {
|
|
468
|
-
|
|
454
|
+
continue
|
|
469
455
|
}
|
|
470
456
|
|
|
471
|
-
const progress = taskInProgress
|
|
472
|
-
? schedule.getTaskElapsed(entity, i, now) / task.duration.toNumber()
|
|
473
|
-
: undefined
|
|
457
|
+
const progress = taskInProgress ? elapsed / duration : undefined
|
|
474
458
|
|
|
475
459
|
switch (task.type.toNumber()) {
|
|
476
460
|
case TaskType.RECHARGE:
|
|
@@ -504,11 +488,3 @@ export function projectEntityAt(entity: Projectable, now: Date): ProjectedEntity
|
|
|
504
488
|
|
|
505
489
|
return projected
|
|
506
490
|
}
|
|
507
|
-
|
|
508
|
-
export function projectFromCurrentStateAt(
|
|
509
|
-
snapshot: ProjectableSnapshot,
|
|
510
|
-
now: Date
|
|
511
|
-
): ProjectedEntity {
|
|
512
|
-
const projectable = buildRemainingProjectable(snapshot)
|
|
513
|
-
return projectable ? projectEntityAt(projectable, now) : createProjectedEntity(snapshot)
|
|
514
|
-
}
|