@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
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Name,
|
|
3
|
+
UInt16,
|
|
4
|
+
UInt32,
|
|
5
|
+
UInt64,
|
|
6
|
+
type NameType,
|
|
7
|
+
type UInt16Type,
|
|
8
|
+
type UInt32Type,
|
|
9
|
+
type UInt64Type,
|
|
10
|
+
} from '@wharfkit/antelope'
|
|
11
|
+
import {BaseManager} from './base'
|
|
12
|
+
import {ServerContract} from '../contracts'
|
|
13
|
+
import {getItem} from '../data/catalog'
|
|
14
|
+
import {ENTITY_DEPOT} from '../data/kind-registry'
|
|
15
|
+
import {isCivicEntity} from '../influence/civic'
|
|
16
|
+
import {projectEntity} from '../scheduling/projection'
|
|
17
|
+
|
|
18
|
+
export type ShuttleMode = 'internal' | 'own' | 'ship' | 'bays'
|
|
19
|
+
|
|
20
|
+
export type ShuttleReasonCode =
|
|
21
|
+
| 'workshop-capped'
|
|
22
|
+
| 'no-generator'
|
|
23
|
+
| 'not-equipped'
|
|
24
|
+
| 'job-cap'
|
|
25
|
+
| 'target-busy'
|
|
26
|
+
| 'cargo-wont-fit'
|
|
27
|
+
| 'depot-full'
|
|
28
|
+
| 'not-stored'
|
|
29
|
+
| 'no-storage'
|
|
30
|
+
| 'bays-booked'
|
|
31
|
+
| 'player-cap'
|
|
32
|
+
| 'queue-full'
|
|
33
|
+
| 'inputs-unavailable'
|
|
34
|
+
| 'ship-energy'
|
|
35
|
+
| 'dropoff-collision'
|
|
36
|
+
| 'ship-capped'
|
|
37
|
+
| 'departs'
|
|
38
|
+
| 'cargo-not-aboard'
|
|
39
|
+
| 'no-capacity'
|
|
40
|
+
| 'schedule-full'
|
|
41
|
+
| 'plot-recipe'
|
|
42
|
+
| 'not-ready'
|
|
43
|
+
| 'unknown'
|
|
44
|
+
|
|
45
|
+
export interface ShuttleReason {
|
|
46
|
+
code: ShuttleReasonCode
|
|
47
|
+
reason: string
|
|
48
|
+
at?: Date
|
|
49
|
+
until?: Date
|
|
50
|
+
have?: number
|
|
51
|
+
need?: number
|
|
52
|
+
cap?: number
|
|
53
|
+
taskType?: number
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ShuttleOption {
|
|
57
|
+
shuttledBy?: string
|
|
58
|
+
mode: ShuttleMode
|
|
59
|
+
hostId: string
|
|
60
|
+
laneKey: number
|
|
61
|
+
duration: number
|
|
62
|
+
resolved: boolean
|
|
63
|
+
start?: Date
|
|
64
|
+
finish?: Date
|
|
65
|
+
bays: number
|
|
66
|
+
window?: {startsAt: Date; completesAt: Date}
|
|
67
|
+
energyCost?: number
|
|
68
|
+
blocked?: ShuttleReason
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ShuttleOptions {
|
|
72
|
+
blocked?: ShuttleReason
|
|
73
|
+
options: ShuttleOption[]
|
|
74
|
+
auto?: ShuttleOption
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// contract strings verbatim from errors.hpp and placement.hpp
|
|
78
|
+
const REASON_CODES: Record<string, ShuttleReasonCode> = {
|
|
79
|
+
'workshop has a pending plan-capper; cannot accept new jobs': 'workshop-capped',
|
|
80
|
+
'ship needs an energy source to book a craft job': 'no-generator',
|
|
81
|
+
'entity cannot recharge': 'no-generator',
|
|
82
|
+
'workshop has no fabricator installed': 'not-equipped',
|
|
83
|
+
'dock has no assembly arm installed': 'not-equipped',
|
|
84
|
+
'too many bookings waiting on materials at this building': 'job-cap',
|
|
85
|
+
'upgrade target is busy': 'target-busy',
|
|
86
|
+
'Entity must be idle to modify modules.': 'target-busy',
|
|
87
|
+
'target cargo would not fit the upgraded capacity': 'cargo-wont-fit',
|
|
88
|
+
'player storage allowance at this depot is exceeded': 'depot-full',
|
|
89
|
+
'player has no such item stored at this depot': 'not-stored',
|
|
90
|
+
'player has fewer of that item stored at this depot': 'not-stored',
|
|
91
|
+
'entity has no storage': 'no-storage',
|
|
92
|
+
'target entity has no storage': 'no-storage',
|
|
93
|
+
"the building's shuttle bays are fully booked": 'bays-booked',
|
|
94
|
+
'player already has the most transfers this building takes': 'player-cap',
|
|
95
|
+
'fabricator queue is full': 'queue-full',
|
|
96
|
+
'Cannot unload cargo that is not loaded.': 'inputs-unavailable',
|
|
97
|
+
'Insufficient inputs for recipe.': 'inputs-unavailable',
|
|
98
|
+
'Cargo debit exceeds available quantity.': 'inputs-unavailable',
|
|
99
|
+
'Craft requires more energy than entity has.': 'ship-energy',
|
|
100
|
+
'identical Workshop delivery already scheduled at this time; finish that delivery or change the shipment':
|
|
101
|
+
'dropoff-collision',
|
|
102
|
+
'cannot append: schedule is capped by a pending plan-capper': 'ship-capped',
|
|
103
|
+
'ship departs before the shuttle completes': 'departs',
|
|
104
|
+
'giver has insufficient giveable cargo': 'cargo-not-aboard',
|
|
105
|
+
'Entity cargo capacity would be exceeded.': 'no-capacity',
|
|
106
|
+
'lane queue is full': 'schedule-full',
|
|
107
|
+
'No recipe found for plot target.': 'plot-recipe',
|
|
108
|
+
'Item is not part of the plot target recipe.': 'plot-recipe',
|
|
109
|
+
'Deposit would exceed plot recipe requirement.': 'plot-recipe',
|
|
110
|
+
'craft job is not finished yet': 'not-ready',
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const BOOKING_LEVEL_CODES: ReadonlySet<ShuttleReasonCode> = new Set([
|
|
114
|
+
'workshop-capped',
|
|
115
|
+
'no-generator',
|
|
116
|
+
'not-equipped',
|
|
117
|
+
'job-cap',
|
|
118
|
+
'target-busy',
|
|
119
|
+
'cargo-wont-fit',
|
|
120
|
+
'depot-full',
|
|
121
|
+
'not-stored',
|
|
122
|
+
'no-storage',
|
|
123
|
+
'not-ready',
|
|
124
|
+
])
|
|
125
|
+
|
|
126
|
+
// never-shuttle rejections from resolve_civic_carrier; the query never omits, so the wrapper drops them here
|
|
127
|
+
export const OMITTED_REASONS: ReadonlySet<string> = new Set([
|
|
128
|
+
'that ship has no shuttle bay',
|
|
129
|
+
'that ship belongs to another player',
|
|
130
|
+
'that ship is not here',
|
|
131
|
+
"only a Depot's public shuttle bays can take this",
|
|
132
|
+
"choose the building's own shuttle instead",
|
|
133
|
+
'counterpart has a pending plan-capper; cannot place a hold on it',
|
|
134
|
+
'entity not found',
|
|
135
|
+
])
|
|
136
|
+
|
|
137
|
+
export function shuttleReasonCode(reason: string): ShuttleReasonCode {
|
|
138
|
+
return REASON_CODES[reason] ?? 'unknown'
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const MODES: ShuttleMode[] = ['internal', 'own', 'ship', 'bays']
|
|
142
|
+
|
|
143
|
+
const modeRank = (o: ShuttleOption) => (o.mode === 'internal' || o.mode === 'own' ? 0 : 1)
|
|
144
|
+
|
|
145
|
+
const finishRank = (o: ShuttleOption) => o.finish?.getTime() ?? Number.POSITIVE_INFINITY
|
|
146
|
+
|
|
147
|
+
export function rankShuttleOptions(a: ShuttleOption, b: ShuttleOption): number {
|
|
148
|
+
const blocked = (o: ShuttleOption) => (o.blocked ? 1 : 0)
|
|
149
|
+
if (blocked(a) !== blocked(b)) return blocked(a) - blocked(b)
|
|
150
|
+
if (finishRank(a) !== finishRank(b)) return finishRank(a) < finishRank(b) ? -1 : 1
|
|
151
|
+
if (modeRank(a) !== modeRank(b)) return modeRank(a) - modeRank(b)
|
|
152
|
+
return Number(BigInt(a.hostId) - BigInt(b.hostId))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
type RawOptions = ServerContract.Types.shuttle_options
|
|
156
|
+
|
|
157
|
+
function toDate(tp: {toDate(): Date} | undefined): Date | undefined {
|
|
158
|
+
return tp ? tp.toDate() : undefined
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function wireTime(tp: {toMilliseconds(): number; toDate(): Date}): Date | undefined {
|
|
162
|
+
return tp.toMilliseconds() === 0 ? undefined : tp.toDate()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function toNum(v: {toNumber(): number} | undefined): number | undefined {
|
|
166
|
+
return v === undefined ? undefined : v.toNumber()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function mapReason(r: ServerContract.Types.shuttle_rejection): ShuttleReason {
|
|
170
|
+
return {
|
|
171
|
+
code: shuttleReasonCode(r.reason),
|
|
172
|
+
reason: r.reason,
|
|
173
|
+
at: toDate(r.at),
|
|
174
|
+
until: toDate(r.until),
|
|
175
|
+
have: toNum(r.have),
|
|
176
|
+
need: toNum(r.need),
|
|
177
|
+
cap: toNum(r.cap),
|
|
178
|
+
taskType: toNum(r.task_type),
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function mapOptions(raw: RawOptions): ShuttleOptions {
|
|
183
|
+
const options: ShuttleOption[] = []
|
|
184
|
+
for (const o of raw.options) {
|
|
185
|
+
const rejection = o.rejection ? mapReason(o.rejection) : undefined
|
|
186
|
+
if (rejection && OMITTED_REASONS.has(rejection.reason)) continue
|
|
187
|
+
const window =
|
|
188
|
+
o.window_starts_at && o.window_completes_at
|
|
189
|
+
? {
|
|
190
|
+
startsAt: toDate(o.window_starts_at)!,
|
|
191
|
+
completesAt: toDate(o.window_completes_at)!,
|
|
192
|
+
}
|
|
193
|
+
: undefined
|
|
194
|
+
const start = wireTime(o.start)
|
|
195
|
+
const finish = wireTime(o.finish)
|
|
196
|
+
options.push({
|
|
197
|
+
shuttledBy: o.shuttled_by ? o.shuttled_by.toString() : undefined,
|
|
198
|
+
mode: MODES[o.mode.toNumber()] ?? 'internal',
|
|
199
|
+
hostId: o.host_id.toString(),
|
|
200
|
+
laneKey: o.lane_key.toNumber(),
|
|
201
|
+
duration: o.duration.toNumber(),
|
|
202
|
+
resolved: o.resolved,
|
|
203
|
+
start,
|
|
204
|
+
finish,
|
|
205
|
+
bays: o.bays.toNumber(),
|
|
206
|
+
window,
|
|
207
|
+
energyCost: toNum(o.energy_cost),
|
|
208
|
+
blocked: rejection,
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
options.sort(rankShuttleOptions)
|
|
212
|
+
const first = options[0]?.blocked
|
|
213
|
+
const blocked =
|
|
214
|
+
first &&
|
|
215
|
+
BOOKING_LEVEL_CODES.has(first.code) &&
|
|
216
|
+
options.every((o) => o.blocked?.code === first.code)
|
|
217
|
+
? first
|
|
218
|
+
: undefined
|
|
219
|
+
const auto = blocked ? undefined : options.find((o) => !o.blocked)
|
|
220
|
+
return {blocked, options, auto}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export type CandidateEntity = Pick<
|
|
224
|
+
ServerContract.Types.entity_row,
|
|
225
|
+
'id' | 'owner' | 'kind' | 'coordinates' | 'modules' | 'item_id' | 'lanes'
|
|
226
|
+
>
|
|
227
|
+
|
|
228
|
+
const SHUTTLE_CANDIDATE_CAP = 8
|
|
229
|
+
|
|
230
|
+
function siteOf(entity: CandidateEntity) {
|
|
231
|
+
return projectEntity({...entity, cargo: [], cargomass: UInt32.from(0)}).location
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function hasLoaderModule(entity: CandidateEntity): boolean {
|
|
235
|
+
return entity.modules.some(
|
|
236
|
+
(m) => m.installed && getItem(m.installed.item_id).moduleType === 'loader'
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function shuttleCandidates(
|
|
241
|
+
req: {building: CandidateEntity; shipId: UInt64Type; player: NameType},
|
|
242
|
+
entities: readonly CandidateEntity[],
|
|
243
|
+
civicOwner: NameType
|
|
244
|
+
): string[] {
|
|
245
|
+
const {building, shipId, player} = req
|
|
246
|
+
const buildingId = UInt64.from(building.id)
|
|
247
|
+
const bookingShipId = UInt64.from(shipId)
|
|
248
|
+
const playerName = Name.from(player)
|
|
249
|
+
const site = siteOf(building)
|
|
250
|
+
|
|
251
|
+
const depots: UInt64[] = []
|
|
252
|
+
const ships: UInt64[] = []
|
|
253
|
+
const seen = new Set<string>()
|
|
254
|
+
|
|
255
|
+
for (const entity of entities) {
|
|
256
|
+
const id = UInt64.from(entity.id)
|
|
257
|
+
if (id.equals(buildingId) || id.equals(bookingShipId)) continue
|
|
258
|
+
if (seen.has(id.toString())) continue
|
|
259
|
+
if (!siteOf(entity).equals(site)) continue
|
|
260
|
+
|
|
261
|
+
if (isCivicEntity(entity, civicOwner)) {
|
|
262
|
+
if (Name.from(entity.kind).equals(ENTITY_DEPOT)) {
|
|
263
|
+
depots.push(id)
|
|
264
|
+
seen.add(id.toString())
|
|
265
|
+
}
|
|
266
|
+
continue
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!Name.from(entity.owner).equals(playerName)) continue
|
|
270
|
+
if (!hasLoaderModule(entity)) continue
|
|
271
|
+
ships.push(id)
|
|
272
|
+
seen.add(id.toString())
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const byId = (a: UInt64, b: UInt64) => Number(BigInt(a.toString()) - BigInt(b.toString()))
|
|
276
|
+
depots.sort(byId)
|
|
277
|
+
ships.sort(byId)
|
|
278
|
+
|
|
279
|
+
return [...depots, ...ships].slice(0, SHUTTLE_CANDIDATE_CAP).map((id) => id.toString())
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function chainAssertion(e: unknown): string | undefined {
|
|
283
|
+
const detailsMessage = (e as {details?: {message?: string}[]}).details?.[0]?.message
|
|
284
|
+
const message = detailsMessage ?? (e instanceof Error ? e.message : String(e))
|
|
285
|
+
const match = message.match(/assertion failure with message:\s*(.*)/i)
|
|
286
|
+
return match ? match[1].trim() : undefined
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export class ShuttleManager extends BaseManager {
|
|
290
|
+
private async query(
|
|
291
|
+
name:
|
|
292
|
+
| 'getcraftopts'
|
|
293
|
+
| 'getbuildopts'
|
|
294
|
+
| 'getclaimopts'
|
|
295
|
+
| 'getcbldopts'
|
|
296
|
+
| 'getstoreopts'
|
|
297
|
+
| 'gettakeopts',
|
|
298
|
+
data: unknown
|
|
299
|
+
): Promise<ShuttleOptions> {
|
|
300
|
+
let raw: RawOptions
|
|
301
|
+
try {
|
|
302
|
+
raw = (await this.server.readonly(name as never, data as never)) as RawOptions
|
|
303
|
+
} catch (e) {
|
|
304
|
+
const reason = chainAssertion(e)
|
|
305
|
+
if (reason === undefined) throw e
|
|
306
|
+
return {
|
|
307
|
+
blocked: {code: shuttleReasonCode(reason), reason},
|
|
308
|
+
options: [],
|
|
309
|
+
auto: undefined,
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return mapOptions(ServerContract.Types.shuttle_options.from(raw))
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
craft(req: {
|
|
316
|
+
shipId: UInt64Type
|
|
317
|
+
workshopId: UInt64Type
|
|
318
|
+
recipeId: UInt16Type
|
|
319
|
+
quantity: UInt32Type
|
|
320
|
+
inputs: ServerContract.ActionParams.Type.cargo_item[]
|
|
321
|
+
candidates: UInt64Type[]
|
|
322
|
+
recharge?: boolean
|
|
323
|
+
}): Promise<ShuttleOptions> {
|
|
324
|
+
return this.query('getcraftopts', {
|
|
325
|
+
ship_id: UInt64.from(req.shipId),
|
|
326
|
+
workshop_id: UInt64.from(req.workshopId),
|
|
327
|
+
recipe_id: UInt16.from(req.recipeId),
|
|
328
|
+
quantity: UInt32.from(req.quantity),
|
|
329
|
+
inputs: req.inputs,
|
|
330
|
+
candidates: req.candidates.map((c) => UInt64.from(c)),
|
|
331
|
+
recharge: req.recharge ?? false,
|
|
332
|
+
})
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
build(req: {
|
|
336
|
+
targetId: UInt64Type
|
|
337
|
+
dockId: UInt64Type
|
|
338
|
+
targetItemId: UInt16Type
|
|
339
|
+
inputs: ServerContract.ActionParams.Type.cargo_item[]
|
|
340
|
+
candidates: UInt64Type[]
|
|
341
|
+
}): Promise<ShuttleOptions> {
|
|
342
|
+
return this.query('getbuildopts', {
|
|
343
|
+
target_id: UInt64.from(req.targetId),
|
|
344
|
+
dock_id: UInt64.from(req.dockId),
|
|
345
|
+
target_item_id: UInt16.from(req.targetItemId),
|
|
346
|
+
inputs: req.inputs,
|
|
347
|
+
candidates: req.candidates.map((c) => UInt64.from(c)),
|
|
348
|
+
})
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
claim(req: {
|
|
352
|
+
jobId: UInt64Type
|
|
353
|
+
shipId: UInt64Type
|
|
354
|
+
candidates: UInt64Type[]
|
|
355
|
+
}): Promise<ShuttleOptions> {
|
|
356
|
+
return this.query('getclaimopts', {
|
|
357
|
+
job_id: UInt64.from(req.jobId),
|
|
358
|
+
ship_id: UInt64.from(req.shipId),
|
|
359
|
+
candidates: req.candidates.map((c) => UInt64.from(c)),
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
cancelBuild(req: {jobId: UInt64Type; candidates: UInt64Type[]}): Promise<ShuttleOptions> {
|
|
364
|
+
return this.query('getcbldopts', {
|
|
365
|
+
job_id: UInt64.from(req.jobId),
|
|
366
|
+
candidates: req.candidates.map((c) => UInt64.from(c)),
|
|
367
|
+
})
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
store(req: {
|
|
371
|
+
shipId: UInt64Type
|
|
372
|
+
depotId: UInt64Type
|
|
373
|
+
items: ServerContract.ActionParams.Type.cargo_item[]
|
|
374
|
+
candidates: UInt64Type[]
|
|
375
|
+
}): Promise<ShuttleOptions> {
|
|
376
|
+
return this.query('getstoreopts', {
|
|
377
|
+
ship_id: UInt64.from(req.shipId),
|
|
378
|
+
depot_id: UInt64.from(req.depotId),
|
|
379
|
+
items: req.items,
|
|
380
|
+
candidates: req.candidates.map((c) => UInt64.from(c)),
|
|
381
|
+
})
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
take(req: {
|
|
385
|
+
shipId: UInt64Type
|
|
386
|
+
depotId: UInt64Type
|
|
387
|
+
items: ServerContract.ActionParams.Type.cargo_item[]
|
|
388
|
+
candidates: UInt64Type[]
|
|
389
|
+
}): Promise<ShuttleOptions> {
|
|
390
|
+
return this.query('gettakeopts', {
|
|
391
|
+
ship_id: UInt64.from(req.shipId),
|
|
392
|
+
depot_id: UInt64.from(req.depotId),
|
|
393
|
+
items: req.items,
|
|
394
|
+
candidates: req.candidates.map((c) => UInt64.from(c)),
|
|
395
|
+
})
|
|
396
|
+
}
|
|
397
|
+
}
|
|
@@ -4,7 +4,7 @@ import type {ResourceCategory} from '../types'
|
|
|
4
4
|
import {getItem, getModules} from '../data/catalog'
|
|
5
5
|
import {ENTITY_SHIP, getPackedEntityType} from '../data/kind-registry'
|
|
6
6
|
import {getEntityLayout} from '../data/recipes-runtime'
|
|
7
|
-
import {entityMetadata,
|
|
7
|
+
import {entityMetadata, getItemFamily} from '../data/metadata'
|
|
8
8
|
import {
|
|
9
9
|
getModuleCapabilityType,
|
|
10
10
|
isModuleItem,
|
|
@@ -370,7 +370,7 @@ function resolveEntity(
|
|
|
370
370
|
modName = modItem.name
|
|
371
371
|
modTier = modItem.tier
|
|
372
372
|
} catch {
|
|
373
|
-
modName =
|
|
373
|
+
modName = getItemFamily(modItemId)?.name ?? 'Module'
|
|
374
374
|
}
|
|
375
375
|
const group = computeCapabilityGroup(modType, decodedStats, modTier, slot.outputPct)
|
|
376
376
|
return {
|
|
@@ -106,7 +106,17 @@ export function incomingSources(
|
|
|
106
106
|
return out
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
// Mirrors for_each_task_cargo's hosted_civic_leg guard: a hosted Drop-off/Pickup moves E's cargo, not the host's.
|
|
110
|
+
export function isHostedCivicLeg(task: Task): boolean {
|
|
111
|
+
const type = task.type.toNumber()
|
|
112
|
+
return (
|
|
113
|
+
(type === TaskType.CIVIC_DEPOSIT || type === TaskType.CIVIC_WITHDRAW) &&
|
|
114
|
+
task.couplings.length > 0
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
109
118
|
export function taskCargoEffect(task: Task): CargoEffect {
|
|
119
|
+
if (isHostedCivicLeg(task)) return {added: [], removed: []}
|
|
110
120
|
switch (taskCargoRule(task.type.toNumber())) {
|
|
111
121
|
case 'all-in':
|
|
112
122
|
case 'undeploy':
|
package/src/scheduling/energy.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import type {ServerContract} from '../contracts'
|
|
1
2
|
import {TaskType} from '../types'
|
|
2
3
|
import {createProjectedEntity, type Projectable} from './projection'
|
|
3
|
-
import {orderedTasks} from './schedule'
|
|
4
|
+
import {orderedTasks, unappliedTasks} from './schedule'
|
|
5
|
+
import {taskEnergyDrawNeedsCoordinates, taskEnergyEffect} from './task-effects'
|
|
6
|
+
|
|
7
|
+
type Task = ServerContract.Types.task
|
|
4
8
|
|
|
5
9
|
export function energyAtTime(entity: Projectable, now: Date): number {
|
|
6
10
|
const projected = createProjectedEntity(entity)
|
|
@@ -45,3 +49,53 @@ export function energyAtTime(entity: Projectable, now: Date): number {
|
|
|
45
49
|
|
|
46
50
|
return clamp(running)
|
|
47
51
|
}
|
|
52
|
+
|
|
53
|
+
// Mirrors calc_task_effect's energy branch (projection.cpp): the step this task's type applies to running energy.
|
|
54
|
+
function applyTaskEnergyEffect(
|
|
55
|
+
taskType: number,
|
|
56
|
+
hasCoordinates: boolean,
|
|
57
|
+
capacity: number | undefined,
|
|
58
|
+
cost: number | undefined,
|
|
59
|
+
energy: number
|
|
60
|
+
): number {
|
|
61
|
+
const effect = taskEnergyEffect(taskType)
|
|
62
|
+
if (effect === 'fills') return capacity !== undefined ? capacity : energy
|
|
63
|
+
if (effect === 'zeroes') return cost === undefined ? 0 : energy > cost ? energy - cost : 0
|
|
64
|
+
if (cost === undefined) return energy
|
|
65
|
+
if (effect === 'draws' && (!taskEnergyDrawNeedsCoordinates(taskType) || hasCoordinates)) {
|
|
66
|
+
return energy > cost ? energy - cost : 0
|
|
67
|
+
}
|
|
68
|
+
return energy
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function taskCost(task: Task): number | undefined {
|
|
72
|
+
return task.energy_cost !== undefined ? Number(task.energy_cost) : undefined
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Walks tasks in schedule order, gating on funding at each draw; false on the first shortfall.
|
|
76
|
+
function tasksFundedFrom(
|
|
77
|
+
tasks: readonly {task: Task}[],
|
|
78
|
+
capacity: number | undefined,
|
|
79
|
+
baseEnergy: number
|
|
80
|
+
): boolean {
|
|
81
|
+
let energy = baseEnergy
|
|
82
|
+
for (const {task} of tasks) {
|
|
83
|
+
const cost = taskCost(task)
|
|
84
|
+
if (cost !== undefined && energy < cost) return false
|
|
85
|
+
energy = applyTaskEnergyEffect(
|
|
86
|
+
task.type.toNumber(),
|
|
87
|
+
task.coordinates !== undefined,
|
|
88
|
+
capacity,
|
|
89
|
+
cost,
|
|
90
|
+
energy
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
return true
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Mirrors walk_pending_energy(require_funded=true) / energy_draws_funded in projection.cpp.
|
|
97
|
+
export function energyDrawsFunded(entity: Projectable, baseEnergy: number): boolean {
|
|
98
|
+
const projected = createProjectedEntity(entity)
|
|
99
|
+
const capacity = projected.generator ? Number(projected.generator.capacity) : undefined
|
|
100
|
+
return tasksFundedFrom(unappliedTasks(entity), capacity, baseEnergy)
|
|
101
|
+
}
|