@shipload/sdk 1.0.0-next.26 → 1.0.0-next.27
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 +193 -85
- package/lib/shipload.js +775 -378
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +764 -377
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +13 -8
- package/lib/testing.js +38 -23
- package/lib/testing.js.map +1 -1
- package/lib/testing.m.js +38 -23
- 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/entities/entity.ts +1 -1
- package/src/entities/makers.ts +14 -5
- package/src/index-module.ts +24 -4
- package/src/managers/actions.ts +10 -1
- package/src/managers/construction.ts +67 -65
- 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
|
@@ -4,6 +4,7 @@ import type {ServerContract} from '../contracts'
|
|
|
4
4
|
import {PlotManager} from './plot'
|
|
5
5
|
import {getItem} from '../data/catalog'
|
|
6
6
|
import {calc_craft_duration} from '../capabilities/crafting'
|
|
7
|
+
import {getLanes, getTasks} from '../scheduling/schedule'
|
|
7
8
|
import {TaskType} from '../types'
|
|
8
9
|
import type {
|
|
9
10
|
BuildableTarget,
|
|
@@ -94,43 +95,43 @@ export class ConstructionManager extends BaseManager {
|
|
|
94
95
|
const buckets = new Map<string, Map<string, InboundTransfer>>()
|
|
95
96
|
const nowMs = now.getTime()
|
|
96
97
|
for (const entity of entities) {
|
|
97
|
-
const schedule = entity.schedule
|
|
98
|
-
if (!schedule) continue
|
|
99
98
|
const entityIdStr = entity.id.toString()
|
|
100
99
|
const sourceName = entity.entity_name || entityIdStr
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
existing
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
100
|
+
for (const lane of getLanes(entity)) {
|
|
101
|
+
const startedMs = lane.schedule.started.toDate().getTime()
|
|
102
|
+
let cumulativeSec = 0
|
|
103
|
+
for (const task of lane.schedule.tasks) {
|
|
104
|
+
cumulativeSec += task.duration.toNumber()
|
|
105
|
+
if (!isTransferTask(task)) continue
|
|
106
|
+
if (!task.entitytarget) continue
|
|
107
|
+
const projectedEndMs = startedMs + cumulativeSec * 1000
|
|
108
|
+
if (projectedEndMs < nowMs) continue
|
|
109
|
+
const targetIdStr = task.entitytarget.entity_id.toString()
|
|
110
|
+
const etaSeconds = Math.max(0, Math.round((projectedEndMs - nowMs) / 1000))
|
|
111
|
+
let perTarget = buckets.get(targetIdStr)
|
|
112
|
+
if (!perTarget) {
|
|
113
|
+
perTarget = new Map()
|
|
114
|
+
buckets.set(targetIdStr, perTarget)
|
|
115
|
+
}
|
|
116
|
+
for (const c of task.cargo) {
|
|
117
|
+
const itemId = c.item_id.toNumber()
|
|
118
|
+
const quantity = c.quantity.toNumber()
|
|
119
|
+
if (quantity === 0) continue
|
|
120
|
+
const key = `${entityIdStr}#${itemId}`
|
|
121
|
+
const existing = perTarget.get(key)
|
|
122
|
+
if (existing) {
|
|
123
|
+
existing.quantity += quantity
|
|
124
|
+
existing.etaSeconds = Math.min(existing.etaSeconds, etaSeconds)
|
|
125
|
+
} else {
|
|
126
|
+
perTarget.set(key, {
|
|
127
|
+
sourceEntityId: entity.id,
|
|
128
|
+
sourceEntityType: entity.type,
|
|
129
|
+
sourceName,
|
|
130
|
+
itemId,
|
|
131
|
+
quantity,
|
|
132
|
+
etaSeconds,
|
|
133
|
+
})
|
|
134
|
+
}
|
|
134
135
|
}
|
|
135
136
|
}
|
|
136
137
|
}
|
|
@@ -152,25 +153,24 @@ export class ConstructionManager extends BaseManager {
|
|
|
152
153
|
completesAt: number
|
|
153
154
|
hasStarted: boolean
|
|
154
155
|
} | null {
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
hasStarted: startsAt <= now.getTime(),
|
|
156
|
+
for (const lane of getLanes(plot)) {
|
|
157
|
+
const startedMs = lane.schedule.started.toDate().getTime()
|
|
158
|
+
let startSec = 0
|
|
159
|
+
for (const task of lane.schedule.tasks) {
|
|
160
|
+
if (task.type.toNumber() === TaskType.RESERVED) {
|
|
161
|
+
if (!task.entitytarget) return null
|
|
162
|
+
const startsAt = startedMs + startSec * 1000
|
|
163
|
+
const completesAt = startsAt + task.duration.toNumber() * 1000
|
|
164
|
+
return {
|
|
165
|
+
builderId: task.entitytarget.entity_id,
|
|
166
|
+
group: task.entitygroup ?? undefined,
|
|
167
|
+
startsAt,
|
|
168
|
+
completesAt,
|
|
169
|
+
hasStarted: startsAt <= now.getTime(),
|
|
170
|
+
}
|
|
171
171
|
}
|
|
172
|
+
startSec += task.duration.toNumber()
|
|
172
173
|
}
|
|
173
|
-
startSec += task.duration.toNumber()
|
|
174
174
|
}
|
|
175
175
|
return null
|
|
176
176
|
}
|
|
@@ -179,19 +179,22 @@ export class ConstructionManager extends BaseManager {
|
|
|
179
179
|
builder: ServerContract.Types.entity_info | undefined,
|
|
180
180
|
group: UInt64 | undefined
|
|
181
181
|
): {cancelable: boolean; blockingTaskCount: number} {
|
|
182
|
-
if (!builder
|
|
182
|
+
if (!builder || group === undefined) {
|
|
183
183
|
return {cancelable: false, blockingTaskCount: 0}
|
|
184
184
|
}
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
(
|
|
188
|
-
t
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
185
|
+
for (const lane of getLanes(builder)) {
|
|
186
|
+
const tasks = lane.schedule.tasks
|
|
187
|
+
const buildIdx = tasks.findIndex(
|
|
188
|
+
(t) =>
|
|
189
|
+
t.type.toNumber() === TaskType.BUILDPLOT &&
|
|
190
|
+
t.entitygroup !== undefined &&
|
|
191
|
+
t.entitygroup.equals(group)
|
|
192
|
+
)
|
|
193
|
+
if (buildIdx < 0) continue
|
|
194
|
+
const trailing = tasks.length - 1 - buildIdx
|
|
195
|
+
return {cancelable: trailing === 0, blockingTaskCount: trailing}
|
|
196
|
+
}
|
|
197
|
+
return {cancelable: false, blockingTaskCount: 0}
|
|
195
198
|
}
|
|
196
199
|
|
|
197
200
|
private buildFromReservation(
|
|
@@ -357,9 +360,8 @@ function isTransferTask(task: ServerContract.Types.task): boolean {
|
|
|
357
360
|
}
|
|
358
361
|
|
|
359
362
|
function reservationsOf(source: ServerContract.Types.entity_info): Reservation[] {
|
|
360
|
-
if (!source.schedule) return []
|
|
361
363
|
const out = new Map<string, Reservation>()
|
|
362
|
-
for (const task of source
|
|
364
|
+
for (const task of getTasks(source)) {
|
|
363
365
|
if (!isTransferTask(task)) continue
|
|
364
366
|
if (!task.entitytarget) continue
|
|
365
367
|
const targetType = task.entitytarget.entity_type
|
|
@@ -1,86 +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
|
|
73
112
|
}
|
|
74
113
|
|
|
75
114
|
currentTaskProgressFloat(now: Date): number {
|
|
76
|
-
return
|
|
115
|
+
return this.lane ? core.currentTaskProgressFloatForLane(this.lane.schedule, now) : 0
|
|
77
116
|
}
|
|
78
117
|
|
|
79
118
|
progress(now: Date): number {
|
|
80
|
-
return
|
|
119
|
+
return this.lane ? core.laneProgress(this.lane.schedule, now) : 0
|
|
81
120
|
}
|
|
82
121
|
}
|
|
83
122
|
|
|
84
|
-
export function createScheduleAccessor(
|
|
85
|
-
|
|
123
|
+
export function createScheduleAccessor(
|
|
124
|
+
entity: ScheduleData,
|
|
125
|
+
laneKey: number = LANE_MOBILITY
|
|
126
|
+
): ScheduleAccessor {
|
|
127
|
+
return new ScheduleAccessor(entity, laneKey)
|
|
86
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
|
+
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
|
+
}
|