@shipload/sdk 2.0.0-rc11 → 2.0.0-rc12

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipload/sdk",
3
3
  "description": "SDKs for Shipload",
4
- "version": "2.0.0-rc11",
4
+ "version": "2.0.0-rc12",
5
5
  "homepage": "https://github.com/shipload/sdk",
6
6
  "license": "MIT",
7
7
  "main": "lib/shipload.js",
@@ -1,7 +1,8 @@
1
- import {UInt32, UInt64, UInt64Type} from '@wharfkit/antelope'
1
+ import {UInt16, UInt32, UInt64, UInt64Type} from '@wharfkit/antelope'
2
2
  import {ServerContract} from '../contracts'
3
3
  import {StorageCapability} from '../types/capabilities'
4
4
  import {getItem} from '../market/items'
5
+ import {INSUFFICIENT_ITEM_QUANTITY} from '../errors'
5
6
 
6
7
  export interface HasCargo {
7
8
  cargo: ServerContract.Types.cargo_item[]
@@ -15,7 +16,13 @@ export interface HasCargomass {
15
16
  cargomass: UInt32
16
17
  }
17
18
 
18
- export function calcCargoItemMass(item: ServerContract.Types.cargo_item): UInt64 {
19
+ interface MassInput {
20
+ item_id: UInt16
21
+ quantity: UInt32
22
+ modules: ServerContract.Types.module_entry[]
23
+ }
24
+
25
+ export function calcCargoItemMass(item: MassInput): UInt64 {
19
26
  const itemDef = getItem(item.item_id)
20
27
  let mass = UInt64.from(itemDef.mass).multiplying(item.quantity)
21
28
 
@@ -37,6 +44,14 @@ export function calcCargoMass(entity: HasCargo): UInt64 {
37
44
  return mass
38
45
  }
39
46
 
47
+ export function calcStacksMass(stacks: CargoStack[]): UInt64 {
48
+ let mass = UInt64.from(0)
49
+ for (const s of stacks) {
50
+ mass = mass.adding(calcCargoItemMass(s))
51
+ }
52
+ return mass
53
+ }
54
+
40
55
  export function availableCapacity(entity: StorageCapability): UInt64 {
41
56
  const cargoMass = calcCargoMass(entity)
42
57
  return entity.capacity.gt(cargoMass)
@@ -70,3 +85,78 @@ export function isFull(entity: HasCapacity & HasCargomass): boolean {
70
85
  export function isFullFromMass(capacity: UInt64Type, cargoMass: UInt64Type): boolean {
71
86
  return UInt64.from(cargoMass).gte(capacity)
72
87
  }
88
+
89
+ export interface CargoStack {
90
+ item_id: UInt16
91
+ quantity: UInt32
92
+ seed?: UInt64
93
+ modules: ServerContract.Types.module_entry[]
94
+ }
95
+
96
+ export function cargoItemToStack(item: ServerContract.Types.cargo_item): CargoStack {
97
+ return {
98
+ item_id: UInt16.from(item.item_id),
99
+ quantity: UInt32.from(item.quantity),
100
+ seed: item.seed,
101
+ modules: item.modules ?? [],
102
+ }
103
+ }
104
+
105
+ export function stackToCargoItem(stack: CargoStack): ServerContract.Types.cargo_item {
106
+ return ServerContract.Types.cargo_item.from({
107
+ item_id: stack.item_id,
108
+ quantity: stack.quantity,
109
+ seed: stack.seed,
110
+ modules: stack.modules,
111
+ })
112
+ }
113
+
114
+ function seedEquals(a?: UInt64, b?: UInt64): boolean {
115
+ if (!a && !b) return true
116
+ if (!a || !b) return false
117
+ return a.equals(b)
118
+ }
119
+
120
+ function stackIdentityEqual(a: CargoStack, b: CargoStack): boolean {
121
+ return a.item_id.equals(b.item_id) && seedEquals(a.seed, b.seed)
122
+ }
123
+
124
+ export function stackKey(s: CargoStack): string {
125
+ const seedVal = s.seed ? s.seed.toString() : '0'
126
+ return `${s.item_id.toNumber()}:${seedVal}`
127
+ }
128
+
129
+ export function stacksEqual(a: CargoStack, b: CargoStack): boolean {
130
+ return stackIdentityEqual(a, b) && a.quantity.equals(b.quantity)
131
+ }
132
+
133
+ export function mergeStacks(stacks: CargoStack[], add: CargoStack): CargoStack[] {
134
+ const idx = stacks.findIndex((s) => stackIdentityEqual(s, add))
135
+ if (idx === -1) {
136
+ return [...stacks, {...add}]
137
+ }
138
+ const result = stacks.slice()
139
+ result[idx] = {
140
+ ...result[idx],
141
+ quantity: UInt32.from(result[idx].quantity.adding(add.quantity)),
142
+ }
143
+ return result
144
+ }
145
+
146
+ export function removeFromStacks(stacks: CargoStack[], remove: CargoStack): CargoStack[] {
147
+ const idx = stacks.findIndex((s) => stackIdentityEqual(s, remove))
148
+ if (idx === -1) {
149
+ throw new Error(INSUFFICIENT_ITEM_QUANTITY)
150
+ }
151
+ const target = stacks[idx]
152
+ if (target.quantity.lt(remove.quantity)) {
153
+ throw new Error(INSUFFICIENT_ITEM_QUANTITY)
154
+ }
155
+ const remaining = UInt32.from(target.quantity.subtracting(remove.quantity))
156
+ if (remaining.equals(UInt32.from(0))) {
157
+ return [...stacks.slice(0, idx), ...stacks.slice(idx + 1)]
158
+ }
159
+ const result = stacks.slice()
160
+ result[idx] = {...target, quantity: remaining}
161
+ return result
162
+ }
@@ -20,7 +20,7 @@ const categories: CategoryInfo[] = [
20
20
  {
21
21
  id: 'precious',
22
22
  name: 'Precious Metals',
23
- label: 'Precious',
23
+ label: 'Precious Metal',
24
24
  description:
25
25
  'Conductive, corrosion-resistant — electronics, energy systems, precision components',
26
26
  color: categoryColors.precious,
@@ -4,9 +4,11 @@ import {
4
4
  entityRecipes,
5
5
  getComponentById,
6
6
  getEntityRecipe,
7
+ getEntityRecipeByItemId,
7
8
  getModuleRecipe,
8
9
  moduleRecipes,
9
10
  } from '../data/recipes'
11
+ import {getStatDefinitions} from './stats'
10
12
  import {deriveResourceStats} from './stratum'
11
13
 
12
14
  export interface StackInput {
@@ -195,3 +197,77 @@ export function blendCargoStacks(
195
197
  const blended = allKeys.map((key) => Math.max(1, Math.min(999, blendStacks(decoded, key))))
196
198
  return UInt64.from(encodeStats(blended))
197
199
  }
200
+
201
+ export interface RecipeSlotInput {
202
+ itemId: number
203
+ category: ResourceCategory | undefined
204
+ stacks: {quantity: number; seed: bigint}[]
205
+ }
206
+
207
+ function decodeRawStackToCategoryStats(
208
+ seed: bigint,
209
+ category: ResourceCategory
210
+ ): Record<string, number> {
211
+ const raw = deriveResourceStats(seed)
212
+ const defs = getStatDefinitions(category)
213
+ const result: Record<string, number> = {}
214
+ if (defs[0]) result[defs[0].key] = raw.stat1
215
+ if (defs[1]) result[defs[1].key] = raw.stat2
216
+ if (defs[2]) result[defs[2].key] = raw.stat3
217
+ return result
218
+ }
219
+
220
+ export function computeCraftedOutputSeed(
221
+ outputItemId: number,
222
+ slotInputs: RecipeSlotInput[]
223
+ ): UInt64 {
224
+ const component = getComponentById(outputItemId)
225
+ if (component) {
226
+ const categoryStacks: CategoryStacks[] = []
227
+ for (const slot of slotInputs) {
228
+ if (!slot.category) continue
229
+ const slotIsComponent = getComponentById(slot.itemId) !== undefined
230
+ const stacks: StackInput[] = slot.stacks.map((s) => ({
231
+ quantity: s.quantity,
232
+ stats: slotIsComponent
233
+ ? decodeCraftedItemStats(slot.itemId, s.seed)
234
+ : decodeRawStackToCategoryStats(s.seed, slot.category!),
235
+ }))
236
+ categoryStacks.push({category: slot.category, stacks})
237
+ }
238
+ const stats = computeComponentStats(outputItemId, categoryStacks)
239
+ const ordered = component.stats.map((statDef) => {
240
+ const found = stats.find((s) => s.key === statDef.key)
241
+ return found ? found.value : 0
242
+ })
243
+ return UInt64.from(encodeStats(ordered))
244
+ }
245
+
246
+ const entityRecipe = getEntityRecipeByItemId(outputItemId)
247
+ if (entityRecipe) {
248
+ const componentStacks: Record<number, {quantity: number; stats: Record<string, number>}[]> =
249
+ {}
250
+ for (const slot of slotInputs) {
251
+ if (slot.category !== undefined) {
252
+ throw new Error(
253
+ `entity recipe ${entityRecipe.id} expects component inputs but slot itemId=${slot.itemId} has category=${slot.category}`
254
+ )
255
+ }
256
+ const list = (componentStacks[slot.itemId] ??= [])
257
+ for (const s of slot.stacks) {
258
+ list.push({
259
+ quantity: s.quantity,
260
+ stats: decodeCraftedItemStats(slot.itemId, s.seed),
261
+ })
262
+ }
263
+ }
264
+ const stats = computeEntityStats(entityRecipe.id, componentStacks)
265
+ const ordered = entityRecipe.stats.map((statDef) => {
266
+ const found = stats.find((s) => s.key === statDef.key)
267
+ return found ? found.value : 0
268
+ })
269
+ return UInt64.from(encodeStats(ordered))
270
+ }
271
+
272
+ throw new Error(`computeCraftedOutputSeed: no recipe found for outputItemId=${outputItemId}`)
273
+ }
@@ -7,7 +7,6 @@ export {
7
7
  getLocationCandidates,
8
8
  getDepthThreshold,
9
9
  getResourceTier,
10
- depthScaleFactor,
11
10
  DEPTH_THRESHOLD_T1,
12
11
  DEPTH_THRESHOLD_T2,
13
12
  DEPTH_THRESHOLD_T3,
@@ -24,5 +23,8 @@ export {
24
23
  PLANET_SUBTYPE_INDUSTRIAL,
25
24
  } from './resources'
26
25
 
26
+ export {RESERVE_TIERS, TIER_ROLL_MAX, rollTier, rollWithinTier} from './tiers'
27
+ export type {ReserveTier, TierRange} from './tiers'
28
+
27
29
  export * from './stats'
28
30
  export * from './crafting'
@@ -9,7 +9,7 @@ export const DEPTH_THRESHOLD_T5 = 55000
9
9
  export const LOCATION_MIN_DEPTH = 500
10
10
  export const LOCATION_MAX_DEPTH = 65535
11
11
 
12
- export const YIELD_THRESHOLD = Math.floor(0.003 * 0xffffffff)
12
+ export const YIELD_THRESHOLD = Math.floor(0.001 * 0xffffffff)
13
13
 
14
14
  export const PLANET_SUBTYPE_GAS_GIANT = 0
15
15
  export const PLANET_SUBTYPE_ROCKY = 1
@@ -134,9 +134,3 @@ export function getEligibleResources(
134
134
  return stratum >= threshold
135
135
  })
136
136
  }
137
-
138
- export function depthScaleFactor(stratum: number): number {
139
- if (stratum <= 1) return 1.0
140
- const logScale = Math.log(stratum) / Math.log(65535)
141
- return 1.0 + logScale * 2.0
142
- }
@@ -1,12 +1,8 @@
1
1
  import {Bytes, Checksum256, Checksum256Type} from '@wharfkit/antelope'
2
2
  import {hash512} from '../utils/hash'
3
3
  import {Coordinates, CoordinatesType} from '../types'
4
- import {
5
- depthScaleFactor,
6
- getEligibleResources,
7
- getResourceWeight,
8
- YIELD_THRESHOLD,
9
- } from './resources'
4
+ import {getEligibleResources, getResourceWeight, YIELD_THRESHOLD} from './resources'
5
+ import {RESERVE_TIERS, rollTier, rollWithinTier} from './tiers'
10
6
 
11
7
  export interface StratumInfo {
12
8
  itemId: number
@@ -39,9 +35,10 @@ export function deriveStratum(
39
35
 
40
36
  let reserve = 0
41
37
  if (rawReserve <= YIELD_THRESHOLD) {
42
- const baseReserve = (rawReserve % 333) + 1
43
- const scale = depthScaleFactor(stratum)
44
- reserve = Math.floor(baseReserve * scale)
38
+ const tierRoll = ((bytes[18] << 8) | bytes[19]) >>> 0
39
+ const withinRoll = ((bytes[20] << 8) | bytes[21]) >>> 0
40
+ const tier = rollTier(tierRoll, stratum)
41
+ reserve = rollWithinTier(withinRoll, RESERVE_TIERS[tier])
45
42
  }
46
43
 
47
44
  if (reserve === 0) return {itemId: 0, seed: 0n, richness: 0, reserve: 0}
@@ -105,11 +102,12 @@ export function deriveResourceStats(seed: bigint): ResourceStats {
105
102
  (hashBytes[offset] * 0x1000000 +
106
103
  (hashBytes[offset + 1] << 16) +
107
104
  (hashBytes[offset + 2] << 8) +
108
- hashBytes[offset + 3]) >>> 0
105
+ hashBytes[offset + 3]) >>>
106
+ 0
109
107
 
110
108
  const weibull = (raw: number): number => {
111
109
  const u = raw / 4294967296
112
- let x = 0.27 * Math.sqrt(-Math.log(1 - u))
110
+ let x = 0.24 * Math.sqrt(-Math.log(1 - u))
113
111
  if (x > 1) x = 1
114
112
  let val = Math.floor(x * 999) + 1
115
113
  if (val > 999) val = 999
@@ -0,0 +1,54 @@
1
+ export type ReserveTier = 'small' | 'medium' | 'large' | 'massive' | 'motherlode'
2
+
3
+ export interface TierRange {
4
+ min: number
5
+ max: number
6
+ }
7
+
8
+ export const RESERVE_TIERS: Record<ReserveTier, TierRange> = {
9
+ small: {min: 15, max: 60},
10
+ medium: {min: 100, max: 200},
11
+ large: {min: 400, max: 700},
12
+ massive: {min: 1000, max: 2500},
13
+ motherlode: {min: 4000, max: 10000},
14
+ }
15
+
16
+ const SHALLOW_THRESHOLDS = {
17
+ small: 0.8,
18
+ medium: 0.991946,
19
+ large: 0.999946,
20
+ massive: 0.999996,
21
+ }
22
+
23
+ const DEEP_THRESHOLDS = {
24
+ small: 0.5,
25
+ medium: 0.95892,
26
+ large: 0.99892,
27
+ massive: 0.99992,
28
+ }
29
+
30
+ export const TIER_ROLL_MAX = 0x10000 // 65536
31
+
32
+ function lerp(a: number, b: number, t: number): number {
33
+ return a + (b - a) * t
34
+ }
35
+
36
+ export function rollTier(tierRoll: number, stratum: number): ReserveTier {
37
+ const d = Math.min(stratum, 65535) / 65535
38
+ const smallMax = lerp(SHALLOW_THRESHOLDS.small, DEEP_THRESHOLDS.small, d) * TIER_ROLL_MAX
39
+ const mediumMax = lerp(SHALLOW_THRESHOLDS.medium, DEEP_THRESHOLDS.medium, d) * TIER_ROLL_MAX
40
+ const largeMax = lerp(SHALLOW_THRESHOLDS.large, DEEP_THRESHOLDS.large, d) * TIER_ROLL_MAX
41
+ const massiveMax = lerp(SHALLOW_THRESHOLDS.massive, DEEP_THRESHOLDS.massive, d) * TIER_ROLL_MAX
42
+
43
+ if (tierRoll < smallMax) return 'small'
44
+ if (tierRoll < mediumMax) return 'medium'
45
+ if (tierRoll < largeMax) return 'large'
46
+ if (tierRoll < massiveMax) return 'massive'
47
+ return 'motherlode'
48
+ }
49
+
50
+ export function rollWithinTier(withinRoll: number, range: TierRange): number {
51
+ const u = withinRoll / 65535
52
+ const skewed = u * u
53
+ return Math.floor(range.min + skewed * (range.max - range.min))
54
+ }
package/src/errors.ts CHANGED
@@ -44,3 +44,8 @@ export const SHIP_CARGO_NOT_LOADED = 'Cannot unload cargo that is not loaded.'
44
44
  export const WAREHOUSE_NOT_FOUND = 'Cannot find warehouse for given id.'
45
45
  export const WAREHOUSE_ALREADY_AT_LOCATION = 'Warehouse already exists at this location.'
46
46
  export const WAREHOUSE_CAPACITY_EXCEEDED = 'Warehouse capacity would be exceeded.'
47
+ export const ENTITY_CAPACITY_EXCEEDED = 'Entity cargo capacity would be exceeded.'
48
+ export const RECIPE_INPUTS_INSUFFICIENT = 'Insufficient inputs for recipe.'
49
+ export const RECIPE_INPUTS_INVALID = 'Input cargo does not match recipe requirements.'
50
+ export const RECIPE_INPUTS_EXCESS = 'Provided inputs exceed recipe requirements.'
51
+ export const RECIPE_INPUTS_MIXED = 'All stacks for a recipe input must be the same resource.'
@@ -66,7 +66,6 @@ export {
66
66
  getLocationCandidates,
67
67
  getDepthThreshold,
68
68
  getResourceTier,
69
- depthScaleFactor,
70
69
  DEPTH_THRESHOLD_T1,
71
70
  DEPTH_THRESHOLD_T2,
72
71
  DEPTH_THRESHOLD_T3,
@@ -84,6 +83,9 @@ export {
84
83
 
85
84
  export type {StratumInfo, ResourceStats} from './derivation'
86
85
 
86
+ export {RESERVE_TIERS, TIER_ROLL_MAX, rollTier, rollWithinTier} from './derivation'
87
+ export type {ReserveTier, TierRange} from './derivation'
88
+
87
89
  export {getStatDefinitions, getStatName, resolveStats} from './derivation'
88
90
  export type {StatDefinition, NamedStats} from './derivation'
89
91
 
@@ -135,7 +137,12 @@ export type {HasCargo} from './entities/inventory-accessor'
135
137
  export * as cargoUtils from './entities/cargo-utils'
136
138
  export type {CargoData} from './entities/cargo-utils'
137
139
 
138
- export {projectEntity, projectEntityAt, createProjectedEntity} from './scheduling/projection'
140
+ export {
141
+ createProjectedEntity,
142
+ projectEntity,
143
+ projectEntityAt,
144
+ validateSchedule,
145
+ } from './scheduling/projection'
139
146
  export type {Projectable, ProjectedEntity} from './scheduling/projection'
140
147
 
141
148
  export * from './types/capabilities'
@@ -230,8 +237,9 @@ export {
230
237
  blendCrossGroup,
231
238
  categoryItemMass,
232
239
  computeInputMass,
240
+ computeCraftedOutputSeed,
233
241
  } from './derivation/crafting'
234
- export type {StackInput, CategoryStacks} from './derivation/crafting'
242
+ export type {StackInput, CategoryStacks, RecipeSlotInput} from './derivation/crafting'
235
243
 
236
244
  export {computeContainerCapabilities, computeContainerT2Capabilities} from './entities/container'
237
245
 
@@ -1,9 +1,14 @@
1
1
  import {UInt16, UInt16Type} from '@wharfkit/antelope'
2
2
  import {Item} from '../types'
3
3
  import itemsData from '../data/items.json'
4
+ import {computeInputMass} from '../derivation/crafting'
5
+ import {getComponentById, getEntityRecipeByItemId, getModuleRecipeByItemId} from '../data/recipes'
4
6
 
5
- const items: Item[] = itemsData.map((g) =>
6
- Item.from({
7
+ const itemsById: Map<number, Item> = new Map()
8
+ const synthesizedCache: Map<number, Item> = new Map()
9
+
10
+ for (const g of itemsData) {
11
+ const item = Item.from({
7
12
  id: g.id,
8
13
  name: g.name,
9
14
  description: g.description,
@@ -12,19 +17,73 @@ const items: Item[] = itemsData.map((g) =>
12
17
  tier: g.tier,
13
18
  color: g.color,
14
19
  })
15
- )
20
+ itemsById.set(item.id.toNumber(), item)
21
+ }
22
+
23
+ export const itemIds = Array.from(itemsById.values(), (i) => i.id)
24
+
25
+ interface RecipeSource {
26
+ name: string
27
+ description: string
28
+ mass: number
29
+ color: string
30
+ }
31
+
32
+ function synthesizeItem(id: number, source: RecipeSource): Item {
33
+ return Item.from({
34
+ id,
35
+ name: source.name,
36
+ description: source.description,
37
+ mass: source.mass,
38
+ category: 'metal',
39
+ tier: 't1',
40
+ color: source.color,
41
+ })
42
+ }
43
+
44
+ function synthesizeFromRecipes(id: number): Item | undefined {
45
+ const component = getComponentById(id)
46
+ if (component) return synthesizeItem(id, component)
16
47
 
17
- export const itemIds = items.map((i) => i.id)
48
+ const entityRecipe = getEntityRecipeByItemId(id)
49
+ if (entityRecipe) {
50
+ return synthesizeItem(id, {
51
+ ...entityRecipe,
52
+ mass: computeInputMass(entityRecipe.id, 'entity'),
53
+ })
54
+ }
55
+
56
+ const moduleRecipe = getModuleRecipeByItemId(id)
57
+ if (moduleRecipe) {
58
+ return synthesizeItem(id, {
59
+ ...moduleRecipe,
60
+ mass: computeInputMass(moduleRecipe.id, 'module'),
61
+ })
62
+ }
63
+
64
+ return undefined
65
+ }
18
66
 
19
67
  export function getItem(itemId: UInt16Type): Item {
20
- const id = UInt16.from(itemId)
21
- const item = items.find((i) => i.id.equals(id))
22
- if (!item) {
23
- throw new Error(`Item with id ${id} not found`)
68
+ const id = UInt16.from(itemId).toNumber()
69
+ const existing = itemsById.get(id) ?? synthesizedCache.get(id)
70
+ if (existing) return existing
71
+
72
+ const synthesized = synthesizeFromRecipes(id)
73
+ if (synthesized) {
74
+ synthesizedCache.set(id, synthesized)
75
+ return synthesized
24
76
  }
25
- return item
77
+
78
+ throw new Error(`Item with id ${id} not found`)
26
79
  }
27
80
 
28
81
  export function getItems(): Item[] {
29
- return items
82
+ return Array.from(itemsById.values())
83
+ }
84
+
85
+ export function registerItem(item: Item): void {
86
+ const id = item.id.toNumber()
87
+ itemsById.set(id, item)
88
+ synthesizedCache.delete(id)
30
89
  }