@shipload/sdk 1.0.0-next.34 → 1.0.0-next.36
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 +398 -51
- package/lib/shipload.js +1481 -400
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +1442 -401
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +101 -20
- package/lib/testing.js +201 -57
- package/lib/testing.js.map +1 -1
- package/lib/testing.m.js +201 -57
- package/lib/testing.m.js.map +1 -1
- package/package.json +1 -1
- package/src/capabilities/crafting.ts +2 -3
- package/src/capabilities/gathering.test.ts +16 -0
- package/src/capabilities/gathering.ts +8 -11
- package/src/contracts/server.ts +147 -29
- package/src/coordinates/address.ts +88 -0
- package/src/coordinates/constants.test.ts +15 -0
- package/src/coordinates/constants.ts +23 -0
- package/src/coordinates/index.ts +15 -0
- package/src/coordinates/memo.test.ts +47 -0
- package/src/coordinates/memo.ts +20 -0
- package/src/coordinates/permutation.ts +77 -0
- package/src/coordinates/regions.ts +48 -0
- package/src/coordinates/sectors.ts +115 -0
- package/src/data/capability-formulas.ts +0 -1
- package/src/data/entities.json +4 -4
- package/src/data/items.json +5 -5
- package/src/data/recipes.json +39 -65
- package/src/derivation/capabilities.test.ts +133 -0
- package/src/derivation/capabilities.ts +66 -14
- package/src/derivation/rollups.test.ts +55 -0
- package/src/derivation/rollups.ts +56 -0
- package/src/derivation/wormhole.ts +115 -0
- package/src/entities/makers.ts +30 -3
- package/src/errors.ts +2 -0
- package/src/index-module.ts +38 -2
- package/src/managers/actions.ts +79 -5
- package/src/managers/construction.ts +6 -4
- package/src/managers/context.ts +9 -0
- package/src/managers/coordinates.ts +14 -0
- package/src/managers/plot.ts +2 -4
- package/src/nft/description.ts +25 -6
- package/src/planner/index.ts +127 -0
- package/src/planner/planner.test.ts +319 -0
- package/src/resolution/resolve-item.ts +4 -1
- package/src/scheduling/availability.ts +1 -1
- package/src/scheduling/cancel.test.ts +348 -0
- package/src/scheduling/cancel.ts +209 -0
- package/src/scheduling/lanes.test.ts +249 -0
- package/src/scheduling/lanes.ts +140 -2
- package/src/scheduling/projection.ts +75 -16
- package/src/scheduling/schedule.ts +3 -1
- package/src/shipload.ts +5 -0
- package/src/testing/projection-parity.ts +26 -2
- package/src/travel/travel.ts +116 -105
- package/src/types/capabilities.ts +23 -6
- package/src/types/entity.ts +3 -3
- package/src/types.ts +2 -1
- package/src/utils/system.ts +11 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type {Checksum256Type} from '@wharfkit/antelope'
|
|
2
|
+
import {hash512} from '../utils/hash'
|
|
3
|
+
|
|
4
|
+
export const WH = {
|
|
5
|
+
RSIZE: 75,
|
|
6
|
+
ZONE: 16384,
|
|
7
|
+
THRESHOLD: 8192,
|
|
8
|
+
MIN_REACH: 50000,
|
|
9
|
+
TRANSIT_SPEED: 500,
|
|
10
|
+
} as const
|
|
11
|
+
|
|
12
|
+
const HALF = Math.round(Math.log2(WH.ZONE))
|
|
13
|
+
const MASK = WH.ZONE - 1
|
|
14
|
+
|
|
15
|
+
function roll16(seed: Checksum256Type, str: string): number {
|
|
16
|
+
const h = hash512(seed, str).array
|
|
17
|
+
return (h[0] << 8) | h[1]
|
|
18
|
+
}
|
|
19
|
+
function feistelF(seed: Checksum256Type, x: number, round: number, key: string): number {
|
|
20
|
+
return roll16(seed, `feistel-${key}-${round}-${x}`) & MASK
|
|
21
|
+
}
|
|
22
|
+
export function feistel(seed: Checksum256Type, idx: number, key: string): number {
|
|
23
|
+
let L = (idx >>> HALF) & MASK
|
|
24
|
+
let R = idx & MASK
|
|
25
|
+
for (let r = 0; r < 4; r++) {
|
|
26
|
+
const nR = L ^ feistelF(seed, R, r, key)
|
|
27
|
+
L = R
|
|
28
|
+
R = nR
|
|
29
|
+
}
|
|
30
|
+
return (L << HALF) | R
|
|
31
|
+
}
|
|
32
|
+
export function feistelInv(seed: Checksum256Type, idx: number, key: string): number {
|
|
33
|
+
let L = (idx >>> HALF) & MASK
|
|
34
|
+
let R = idx & MASK
|
|
35
|
+
for (let r = 3; r >= 0; r--) {
|
|
36
|
+
const nL = R ^ feistelF(seed, L, r, key)
|
|
37
|
+
R = L
|
|
38
|
+
L = nL
|
|
39
|
+
}
|
|
40
|
+
return (L << HALF) | R
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type Region = {rx: number; ry: number}
|
|
44
|
+
|
|
45
|
+
export function regionOf(x: number, y: number): Region {
|
|
46
|
+
return {rx: Math.floor(x / WH.RSIZE), ry: Math.floor(y / WH.RSIZE)}
|
|
47
|
+
}
|
|
48
|
+
export function partnerRegion(seed: Checksum256Type, R: Region): Region {
|
|
49
|
+
const qx = Math.floor(R.rx / WH.ZONE)
|
|
50
|
+
const qy = Math.floor(R.ry / WH.ZONE)
|
|
51
|
+
const zx = qx * WH.ZONE
|
|
52
|
+
const zy = qy * WH.ZONE
|
|
53
|
+
const key = `${qx}:${qy}`
|
|
54
|
+
const idx = (R.ry - zy) * WH.ZONE + (R.rx - zx)
|
|
55
|
+
const p = feistelInv(seed, feistel(seed, idx, key) ^ 1, key)
|
|
56
|
+
return {rx: zx + (p % WH.ZONE), ry: zy + Math.floor(p / WH.ZONE)}
|
|
57
|
+
}
|
|
58
|
+
function regKey(R: Region): string {
|
|
59
|
+
return `${R.rx}:${R.ry}`
|
|
60
|
+
}
|
|
61
|
+
function pairKey(a: Region, b: Region): string {
|
|
62
|
+
const ka = regKey(a)
|
|
63
|
+
const kb = regKey(b)
|
|
64
|
+
return ka < kb ? `${ka}|${kb}` : `${kb}|${ka}`
|
|
65
|
+
}
|
|
66
|
+
function endpointInRegion(seed: Checksum256Type, R: Region, key: string): {x: number; y: number} {
|
|
67
|
+
const h = hash512(seed, `wh-endpoint-${key}-${regKey(R)}`).array
|
|
68
|
+
const ox = ((h[0] << 24) | (h[1] << 16) | (h[2] << 8) | h[3]) >>> 0
|
|
69
|
+
const oy = ((h[4] << 24) | (h[5] << 16) | (h[6] << 8) | h[7]) >>> 0
|
|
70
|
+
return {x: R.rx * WH.RSIZE + (ox % WH.RSIZE), y: R.ry * WH.RSIZE + (oy % WH.RSIZE)}
|
|
71
|
+
}
|
|
72
|
+
function dist(a: {x: number; y: number}, b: {x: number; y: number}): number {
|
|
73
|
+
return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2)
|
|
74
|
+
}
|
|
75
|
+
function wormholeOfRegion(
|
|
76
|
+
seed: Checksum256Type,
|
|
77
|
+
R: Region
|
|
78
|
+
): {A: {x: number; y: number}; B: {x: number; y: number}} | null {
|
|
79
|
+
const P = partnerRegion(seed, R)
|
|
80
|
+
if (P.rx === R.rx && P.ry === R.ry) return null
|
|
81
|
+
const key = pairKey(R, P)
|
|
82
|
+
if (roll16(seed, `wh-exists-${key}`) >= WH.THRESHOLD) return null
|
|
83
|
+
const A = endpointInRegion(seed, R, key)
|
|
84
|
+
const B = endpointInRegion(seed, P, key)
|
|
85
|
+
if (dist(A, B) < WH.MIN_REACH) return null
|
|
86
|
+
return {A, B}
|
|
87
|
+
}
|
|
88
|
+
export function wormholeAtRegionEndpoint(
|
|
89
|
+
seed: Checksum256Type,
|
|
90
|
+
rx: number,
|
|
91
|
+
ry: number
|
|
92
|
+
): {from: {x: number; y: number}; to: {x: number; y: number}} | null {
|
|
93
|
+
const w = wormholeOfRegion(seed, {rx, ry})
|
|
94
|
+
if (!w) return null
|
|
95
|
+
return {from: w.A, to: w.B}
|
|
96
|
+
}
|
|
97
|
+
export function wormholeAt(
|
|
98
|
+
seed: Checksum256Type,
|
|
99
|
+
x: number,
|
|
100
|
+
y: number
|
|
101
|
+
): {x: number; y: number} | null {
|
|
102
|
+
const w = wormholeOfRegion(seed, regionOf(x, y))
|
|
103
|
+
if (!w || w.A.x !== x || w.A.y !== y) return null
|
|
104
|
+
return w.B
|
|
105
|
+
}
|
|
106
|
+
export function isValidWormholePair(
|
|
107
|
+
seed: Checksum256Type,
|
|
108
|
+
ax: number,
|
|
109
|
+
ay: number,
|
|
110
|
+
bx: number,
|
|
111
|
+
by: number
|
|
112
|
+
): boolean {
|
|
113
|
+
const to = wormholeAt(seed, ax, ay)
|
|
114
|
+
return to !== null && to.x === bx && to.y === by
|
|
115
|
+
}
|
package/src/entities/makers.ts
CHANGED
|
@@ -140,13 +140,40 @@ export function makeEntity(packedItemId: number, state: EntityStateInput): Entit
|
|
|
140
140
|
|
|
141
141
|
if (caps.engines) info.engines = caps.engines
|
|
142
142
|
if (caps.generator) info.generator = caps.generator
|
|
143
|
-
if (caps.gatherer) info.gatherer = caps.gatherer
|
|
144
|
-
if (caps.loaders) info.loaders = caps.loaders
|
|
145
|
-
if (caps.crafter) info.crafter = caps.crafter
|
|
146
143
|
if (caps.hauler) info.hauler = caps.hauler
|
|
147
144
|
if (caps.warp) info.warp = caps.warp
|
|
145
|
+
|
|
146
|
+
info.gatherer_lanes = (caps.gathererLanes ?? []).map((l) =>
|
|
147
|
+
ServerContract.Types.gatherer_lane.from({
|
|
148
|
+
slot_index: l.slotIndex,
|
|
149
|
+
yield: l.yield,
|
|
150
|
+
drain: l.drain,
|
|
151
|
+
depth: l.depth,
|
|
152
|
+
output_pct: l.outputPct,
|
|
153
|
+
})
|
|
154
|
+
)
|
|
155
|
+
info.crafter_lanes = (caps.crafterLanes ?? []).map((l) =>
|
|
156
|
+
ServerContract.Types.crafter_lane.from({
|
|
157
|
+
slot_index: l.slotIndex,
|
|
158
|
+
speed: l.speed,
|
|
159
|
+
drain: l.drain,
|
|
160
|
+
output_pct: l.outputPct,
|
|
161
|
+
})
|
|
162
|
+
)
|
|
163
|
+
info.loader_lanes = (caps.loaderLanes ?? []).map((l) =>
|
|
164
|
+
ServerContract.Types.loader_lane.from({
|
|
165
|
+
slot_index: l.slotIndex,
|
|
166
|
+
mass: l.mass,
|
|
167
|
+
thrust: l.thrust,
|
|
168
|
+
output_pct: l.outputPct,
|
|
169
|
+
})
|
|
170
|
+
)
|
|
148
171
|
}
|
|
149
172
|
|
|
173
|
+
if (!info.gatherer_lanes) info.gatherer_lanes = []
|
|
174
|
+
if (!info.crafter_lanes) info.crafter_lanes = []
|
|
175
|
+
if (!info.loader_lanes) info.loader_lanes = []
|
|
176
|
+
|
|
150
177
|
const entityInfo = ServerContract.Types.entity_info.from(info)
|
|
151
178
|
return new Entity(entityInfo)
|
|
152
179
|
}
|
package/src/errors.ts
CHANGED
|
@@ -56,6 +56,8 @@ export const GROUP_HAUL_CAPACITY_EXCEEDED =
|
|
|
56
56
|
'Group travel requires sufficient hauler capacity for all non-self-propelled entities.'
|
|
57
57
|
export const CANCEL_CONTAINS_GROUPED_TASK =
|
|
58
58
|
'Cannot cancel range containing grouped task - cancel non-grouped tasks first.'
|
|
59
|
+
export const WOULD_STRAND = 'Cancelling this would leave a later task without the cargo it needs.'
|
|
60
|
+
export const WOULD_OVERFILL = 'Cancelling this would overfill the other entity with returned cargo.'
|
|
59
61
|
export const WARP_NO_CAPABILITY = 'Entity does not have warp capability.'
|
|
60
62
|
export const WARP_HAS_SCHEDULE = 'Entity must be idle to warp.'
|
|
61
63
|
export const WARP_HAS_CARGO = 'Entity must have no cargo to warp.'
|
package/src/index-module.ts
CHANGED
|
@@ -26,13 +26,11 @@ export type {InstalledModule} from './entities/slot-multiplier'
|
|
|
26
26
|
|
|
27
27
|
export type movement_stats = ServerContract.Types.movement_stats
|
|
28
28
|
export type energy_stats = ServerContract.Types.energy_stats
|
|
29
|
-
export type loader_stats = ServerContract.Types.loader_stats
|
|
30
29
|
export type schedule = ServerContract.Types.schedule
|
|
31
30
|
export type lane = ServerContract.Types.lane
|
|
32
31
|
export type task = ServerContract.Types.task
|
|
33
32
|
export type cargo_item = ServerContract.Types.cargo_item
|
|
34
33
|
export type entity_row = ServerContract.Types.entity_row
|
|
35
|
-
export type gatherer_stats = ServerContract.Types.gatherer_stats
|
|
36
34
|
|
|
37
35
|
export type location_static = ServerContract.Types.location_static
|
|
38
36
|
export type location_derived = ServerContract.Types.location_derived
|
|
@@ -87,6 +85,7 @@ export type {EpochInfo} from './scheduling/epoch'
|
|
|
87
85
|
export {
|
|
88
86
|
getSystemName,
|
|
89
87
|
hasSystem,
|
|
88
|
+
getLocationKind,
|
|
90
89
|
getLocationType,
|
|
91
90
|
getLocationTypeName,
|
|
92
91
|
isGatherableLocation,
|
|
@@ -157,6 +156,7 @@ export {
|
|
|
157
156
|
calc_flighttime,
|
|
158
157
|
calc_loader_acceleration,
|
|
159
158
|
calc_loader_flighttime,
|
|
159
|
+
calc_onesided_duration,
|
|
160
160
|
calc_orbital_altitude,
|
|
161
161
|
calc_rechargetime,
|
|
162
162
|
calc_ship_acceleration,
|
|
@@ -164,6 +164,7 @@ export {
|
|
|
164
164
|
calc_ship_mass,
|
|
165
165
|
calc_ship_rechargetime,
|
|
166
166
|
calc_transfer_duration,
|
|
167
|
+
calc_transit_duration,
|
|
167
168
|
calculateFlightTime,
|
|
168
169
|
calculateLoadTimeBreakdown,
|
|
169
170
|
calculateRefuelingTime,
|
|
@@ -202,8 +203,17 @@ export {
|
|
|
202
203
|
candidateLaneCompletesAt,
|
|
203
204
|
laneKeyForModule,
|
|
204
205
|
rawScheduleEnd,
|
|
206
|
+
resolveLaneGatherer,
|
|
207
|
+
resolveLaneCrafter,
|
|
208
|
+
resolveLaneLoader,
|
|
209
|
+
selectGatherLane,
|
|
205
210
|
workerLaneKey,
|
|
206
211
|
} from './scheduling/lanes'
|
|
212
|
+
export type {
|
|
213
|
+
ResolvedGathererLane,
|
|
214
|
+
ResolvedCrafterLane,
|
|
215
|
+
ResolvedLoaderLane,
|
|
216
|
+
} from './scheduling/lanes'
|
|
207
217
|
export {ScheduleAccessor, createScheduleAccessor} from './scheduling/accessor'
|
|
208
218
|
export {InventoryAccessor, createInventoryAccessor} from './entities/inventory-accessor'
|
|
209
219
|
export type {HasCargo} from './entities/inventory-accessor'
|
|
@@ -232,6 +242,15 @@ export type {TaskCargoChange, TaskCargoDirection} from './scheduling/task-cargo'
|
|
|
232
242
|
export {composeIdleResolve} from './scheduling/idle-resolve'
|
|
233
243
|
export type {CounterpartLookup, IdleResolveTarget} from './scheduling/idle-resolve'
|
|
234
244
|
|
|
245
|
+
export {cancelEligibility, CancelBlockReason} from './scheduling/cancel'
|
|
246
|
+
export type {
|
|
247
|
+
CancelPlan,
|
|
248
|
+
CancelEffects,
|
|
249
|
+
CancelRefund,
|
|
250
|
+
CancelReleasedHold,
|
|
251
|
+
CancelEligibilityInput,
|
|
252
|
+
} from './scheduling/cancel'
|
|
253
|
+
|
|
235
254
|
export {
|
|
236
255
|
projectedCargoAvailableAt,
|
|
237
256
|
availableForItem,
|
|
@@ -356,6 +375,19 @@ export {
|
|
|
356
375
|
} from './derivation/capabilities'
|
|
357
376
|
export type {GathererDepthParams, ComputedCapabilities} from './derivation/capabilities'
|
|
358
377
|
|
|
378
|
+
export {
|
|
379
|
+
WH,
|
|
380
|
+
feistel,
|
|
381
|
+
feistelInv,
|
|
382
|
+
regionOf,
|
|
383
|
+
partnerRegion,
|
|
384
|
+
wormholeAt,
|
|
385
|
+
wormholeAtRegionEndpoint,
|
|
386
|
+
isValidWormholePair,
|
|
387
|
+
} from './derivation/wormhole'
|
|
388
|
+
|
|
389
|
+
export {rollupGatherer, rollupCrafter, rollupLoaders} from './derivation/rollups'
|
|
390
|
+
|
|
359
391
|
export {resolveItem} from './resolution/resolve-item'
|
|
360
392
|
export type {
|
|
361
393
|
ResolvedItem,
|
|
@@ -460,8 +492,12 @@ export {
|
|
|
460
492
|
} from './data/tiers'
|
|
461
493
|
|
|
462
494
|
export {formatMass, formatMassDelta, formatMassScaled, formatLocation} from './format'
|
|
495
|
+
export * from './coordinates'
|
|
463
496
|
|
|
464
497
|
export {displayName, baseName, describeItem} from './resolution/display-name'
|
|
465
498
|
export type {DescribeOptions} from './resolution/display-name'
|
|
466
499
|
|
|
467
500
|
export * from './subscriptions'
|
|
501
|
+
|
|
502
|
+
export {planParallelGather, planParallelTransfer} from './planner'
|
|
503
|
+
export type {LanePlanEntry, PlanTarget, GatherPlanEntity} from './planner'
|
package/src/managers/actions.ts
CHANGED
|
@@ -3,8 +3,10 @@ import {
|
|
|
3
3
|
Checksum256,
|
|
4
4
|
type Checksum256Type,
|
|
5
5
|
Int64,
|
|
6
|
+
type Int64Type,
|
|
6
7
|
Name,
|
|
7
8
|
type NameType,
|
|
9
|
+
Transaction,
|
|
8
10
|
UInt8,
|
|
9
11
|
type UInt8Type,
|
|
10
12
|
UInt16,
|
|
@@ -37,13 +39,17 @@ export class ActionsManager extends BaseManager {
|
|
|
37
39
|
})
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
private entityRefs(entities: EntityRefInput[]) {
|
|
43
|
+
return entities.map((e) =>
|
|
42
44
|
ServerContract.Types.entity_ref.from({
|
|
43
45
|
entity_type: e.entityType,
|
|
44
46
|
entity_id: UInt64.from(e.entityId),
|
|
45
47
|
})
|
|
46
48
|
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
grouptravel(entities: EntityRefInput[], destination: CoordinatesType, recharge = true): Action {
|
|
52
|
+
const entityRefs = this.entityRefs(entities)
|
|
47
53
|
const x = Int64.from(destination.x)
|
|
48
54
|
const y = Int64.from(destination.y)
|
|
49
55
|
|
|
@@ -55,6 +61,44 @@ export class ActionsManager extends BaseManager {
|
|
|
55
61
|
})
|
|
56
62
|
}
|
|
57
63
|
|
|
64
|
+
transit(shipId: UInt64Type, entrance: CoordinatesType, exit: CoordinatesType): Action {
|
|
65
|
+
return this.server.action('transit', {
|
|
66
|
+
id: UInt64.from(shipId),
|
|
67
|
+
ax: Int64.from(entrance.x),
|
|
68
|
+
ay: Int64.from(entrance.y),
|
|
69
|
+
bx: Int64.from(exit.x),
|
|
70
|
+
by: Int64.from(exit.y),
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
grouptransit(
|
|
75
|
+
entities: EntityRefInput[],
|
|
76
|
+
entrance: CoordinatesType,
|
|
77
|
+
exit: CoordinatesType
|
|
78
|
+
): Action {
|
|
79
|
+
const entityRefs = this.entityRefs(entities)
|
|
80
|
+
return this.server.action('grouptransit', {
|
|
81
|
+
entities: entityRefs,
|
|
82
|
+
ax: Int64.from(entrance.x),
|
|
83
|
+
ay: Int64.from(entrance.y),
|
|
84
|
+
bx: Int64.from(exit.x),
|
|
85
|
+
by: Int64.from(exit.y),
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getwormhole(x: Int64Type, y: Int64Type): Action {
|
|
90
|
+
return this.server.action('getwormhole', {x: Int64.from(x), y: Int64.from(y)})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
getdistance(origin: CoordinatesType, destination: CoordinatesType): Action {
|
|
94
|
+
return this.server.action('getdistance', {
|
|
95
|
+
ax: Int64.from(origin.x),
|
|
96
|
+
ay: Int64.from(origin.y),
|
|
97
|
+
bx: Int64.from(destination.x),
|
|
98
|
+
by: Int64.from(destination.y),
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
58
102
|
resolve(entityId: UInt64Type, count?: UInt64Type): Action {
|
|
59
103
|
const params: ServerContract.ActionParams.resolve = {
|
|
60
104
|
id: UInt64.from(entityId),
|
|
@@ -139,13 +183,39 @@ export class ActionsManager extends BaseManager {
|
|
|
139
183
|
sourceId: UInt64Type,
|
|
140
184
|
destinationId: UInt64Type,
|
|
141
185
|
stratum: UInt16Type,
|
|
142
|
-
quantity: UInt32Type
|
|
186
|
+
quantity: UInt32Type,
|
|
187
|
+
slot?: UInt8Type
|
|
143
188
|
): Action {
|
|
144
|
-
|
|
189
|
+
const params: ServerContract.ActionParams.gather = {
|
|
145
190
|
source_id: UInt64.from(sourceId),
|
|
146
191
|
destination_id: UInt64.from(destinationId),
|
|
147
192
|
stratum: UInt16.from(stratum),
|
|
148
193
|
quantity: UInt32.from(quantity),
|
|
194
|
+
}
|
|
195
|
+
if (slot !== undefined) {
|
|
196
|
+
params.slot = UInt8.from(slot)
|
|
197
|
+
}
|
|
198
|
+
return this.server.action('gather', params)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Packs N gather actions into one Transaction; the wallet/session fills in TAPoS at sign time.
|
|
202
|
+
bundleGather(
|
|
203
|
+
gathers: {
|
|
204
|
+
sourceId: UInt64Type
|
|
205
|
+
destinationId: UInt64Type
|
|
206
|
+
stratum: UInt16Type
|
|
207
|
+
quantity: UInt32Type
|
|
208
|
+
slot?: UInt8Type
|
|
209
|
+
}[]
|
|
210
|
+
): Transaction {
|
|
211
|
+
const actions = gathers.map(({sourceId, destinationId, stratum, quantity, slot}) =>
|
|
212
|
+
this.gather(sourceId, destinationId, stratum, quantity, slot)
|
|
213
|
+
)
|
|
214
|
+
return Transaction.from({
|
|
215
|
+
expiration: 0,
|
|
216
|
+
ref_block_num: 0,
|
|
217
|
+
ref_block_prefix: 0,
|
|
218
|
+
actions,
|
|
149
219
|
})
|
|
150
220
|
}
|
|
151
221
|
|
|
@@ -165,7 +235,8 @@ export class ActionsManager extends BaseManager {
|
|
|
165
235
|
recipeId: number,
|
|
166
236
|
quantity: number,
|
|
167
237
|
inputs: ServerContract.ActionParams.Type.cargo_item[],
|
|
168
|
-
target?: UInt64Type
|
|
238
|
+
target?: UInt64Type,
|
|
239
|
+
slot?: UInt8Type
|
|
169
240
|
): Action {
|
|
170
241
|
const params: ServerContract.ActionParams.craft = {
|
|
171
242
|
id: UInt64.from(entityId),
|
|
@@ -176,6 +247,9 @@ export class ActionsManager extends BaseManager {
|
|
|
176
247
|
if (target !== undefined) {
|
|
177
248
|
params.target = UInt64.from(target)
|
|
178
249
|
}
|
|
250
|
+
if (slot !== undefined) {
|
|
251
|
+
params.slot = UInt8.from(slot)
|
|
252
|
+
}
|
|
179
253
|
return this.server.action('craft', params)
|
|
180
254
|
}
|
|
181
255
|
|
|
@@ -67,8 +67,9 @@ export class ConstructionManager extends BaseManager {
|
|
|
67
67
|
if (!entity.owner.equals(target.ownerName)) continue
|
|
68
68
|
if (entity.id.equals(target.entityId)) continue
|
|
69
69
|
if (!coordsEqual(entity.coordinates, target.coordinates)) continue
|
|
70
|
-
const
|
|
71
|
-
if (
|
|
70
|
+
const crafterLanes = entity.crafter_lanes ?? []
|
|
71
|
+
if (crafterLanes.length === 0) continue
|
|
72
|
+
const speed = crafterLanes.reduce((s, l) => s + Number(l.speed), 0)
|
|
72
73
|
out.push({
|
|
73
74
|
entityId: entity.id,
|
|
74
75
|
entityType: entity.type,
|
|
@@ -345,8 +346,9 @@ function partitionSources(
|
|
|
345
346
|
const reserved = reservedByItemFor(entity)
|
|
346
347
|
const relevant = matchRelevantCargo(entity, target, cargo, reserved)
|
|
347
348
|
if (relevant.length === 0) continue
|
|
348
|
-
const
|
|
349
|
-
const
|
|
349
|
+
const loaderLanes = entity.loader_lanes ?? []
|
|
350
|
+
const loaderCount = loaderLanes.length
|
|
351
|
+
const loaderTotalMass = loaderLanes.reduce((s, l) => s + Number(l.mass), 0)
|
|
350
352
|
const ref: SourceEntityRef = {
|
|
351
353
|
entityId: entity.id,
|
|
352
354
|
name: entity.id.toString(),
|
package/src/managers/context.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {GameState} from '../entities/gamestate'
|
|
|
6
6
|
import {EntitiesManager} from './entities'
|
|
7
7
|
import {PlayersManager} from './players'
|
|
8
8
|
import {LocationsManager} from './locations'
|
|
9
|
+
import {CoordinatesManager} from './coordinates'
|
|
9
10
|
import {EpochsManager} from './epochs'
|
|
10
11
|
import {ActionsManager} from './actions'
|
|
11
12
|
import {NftManager} from './nft'
|
|
@@ -15,6 +16,7 @@ export class GameContext {
|
|
|
15
16
|
private _entities?: EntitiesManager
|
|
16
17
|
private _players?: PlayersManager
|
|
17
18
|
private _locations?: LocationsManager
|
|
19
|
+
private _coordinates?: CoordinatesManager
|
|
18
20
|
private _epochs?: EpochsManager
|
|
19
21
|
private _actions?: ActionsManager
|
|
20
22
|
private _nft?: NftManager
|
|
@@ -52,6 +54,13 @@ export class GameContext {
|
|
|
52
54
|
return this._locations
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
get coordinates(): CoordinatesManager {
|
|
58
|
+
if (!this._coordinates) {
|
|
59
|
+
this._coordinates = new CoordinatesManager(this)
|
|
60
|
+
}
|
|
61
|
+
return this._coordinates
|
|
62
|
+
}
|
|
63
|
+
|
|
55
64
|
get epochs(): EpochsManager {
|
|
56
65
|
if (!this._epochs) {
|
|
57
66
|
this._epochs = new EpochsManager(this)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {BaseManager} from './base'
|
|
2
|
+
import {type CoordinateAddress, decodeAddress, encodeAddressMemo} from '../coordinates'
|
|
3
|
+
|
|
4
|
+
export class CoordinatesManager extends BaseManager {
|
|
5
|
+
async encode(x: number, y: number): Promise<CoordinateAddress> {
|
|
6
|
+
const game = await this.getGame()
|
|
7
|
+
return encodeAddressMemo(game.config.seed, x, y)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async decode(addr: CoordinateAddress): Promise<{x: number; y: number}> {
|
|
11
|
+
const game = await this.getGame()
|
|
12
|
+
return decodeAddress(game.config.seed, addr)
|
|
13
|
+
}
|
|
14
|
+
}
|
package/src/managers/plot.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {computeInputMass} from '../derivation/crafting'
|
|
|
5
5
|
import {calc_craft_duration} from '../capabilities/crafting'
|
|
6
6
|
import {TaskType} from '../types'
|
|
7
7
|
import {BaseManager} from './base'
|
|
8
|
+
import type {CrafterStats} from '../types/capabilities'
|
|
8
9
|
import type {ServerContract} from '../contracts'
|
|
9
10
|
import type {BuildableTarget, ScheduledBuild} from './construction-types'
|
|
10
11
|
|
|
@@ -112,10 +113,7 @@ export class PlotManager extends BaseManager {
|
|
|
112
113
|
return this.progress(plot, cargo).isComplete
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
timeToComplete(
|
|
116
|
-
plot: ServerContract.Types.entity_info,
|
|
117
|
-
crafter: ServerContract.Types.crafter_stats
|
|
118
|
-
): number {
|
|
116
|
+
timeToComplete(plot: ServerContract.Types.entity_info, crafter: CrafterStats): number {
|
|
119
117
|
const capacity = Number(plot.capacity?.toString() ?? '0')
|
|
120
118
|
const speed = Number(crafter.speed.toString())
|
|
121
119
|
if (speed === 0) return 0
|
package/src/nft/description.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
ITEM_CRAFTER_T1,
|
|
16
16
|
ITEM_ENGINE_T1,
|
|
17
17
|
ITEM_EXTRACTOR_T1_PACKED,
|
|
18
|
+
ITEM_FACTORY_T1_PACKED,
|
|
18
19
|
ITEM_GATHERER_T1,
|
|
19
20
|
ITEM_GENERATOR_T1,
|
|
20
21
|
ITEM_HAULER_T1,
|
|
@@ -38,13 +39,23 @@ export function computeBaseHullmass(stats: bigint): number {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
export function computeBaseCapacityShip(stats: bigint): number {
|
|
41
|
-
const s = decodeStat(stats, 0) + decodeStat(stats, 2)
|
|
42
|
-
return Math.floor(5_000_000 * 6 ** (s /
|
|
42
|
+
const s = decodeStat(stats, 0) + decodeStat(stats, 2)
|
|
43
|
+
return Math.floor(5_000_000 * 6 ** (s / 1998))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function computeBaseCapacityContainer(stats: bigint): number {
|
|
47
|
+
const s = decodeStat(stats, 0) + decodeStat(stats, 2)
|
|
48
|
+
return Math.floor(22_000_000 * 6 ** (s / 1998))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function computeBaseCapacityContainerT2(stats: bigint): number {
|
|
52
|
+
const s = decodeStat(stats, 0) + decodeStat(stats, 2)
|
|
53
|
+
return Math.floor(24_000_000 * 6 ** (s / 2947))
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
export function computeBaseCapacityWarehouse(stats: bigint): number {
|
|
46
|
-
const s = decodeStat(stats, 0) + decodeStat(stats, 2)
|
|
47
|
-
return Math.floor(100_000_000 * 6 ** (s /
|
|
57
|
+
const s = decodeStat(stats, 0) + decodeStat(stats, 2)
|
|
58
|
+
return Math.floor(100_000_000 * 6 ** (s / 1998))
|
|
48
59
|
}
|
|
49
60
|
|
|
50
61
|
export const computeEngineThrust = (vol: number): number => 400 + idiv(vol * 3, 4)
|
|
@@ -73,6 +84,8 @@ export function entityDisplayName(itemId: number): string {
|
|
|
73
84
|
return 'Warehouse'
|
|
74
85
|
case ITEM_EXTRACTOR_T1_PACKED:
|
|
75
86
|
return 'Extractor'
|
|
87
|
+
case ITEM_FACTORY_T1_PACKED:
|
|
88
|
+
return 'Factory'
|
|
76
89
|
case ITEM_CONTAINER_T1_PACKED:
|
|
77
90
|
return 'Container'
|
|
78
91
|
case ITEM_CONTAINER_T2_PACKED:
|
|
@@ -188,8 +201,14 @@ export function buildEntityDescription(
|
|
|
188
201
|
baseCapacity = computeBaseCapacityShip(hullStats)
|
|
189
202
|
} else if (itemId === ITEM_WAREHOUSE_T1_PACKED) {
|
|
190
203
|
baseCapacity = computeBaseCapacityWarehouse(hullStats)
|
|
191
|
-
} else if (
|
|
192
|
-
|
|
204
|
+
} else if (
|
|
205
|
+
itemId === ITEM_EXTRACTOR_T1_PACKED ||
|
|
206
|
+
itemId === ITEM_FACTORY_T1_PACKED ||
|
|
207
|
+
itemId === ITEM_CONTAINER_T1_PACKED
|
|
208
|
+
) {
|
|
209
|
+
baseCapacity = computeBaseCapacityContainer(hullStats)
|
|
210
|
+
} else if (itemId === ITEM_CONTAINER_T2_PACKED) {
|
|
211
|
+
baseCapacity = computeBaseCapacityContainerT2(hullStats)
|
|
193
212
|
}
|
|
194
213
|
|
|
195
214
|
let out = entityDisplayName(itemId)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type {GathererStats} from '../types/capabilities'
|
|
2
|
+
import type {ServerContract} from '../contracts'
|
|
3
|
+
import {
|
|
4
|
+
calc_gather_duration,
|
|
5
|
+
calc_gather_energy,
|
|
6
|
+
GATHER_MASS_DIVISOR,
|
|
7
|
+
} from '../capabilities/gathering'
|
|
8
|
+
import {projectRemainingAt, type Projectable} from '../scheduling/projection'
|
|
9
|
+
|
|
10
|
+
export interface LanePlanEntry {
|
|
11
|
+
slot: number
|
|
12
|
+
quantity: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type PlanTarget = {quantity: number} | 'max'
|
|
16
|
+
|
|
17
|
+
export interface GatherPlanEntity extends Projectable {
|
|
18
|
+
gatherer_lanes: ServerContract.Types.gatherer_lane[]
|
|
19
|
+
loader_lanes: ServerContract.Types.loader_lane[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// massFactor=1 so plan-time cost is conservative; on-chain recomputes with real mass/richness.
|
|
23
|
+
const PLAN_ITEM_MASS = GATHER_MASS_DIVISOR
|
|
24
|
+
const PLAN_RICHNESS = 1000
|
|
25
|
+
|
|
26
|
+
// 'max' ceiling; Phase 3 passes a real reserve/capacity cap instead.
|
|
27
|
+
const MAX_PLAN_QTY = 10000
|
|
28
|
+
|
|
29
|
+
function gatherEnergyCost(
|
|
30
|
+
lane: ServerContract.Types.gatherer_lane,
|
|
31
|
+
quantity: number,
|
|
32
|
+
stratum: number
|
|
33
|
+
): number {
|
|
34
|
+
const stats = lane as unknown as GathererStats
|
|
35
|
+
const dur = Number(
|
|
36
|
+
calc_gather_duration(stats, PLAN_ITEM_MASS, quantity, stratum, PLAN_RICHNESS)
|
|
37
|
+
)
|
|
38
|
+
return Number(calc_gather_energy(stats, dur))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function allocateProportional(
|
|
42
|
+
lanes: {slot: number; weight: number}[],
|
|
43
|
+
total: number
|
|
44
|
+
): LanePlanEntry[] {
|
|
45
|
+
if (lanes.length === 0) return []
|
|
46
|
+
const weightSum = lanes.reduce((s, l) => s + l.weight, 0)
|
|
47
|
+
if (weightSum === 0) return []
|
|
48
|
+
|
|
49
|
+
const entries: LanePlanEntry[] = lanes.map((l) => ({
|
|
50
|
+
slot: l.slot,
|
|
51
|
+
quantity: Math.floor((total * l.weight) / weightSum),
|
|
52
|
+
}))
|
|
53
|
+
|
|
54
|
+
let remainder = total - entries.reduce((s, e) => s + e.quantity, 0)
|
|
55
|
+
for (let i = 0; remainder > 0; i = (i + 1) % entries.length) {
|
|
56
|
+
entries[i].quantity++
|
|
57
|
+
remainder--
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return entries
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function planParallelGather(
|
|
64
|
+
entity: GatherPlanEntity,
|
|
65
|
+
target: PlanTarget,
|
|
66
|
+
stratum: number,
|
|
67
|
+
now: Date
|
|
68
|
+
): LanePlanEntry[] {
|
|
69
|
+
const reaching = entity.gatherer_lanes.filter((l) => l.depth.toNumber() >= stratum)
|
|
70
|
+
if (reaching.length === 0) throw new Error('no gatherer reaches this stratum')
|
|
71
|
+
|
|
72
|
+
// Projected energy nets out already-queued/in-flight task costs (contract projected_energy()).
|
|
73
|
+
const energy = entity.generator ? Number(projectRemainingAt(entity, now).energy) : Infinity
|
|
74
|
+
|
|
75
|
+
const requestedQty = target === 'max' ? MAX_PLAN_QTY : (target as {quantity: number}).quantity
|
|
76
|
+
|
|
77
|
+
// Ascending by yield so slice(1) sheds the lowest-yield lane first when energy-starved.
|
|
78
|
+
let activeLanes = reaching.slice().sort((a, b) => a.yield.toNumber() - b.yield.toNumber())
|
|
79
|
+
|
|
80
|
+
while (activeLanes.length > 0) {
|
|
81
|
+
const laneWeights = activeLanes.map((l) => ({
|
|
82
|
+
slot: l.slot_index.toNumber(),
|
|
83
|
+
weight: l.yield.toNumber(),
|
|
84
|
+
}))
|
|
85
|
+
|
|
86
|
+
const proposed = allocateProportional(laneWeights, requestedQty)
|
|
87
|
+
|
|
88
|
+
const totalEnergyCost = proposed.reduce((sum, entry) => {
|
|
89
|
+
const lane = activeLanes.find((l) => l.slot_index.toNumber() === entry.slot)!
|
|
90
|
+
return sum + gatherEnergyCost(lane, entry.quantity, stratum)
|
|
91
|
+
}, 0)
|
|
92
|
+
|
|
93
|
+
if (totalEnergyCost <= energy) {
|
|
94
|
+
return proposed.filter((e) => e.quantity > 0)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (activeLanes.length === 1) {
|
|
98
|
+
const lane = activeLanes[0]
|
|
99
|
+
const energyPerUnit = gatherEnergyCost(lane, 1, stratum)
|
|
100
|
+
if (energyPerUnit === 0) return proposed.filter((e) => e.quantity > 0)
|
|
101
|
+
const maxQty = Math.min(requestedQty, Math.floor(energy / energyPerUnit))
|
|
102
|
+
if (maxQty <= 0) return []
|
|
103
|
+
return [{slot: lane.slot_index.toNumber(), quantity: maxQty}]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
activeLanes = activeLanes.slice(1)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return []
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function planParallelTransfer(
|
|
113
|
+
entity: GatherPlanEntity,
|
|
114
|
+
target: PlanTarget
|
|
115
|
+
): LanePlanEntry[] {
|
|
116
|
+
const lanes = entity.loader_lanes.filter((l) => l.thrust.toNumber() > 0)
|
|
117
|
+
if (lanes.length === 0) return []
|
|
118
|
+
|
|
119
|
+
const requestedQty = target === 'max' ? MAX_PLAN_QTY : (target as {quantity: number}).quantity
|
|
120
|
+
|
|
121
|
+
const laneWeights = lanes.map((l) => ({
|
|
122
|
+
slot: l.slot_index.toNumber(),
|
|
123
|
+
weight: l.thrust.toNumber(),
|
|
124
|
+
}))
|
|
125
|
+
|
|
126
|
+
return allocateProportional(laneWeights, requestedQty).filter((e) => e.quantity > 0)
|
|
127
|
+
}
|