@shipload/sdk 1.0.0-next.34 → 1.0.0-next.35

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.
@@ -0,0 +1,77 @@
1
+ import type {Checksum256Type} from '@wharfkit/antelope'
2
+ import {hash512} from '../utils/hash'
3
+
4
+ // FROZEN INTERFACE — round count, key-derivation strings, and mixing constants must never change; per-game variation comes from the seed.
5
+ const ROUNDS = 4
6
+
7
+ export interface FeistelConfig {
8
+ n: number
9
+ halfBits: number
10
+ label: string
11
+ }
12
+
13
+ const keyCache = new Map<string, number[]>()
14
+
15
+ function deriveRoundKeys(seed: Checksum256Type, label: string): number[] {
16
+ const cacheKey = `${seed}:${label}`
17
+ const cached = keyCache.get(cacheKey)
18
+ if (cached) return cached
19
+ const h = hash512(seed, `coord-keys-${label}`).array
20
+ const keys: number[] = []
21
+ for (let i = 0; i < ROUNDS; i++) {
22
+ const o = i * 4
23
+ keys.push(((h[o] << 24) | (h[o + 1] << 16) | (h[o + 2] << 8) | h[o + 3]) >>> 0)
24
+ }
25
+ keyCache.set(cacheKey, keys)
26
+ return keys
27
+ }
28
+
29
+ function roundFn(r: number, key: number, halfBits: number): number {
30
+ let x = (r ^ key) >>> 0
31
+ x = Math.imul(x ^ (x >>> 16), 0x9e3779b1) >>> 0
32
+ x = Math.imul(x ^ (x >>> 13), 0x7feb352d) >>> 0
33
+ x = (x ^ (x >>> 16)) >>> 0
34
+ return x & ((1 << halfBits) - 1)
35
+ }
36
+
37
+ function encryptBlock(x: number, halfBits: number, keys: number[]): number {
38
+ const mask = (1 << halfBits) - 1
39
+ let L = (x >>> halfBits) & mask
40
+ let R = x & mask
41
+ for (let i = 0; i < ROUNDS; i++) {
42
+ const F = roundFn(R, keys[i], halfBits)
43
+ const nL = R
44
+ const nR = (L ^ F) & mask
45
+ L = nL
46
+ R = nR
47
+ }
48
+ return ((L << halfBits) | R) >>> 0
49
+ }
50
+
51
+ function decryptBlock(y: number, halfBits: number, keys: number[]): number {
52
+ const mask = (1 << halfBits) - 1
53
+ let L = (y >>> halfBits) & mask
54
+ let R = y & mask
55
+ for (let i = ROUNDS - 1; i >= 0; i--) {
56
+ const F = roundFn(L, keys[i], halfBits)
57
+ const nR = L
58
+ const nL = (R ^ F) & mask
59
+ L = nL
60
+ R = nR
61
+ }
62
+ return ((L << halfBits) | R) >>> 0
63
+ }
64
+
65
+ export function permute(seed: Checksum256Type, x: number, cfg: FeistelConfig): number {
66
+ const keys = deriveRoundKeys(seed, cfg.label)
67
+ let v = encryptBlock(x, cfg.halfBits, keys)
68
+ while (v >= cfg.n) v = encryptBlock(v, cfg.halfBits, keys)
69
+ return v
70
+ }
71
+
72
+ export function unpermute(seed: Checksum256Type, y: number, cfg: FeistelConfig): number {
73
+ const keys = deriveRoundKeys(seed, cfg.label)
74
+ let v = decryptBlock(y, cfg.halfBits, keys)
75
+ while (v >= cfg.n) v = decryptBlock(v, cfg.halfBits, keys)
76
+ return v
77
+ }
@@ -0,0 +1,48 @@
1
+ import type {Checksum256Type} from '@wharfkit/antelope'
2
+ import {REGION_FEISTEL, REGION_PER_AXIS} from './constants'
3
+ import {permute, unpermute} from './permutation'
4
+
5
+ // FROZEN INTERFACE — phoneme tables; tune aesthetics before launch, then never reorder.
6
+ const ONSETS = ['b', 'd', 'f', 'g', 'k', 'l', 'm', 'n', 'p', 'r', 's', 't'] // 12
7
+ const VOWELS = ['a', 'e', 'i', 'o', 'u'] // 5
8
+ const CODAS = ['n', 'r', 'l', 's', 'k', 'm', 't', 'x'] // 8
9
+ const SYL_BASE = ONSETS.length * VOWELS.length * CODAS.length // 480
10
+
11
+ function syllable(digit: number): string {
12
+ const onset = Math.floor(digit / (VOWELS.length * CODAS.length))
13
+ const rem = digit % (VOWELS.length * CODAS.length)
14
+ const vowel = Math.floor(rem / CODAS.length)
15
+ const coda = rem % CODAS.length
16
+ return ONSETS[onset] + VOWELS[vowel] + CODAS[coda]
17
+ }
18
+
19
+ function unsyllable(chunk: string): number {
20
+ const onset = ONSETS.indexOf(chunk[0])
21
+ const vowel = VOWELS.indexOf(chunk[1])
22
+ const coda = CODAS.indexOf(chunk[2])
23
+ if (onset < 0 || vowel < 0 || coda < 0) throw new Error(`invalid region token chunk: ${chunk}`)
24
+ return onset * (VOWELS.length * CODAS.length) + vowel * CODAS.length + coda
25
+ }
26
+
27
+ export function encodeRegion(seed: Checksum256Type, rx: number, ry: number): string {
28
+ const index = rx * REGION_PER_AXIS + ry
29
+ let n = permute(seed, index, REGION_FEISTEL)
30
+ const d0 = n % SYL_BASE
31
+ n = Math.floor(n / SYL_BASE)
32
+ const d1 = n % SYL_BASE
33
+ const d2 = Math.floor(n / SYL_BASE)
34
+ const token = syllable(d2) + syllable(d1) + syllable(d0)
35
+ return token.charAt(0).toUpperCase() + token.slice(1)
36
+ }
37
+
38
+ export function decodeRegion(seed: Checksum256Type, token: string): {rx: number; ry: number} {
39
+ if (token.length !== 9) throw new Error(`invalid region token length: ${token}`)
40
+ const lower = token.toLowerCase()
41
+ const d2 = unsyllable(lower.slice(0, 3))
42
+ const d1 = unsyllable(lower.slice(3, 6))
43
+ const d0 = unsyllable(lower.slice(6, 9))
44
+ const scrambled = (d2 * SYL_BASE + d1) * SYL_BASE + d0
45
+ if (scrambled >= REGION_FEISTEL.n) throw new Error(`invalid region token: ${token}`)
46
+ const index = unpermute(seed, scrambled, REGION_FEISTEL)
47
+ return {rx: Math.floor(index / REGION_PER_AXIS), ry: index % REGION_PER_AXIS}
48
+ }
@@ -0,0 +1,115 @@
1
+ import type {Checksum256Type} from '@wharfkit/antelope'
2
+ import {SECTOR_FEISTEL, SECTORS_PER_AXIS} from './constants'
3
+ import {permute, unpermute} from './permutation'
4
+
5
+ // FROZEN INTERFACE — curation seed; review before launch, then never reorder.
6
+ export const SECTOR_ADJECTIVES: readonly string[] = [
7
+ 'Amber',
8
+ 'Azure',
9
+ 'Brass',
10
+ 'Cinder',
11
+ 'Cobalt',
12
+ 'Copper',
13
+ 'Coral',
14
+ 'Crimson',
15
+ 'Crystal',
16
+ 'Dusk',
17
+ 'Ember',
18
+ 'Emerald',
19
+ 'Frost',
20
+ 'Glimmer',
21
+ 'Golden',
22
+ 'Hazy',
23
+ 'Indigo',
24
+ 'Iron',
25
+ 'Ivory',
26
+ 'Jade',
27
+ 'Lunar',
28
+ 'Misty',
29
+ 'Neon',
30
+ 'Onyx',
31
+ 'Opal',
32
+ 'Pearl',
33
+ 'Plasma',
34
+ 'Quartz',
35
+ 'Rusty',
36
+ 'Saffron',
37
+ 'Scarlet',
38
+ 'Silver',
39
+ 'Solar',
40
+ 'Static',
41
+ 'Stormy',
42
+ 'Sunny',
43
+ 'Teal',
44
+ 'Umber',
45
+ 'Velvet',
46
+ 'Verdant',
47
+ 'Vermilion',
48
+ 'Violet',
49
+ 'Wispy',
50
+ ]
51
+
52
+ export const SECTOR_NOUNS: readonly string[] = [
53
+ 'Belt',
54
+ 'Bluff',
55
+ 'Cluster',
56
+ 'Coil',
57
+ 'Crest',
58
+ 'Drift',
59
+ 'Expanse',
60
+ 'Fathom',
61
+ 'Flare',
62
+ 'Gulf',
63
+ 'Halo',
64
+ 'Haven',
65
+ 'Hollow',
66
+ 'Maw',
67
+ 'Mesa',
68
+ 'Mire',
69
+ 'Notch',
70
+ 'Nook',
71
+ 'Oasis',
72
+ 'Lagoon',
73
+ 'Peak',
74
+ 'Pocket',
75
+ 'Reach',
76
+ 'Reef',
77
+ 'Ridge',
78
+ 'Rift',
79
+ 'Run',
80
+ 'Shoal',
81
+ 'Shroud',
82
+ 'Span',
83
+ 'Spire',
84
+ 'Spur',
85
+ 'Stretch',
86
+ 'Sprawl',
87
+ 'Tangle',
88
+ 'Trace',
89
+ 'Trench',
90
+ 'Vale',
91
+ 'Vault',
92
+ 'Verge',
93
+ 'Vortex',
94
+ 'Ward',
95
+ 'Wisp',
96
+ ]
97
+
98
+ export function encodeSector(seed: Checksum256Type, sx: number, sy: number): string {
99
+ const index = sx * SECTORS_PER_AXIS + sy
100
+ const scrambled = permute(seed, index, SECTOR_FEISTEL)
101
+ const adj = Math.floor(scrambled / SECTORS_PER_AXIS)
102
+ const noun = scrambled % SECTORS_PER_AXIS
103
+ return `${SECTOR_ADJECTIVES[adj]} ${SECTOR_NOUNS[noun]}`
104
+ }
105
+
106
+ export function decodeSector(seed: Checksum256Type, name: string): {sx: number; sy: number} {
107
+ const parts = name.trim().split(/\s+/)
108
+ if (parts.length !== 2) throw new Error(`invalid sector name: ${name}`)
109
+ const adj = SECTOR_ADJECTIVES.indexOf(parts[0])
110
+ const noun = SECTOR_NOUNS.indexOf(parts[1])
111
+ if (adj < 0 || noun < 0) throw new Error(`unknown sector name: ${name}`)
112
+ const scrambled = adj * SECTORS_PER_AXIS + noun
113
+ const index = unpermute(seed, scrambled, SECTOR_FEISTEL)
114
+ return {sx: Math.floor(index / SECTORS_PER_AXIS), sy: index % SECTORS_PER_AXIS}
115
+ }
@@ -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/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.'
@@ -87,6 +87,7 @@ export type {EpochInfo} from './scheduling/epoch'
87
87
  export {
88
88
  getSystemName,
89
89
  hasSystem,
90
+ getLocationKind,
90
91
  getLocationType,
91
92
  getLocationTypeName,
92
93
  isGatherableLocation,
@@ -164,6 +165,7 @@ export {
164
165
  calc_ship_mass,
165
166
  calc_ship_rechargetime,
166
167
  calc_transfer_duration,
168
+ calc_transit_duration,
167
169
  calculateFlightTime,
168
170
  calculateLoadTimeBreakdown,
169
171
  calculateRefuelingTime,
@@ -232,6 +234,15 @@ export type {TaskCargoChange, TaskCargoDirection} from './scheduling/task-cargo'
232
234
  export {composeIdleResolve} from './scheduling/idle-resolve'
233
235
  export type {CounterpartLookup, IdleResolveTarget} from './scheduling/idle-resolve'
234
236
 
237
+ export {cancelEligibility, CancelBlockReason} from './scheduling/cancel'
238
+ export type {
239
+ CancelPlan,
240
+ CancelEffects,
241
+ CancelRefund,
242
+ CancelReleasedHold,
243
+ CancelEligibilityInput,
244
+ } from './scheduling/cancel'
245
+
235
246
  export {
236
247
  projectedCargoAvailableAt,
237
248
  availableForItem,
@@ -356,6 +367,17 @@ export {
356
367
  } from './derivation/capabilities'
357
368
  export type {GathererDepthParams, ComputedCapabilities} from './derivation/capabilities'
358
369
 
370
+ export {
371
+ WH,
372
+ feistel,
373
+ feistelInv,
374
+ regionOf,
375
+ partnerRegion,
376
+ wormholeAt,
377
+ wormholeAtRegionEndpoint,
378
+ isValidWormholePair,
379
+ } from './derivation/wormhole'
380
+
359
381
  export {resolveItem} from './resolution/resolve-item'
360
382
  export type {
361
383
  ResolvedItem,
@@ -460,6 +482,7 @@ export {
460
482
  } from './data/tiers'
461
483
 
462
484
  export {formatMass, formatMassDelta, formatMassScaled, formatLocation} from './format'
485
+ export * from './coordinates'
463
486
 
464
487
  export {displayName, baseName, describeItem} from './resolution/display-name'
465
488
  export type {DescribeOptions} from './resolution/display-name'
@@ -3,6 +3,7 @@ import {
3
3
  Checksum256,
4
4
  type Checksum256Type,
5
5
  Int64,
6
+ type Int64Type,
6
7
  Name,
7
8
  type NameType,
8
9
  UInt8,
@@ -37,13 +38,17 @@ export class ActionsManager extends BaseManager {
37
38
  })
38
39
  }
39
40
 
40
- grouptravel(entities: EntityRefInput[], destination: CoordinatesType, recharge = true): Action {
41
- const entityRefs = entities.map((e) =>
41
+ private entityRefs(entities: EntityRefInput[]) {
42
+ return entities.map((e) =>
42
43
  ServerContract.Types.entity_ref.from({
43
44
  entity_type: e.entityType,
44
45
  entity_id: UInt64.from(e.entityId),
45
46
  })
46
47
  )
48
+ }
49
+
50
+ grouptravel(entities: EntityRefInput[], destination: CoordinatesType, recharge = true): Action {
51
+ const entityRefs = this.entityRefs(entities)
47
52
  const x = Int64.from(destination.x)
48
53
  const y = Int64.from(destination.y)
49
54
 
@@ -55,6 +60,44 @@ export class ActionsManager extends BaseManager {
55
60
  })
56
61
  }
57
62
 
63
+ transit(shipId: UInt64Type, entrance: CoordinatesType, exit: CoordinatesType): Action {
64
+ return this.server.action('transit', {
65
+ id: UInt64.from(shipId),
66
+ ax: Int64.from(entrance.x),
67
+ ay: Int64.from(entrance.y),
68
+ bx: Int64.from(exit.x),
69
+ by: Int64.from(exit.y),
70
+ })
71
+ }
72
+
73
+ grouptransit(
74
+ entities: EntityRefInput[],
75
+ entrance: CoordinatesType,
76
+ exit: CoordinatesType
77
+ ): Action {
78
+ const entityRefs = this.entityRefs(entities)
79
+ return this.server.action('grouptransit', {
80
+ entities: entityRefs,
81
+ ax: Int64.from(entrance.x),
82
+ ay: Int64.from(entrance.y),
83
+ bx: Int64.from(exit.x),
84
+ by: Int64.from(exit.y),
85
+ })
86
+ }
87
+
88
+ getwormhole(x: Int64Type, y: Int64Type): Action {
89
+ return this.server.action('getwormhole', {x: Int64.from(x), y: Int64.from(y)})
90
+ }
91
+
92
+ getdistance(origin: CoordinatesType, destination: CoordinatesType): Action {
93
+ return this.server.action('getdistance', {
94
+ ax: Int64.from(origin.x),
95
+ ay: Int64.from(origin.y),
96
+ bx: Int64.from(destination.x),
97
+ by: Int64.from(destination.y),
98
+ })
99
+ }
100
+
58
101
  resolve(entityId: UInt64Type, count?: UInt64Type): Action {
59
102
  const params: ServerContract.ActionParams.resolve = {
60
103
  id: UInt64.from(entityId),
@@ -36,7 +36,7 @@ export function taskCargoEffect(task: Task): CargoEffect {
36
36
  }
37
37
  }
38
38
 
39
- function cargoKey(item: CargoItem): string {
39
+ export function cargoKey(item: CargoItem): string {
40
40
  const base = `${item.item_id.toNumber()}:${item.stats.toString()}`
41
41
  const modules = item.modules ?? []
42
42
  const entityId = item.entity_id?.toString()