@shipload/sdk 1.0.0-next.62 → 1.0.0-next.63

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.
Files changed (51) hide show
  1. package/lib/shipload.d.ts +1172 -29
  2. package/lib/shipload.js +3877 -1294
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +3781 -1298
  5. package/lib/shipload.m.js.map +1 -1
  6. package/lib/testing.d.ts +391 -13
  7. package/lib/testing.js +1554 -114
  8. package/lib/testing.js.map +1 -1
  9. package/lib/testing.m.js +1555 -115
  10. package/lib/testing.m.js.map +1 -1
  11. package/package.json +2 -2
  12. package/src/contracts/fund.ts +201 -0
  13. package/src/contracts/platform.ts +73 -1
  14. package/src/contracts/server.ts +946 -26
  15. package/src/contracts.ts +1 -0
  16. package/src/data/charters.json +198 -0
  17. package/src/data/entities.json +36 -0
  18. package/src/data/item-ids.ts +2 -0
  19. package/src/data/items.json +12 -0
  20. package/src/data/kind-registry.json +20 -3
  21. package/src/data/kind-registry.ts +5 -0
  22. package/src/data/metadata.ts +14 -0
  23. package/src/data/recipes.json +0 -48
  24. package/src/derivation/capabilities.ts +14 -0
  25. package/src/derivation/recipe-usage.test.ts +0 -2
  26. package/src/format.ts +9 -0
  27. package/src/index-module.ts +24 -1
  28. package/src/influence/categories.ts +32 -0
  29. package/src/influence/charters.test.ts +126 -0
  30. package/src/influence/charters.ts +120 -0
  31. package/src/influence/citizenry.ts +31 -0
  32. package/src/influence/constants.ts +63 -0
  33. package/src/influence/decay.ts +20 -0
  34. package/src/influence/decomp.ts +95 -0
  35. package/src/influence/demand.ts +106 -0
  36. package/src/influence/duration.ts +36 -0
  37. package/src/influence/index.ts +63 -0
  38. package/src/influence/influence.test.ts +291 -0
  39. package/src/influence/quality.ts +25 -0
  40. package/src/influence/valuation.ts +154 -0
  41. package/src/managers/actions.ts +135 -0
  42. package/src/managers/construction.ts +11 -1
  43. package/src/managers/context.ts +9 -0
  44. package/src/managers/entities.ts +11 -0
  45. package/src/managers/index.ts +12 -0
  46. package/src/managers/influence.test.ts +58 -0
  47. package/src/managers/influence.ts +347 -0
  48. package/src/nft/description.ts +7 -0
  49. package/src/scheduling/task-cargo.ts +1 -0
  50. package/src/shipload.ts +5 -0
  51. package/src/types.ts +23 -4
@@ -0,0 +1,291 @@
1
+ import {describe, expect, test} from 'bun:test'
2
+ import {Checksum256} from '@wharfkit/antelope'
3
+ import {ITEM_ORE_T1, ITEM_PLATE, ITEM_CRYSTAL_T1} from '../data/item-ids'
4
+ import {categoryIndex} from './categories'
5
+ import {
6
+ CHARTER_WORKSHOP_COST,
7
+ DEMAND_TRIPLE_SEED,
8
+ INFLUENCE_ATOMIC_PER_POINT,
9
+ INFLUENCE_D1_SEED,
10
+ NEED_FP_SCALE,
11
+ } from './constants'
12
+ import {decayActive, normalizeActive} from './decay'
13
+ import {
14
+ buildDemand,
15
+ deriveDemand,
16
+ lackingMaskFrom,
17
+ needForCategory,
18
+ pickAcuteCategory,
19
+ popcount5,
20
+ } from './demand'
21
+ import {findDecomp, DECOMP_REGISTRY} from './decomp'
22
+ import {contributeDuration} from './duration'
23
+ import {getStatCount, statsSumSq} from './quality'
24
+ import {pricingFromWeights, valueCargoItem, valueContribution} from './valuation'
25
+ import {citizenryName} from './citizenry'
26
+
27
+ const PAR = 213
28
+ const WORLD = {x: 56, y: 0}
29
+ const packStats = (a: number, b = a, c = a): bigint =>
30
+ BigInt(a) | (BigInt(b) << 10n) | (BigInt(c) << 20n)
31
+
32
+ const SEED_WEIGHTS_FP = [10000, 10510, 14289, 21964, 32326, 42658, 52913, 61128, 68570, 75380]
33
+
34
+ const pricing = pricingFromWeights(
35
+ INFLUENCE_D1_SEED,
36
+ Array.from({length: 5}, (_, category) =>
37
+ SEED_WEIGHTS_FP.map((weightFp, i) => ({category, tier: i + 1, weightFp}))
38
+ ).flat()
39
+ )
40
+
41
+ const baseDemand = buildDemand(0, 255, DEMAND_TRIPLE_SEED)
42
+
43
+ describe('decay', () => {
44
+ test('zero epochs is the identity', () => {
45
+ expect(decayActive(1_000_000n, 0)).toBe(1_000_000n)
46
+ })
47
+
48
+ test('one epoch retains 19/20 with a floor', () => {
49
+ expect(decayActive(1_000_000n, 1)).toBe(950_000n)
50
+ expect(decayActive(19n, 1)).toBe(18n)
51
+ })
52
+
53
+ test('is path independent', () => {
54
+ const direct = decayActive(123_456_789n, 7)
55
+ const staged = decayActive(decayActive(123_456_789n, 3), 4)
56
+ expect(staged).toBe(direct)
57
+ })
58
+
59
+ test('early-exits at zero and clamps the iteration count', () => {
60
+ expect(decayActive(0n, 5_000)).toBe(0n)
61
+ expect(decayActive(1_000_000n, 100_000)).toBe(decayActive(1_000_000n, 1_024))
62
+ })
63
+
64
+ test('normalizeActive never decays backwards', () => {
65
+ expect(normalizeActive({active: 1_000n, lastUpdateEpoch: 9}, 4)).toBe(1_000n)
66
+ })
67
+ })
68
+
69
+ describe('quality', () => {
70
+ test('par stats give exactly the quality divisor', () => {
71
+ expect(statsSumSq(packStats(PAR), 3)).toBe(3 * INFLUENCE_D1_SEED)
72
+ })
73
+
74
+ test('the retired Workshop falls back to three stats', () => {
75
+ expect(getStatCount(10208)).toBe(3)
76
+ })
77
+
78
+ test('a crafted component reports its recipe slot count', () => {
79
+ expect(getStatCount(ITEM_PLATE)).toBe(3)
80
+ })
81
+ })
82
+
83
+ describe('demand', () => {
84
+ test('the lacking mask is the complement over five categories', () => {
85
+ expect(lackingMaskFrom(0b00101)).toBe(0b11010)
86
+ expect(popcount5(0b11010)).toBe(3)
87
+ })
88
+
89
+ test('the acute pick indexes the lacking set only', () => {
90
+ const lacking = 0b10010
91
+ expect(pickAcuteCategory(lacking, 0)).toBe(1)
92
+ expect(pickAcuteCategory(lacking, 1)).toBe(4)
93
+ expect(pickAcuteCategory(lacking, 2)).toBe(1)
94
+ })
95
+
96
+ test('the triple assigns peak, base and floor', () => {
97
+ const view = buildDemand(0b00001, 1, DEMAND_TRIPLE_SEED)
98
+ expect(needForCategory(view, 0)).toBe(DEMAND_TRIPLE_SEED.floor)
99
+ expect(needForCategory(view, 1)).toBe(DEMAND_TRIPLE_SEED.peak)
100
+ expect(needForCategory(view, 2)).toBe(DEMAND_TRIPLE_SEED.base)
101
+ })
102
+
103
+ test('the acute pick rotates with the epoch seed, not the game seed', () => {
104
+ const gameSeed = Checksum256.hash(Buffer.from('game'))
105
+ const coordinates = WORLD
106
+ const picks = new Set<number>()
107
+ for (let i = 0; i < 24; i++) {
108
+ const epochSeed = Checksum256.hash(Buffer.from(`epoch-${i}`))
109
+ picks.add(
110
+ deriveDemand({gameSeed, epochSeed, coordinates, triple: DEMAND_TRIPLE_SEED}).acute
111
+ )
112
+ }
113
+ expect(picks.size).toBeGreaterThan(1)
114
+ })
115
+
116
+ test('the composition partition is stable across epochs', () => {
117
+ const gameSeed = Checksum256.hash(Buffer.from('game'))
118
+ const coordinates = WORLD
119
+ const masks = new Set<number>()
120
+ for (let i = 0; i < 8; i++) {
121
+ const epochSeed = Checksum256.hash(Buffer.from(`epoch-${i}`))
122
+ masks.add(
123
+ deriveDemand({gameSeed, epochSeed, coordinates, triple: DEMAND_TRIPLE_SEED})
124
+ .abundantMask
125
+ )
126
+ }
127
+ expect(masks.size).toBe(1)
128
+ })
129
+ })
130
+
131
+ describe('decomposition', () => {
132
+ test('the registry is sorted by item id and covers every recipe', () => {
133
+ for (let i = 1; i < DECOMP_REGISTRY.length; i++) {
134
+ expect(DECOMP_REGISTRY[i].itemId).toBeGreaterThan(DECOMP_REGISTRY[i - 1].itemId)
135
+ }
136
+ expect(DECOMP_REGISTRY.length).toBe(57)
137
+ })
138
+
139
+ test('the retired Workshop has no decomposition entry', () => {
140
+ expect(findDecomp(10208)).toBeUndefined()
141
+ })
142
+
143
+ test('buckets are sorted and hold only raw resources', () => {
144
+ const entry = findDecomp(ITEM_PLATE)
145
+ if (!entry) throw new Error('Plate has no decomposition entry')
146
+ expect(entry.processedKg).toBeGreaterThan(0)
147
+ for (let i = 1; i < entry.buckets.length; i++) {
148
+ const prev = entry.buckets[i - 1]
149
+ const cur = entry.buckets[i]
150
+ expect(prev.category < cur.category || prev.tier < cur.tier).toBe(true)
151
+ }
152
+ })
153
+ })
154
+
155
+ describe('valuation', () => {
156
+ test('a tonne of par T1 ore at base demand is worth exactly one point', () => {
157
+ const value = valueCargoItem(
158
+ {itemId: ITEM_ORE_T1, quantity: 1, stats: packStats(PAR)},
159
+ baseDemand,
160
+ pricing
161
+ )
162
+ expect(value).toBe(BigInt(INFLUENCE_ATOMIC_PER_POINT))
163
+ })
164
+
165
+ test('the acute multiplier doubles a resource', () => {
166
+ const acute = buildDemand(0, categoryIndex('crystal'), DEMAND_TRIPLE_SEED)
167
+ const item = {itemId: ITEM_CRYSTAL_T1, quantity: 3, stats: packStats(PAR)}
168
+ expect(valueCargoItem(item, acute, pricing)).toBe(
169
+ valueCargoItem(item, baseDemand, pricing) * 2n
170
+ )
171
+ })
172
+
173
+ test('resource valuation is split invariant', () => {
174
+ const one = valueCargoItem(
175
+ {itemId: ITEM_ORE_T1, quantity: 1, stats: packStats(97)},
176
+ baseDemand,
177
+ pricing
178
+ )
179
+ const many = valueCargoItem(
180
+ {itemId: ITEM_ORE_T1, quantity: 137, stats: packStats(97)},
181
+ baseDemand,
182
+ pricing
183
+ )
184
+ expect(many).toBe(one * 137n)
185
+ })
186
+
187
+ test('component valuation is split invariant', () => {
188
+ const one = valueCargoItem(
189
+ {itemId: ITEM_PLATE, quantity: 1, stats: packStats(PAR)},
190
+ baseDemand,
191
+ pricing
192
+ )
193
+ const many = valueCargoItem(
194
+ {itemId: ITEM_PLATE, quantity: 40, stats: packStats(PAR)},
195
+ baseDemand,
196
+ pricing
197
+ )
198
+ expect(many).toBe(one * 40n)
199
+ })
200
+
201
+ test('components ignore demand entirely', () => {
202
+ const acute = buildDemand(0, categoryIndex('ore'), DEMAND_TRIPLE_SEED)
203
+ const item = {itemId: ITEM_PLATE, quantity: 5, stats: packStats(PAR)}
204
+ expect(valueCargoItem(item, acute, pricing)).toBe(valueCargoItem(item, baseDemand, pricing))
205
+ })
206
+
207
+ test('quality scales quadratically within the per-unit floor tolerance', () => {
208
+ const single = valueCargoItem(
209
+ {itemId: ITEM_ORE_T1, quantity: 100, stats: packStats(100)},
210
+ baseDemand,
211
+ pricing
212
+ )
213
+ const doubled = valueCargoItem(
214
+ {itemId: ITEM_ORE_T1, quantity: 100, stats: packStats(200)},
215
+ baseDemand,
216
+ pricing
217
+ )
218
+ const delta = doubled - single * 4n
219
+ expect(delta >= -105n && delta <= 105n).toBe(true)
220
+ })
221
+
222
+ test('a bundle totals its rows', () => {
223
+ const bundle = [
224
+ {itemId: ITEM_ORE_T1, quantity: 10, stats: packStats(PAR)},
225
+ {itemId: ITEM_PLATE, quantity: 2, stats: packStats(PAR)},
226
+ ]
227
+ expect(valueContribution(bundle, baseDemand, pricing)).toBe(
228
+ valueCargoItem(bundle[0], baseDemand, pricing) +
229
+ valueCargoItem(bundle[1], baseDemand, pricing)
230
+ )
231
+ })
232
+
233
+ test('a packed entity cannot be valued', () => {
234
+ expect(() =>
235
+ valueCargoItem({itemId: 10208, quantity: 1, stats: packStats(PAR)}, baseDemand, pricing)
236
+ ).toThrow()
237
+ })
238
+
239
+ test('the Workshop charter costs 200,000 points of hauled par T1 material', () => {
240
+ const perTonne = valueCargoItem(
241
+ {itemId: ITEM_ORE_T1, quantity: 200_000, stats: packStats(PAR)},
242
+ baseDemand,
243
+ pricing
244
+ )
245
+ expect(perTonne).toBe(CHARTER_WORKSHOP_COST)
246
+ })
247
+ })
248
+
249
+ describe('contribution duration', () => {
250
+ test('grows with mass and never falls below a single tick', () => {
251
+ expect(contributeDuration(1_000)).toBeGreaterThan(0)
252
+ expect(contributeDuration(1_000_000)).toBeGreaterThan(contributeDuration(1_000))
253
+ })
254
+
255
+ test('splitting a haul costs more than one trip', () => {
256
+ const single = contributeDuration(100_000_000)
257
+ const hundred = contributeDuration(1_000_000) * 100
258
+ expect(hundred).toBeGreaterThan(single)
259
+ })
260
+
261
+ test('ground level is floored at the base orbital climb', () => {
262
+ expect(contributeDuration(1_000, 0)).toBe(contributeDuration(1_000, 800))
263
+ })
264
+ })
265
+
266
+ describe('citizenry names', () => {
267
+ const gameSeed = Checksum256.hash(Buffer.from('game'))
268
+
269
+ test('is deterministic for a coordinate', () => {
270
+ const a = citizenryName(gameSeed, WORLD)
271
+ const b = citizenryName(gameSeed, WORLD)
272
+ expect(a).toBeDefined()
273
+ expect(a).toBe(b as string)
274
+ })
275
+
276
+ test('composes the world name', () => {
277
+ const name = citizenryName(gameSeed, WORLD) ?? ''
278
+ expect(name.length).toBeGreaterThan(0)
279
+ expect(/Collective|Compact|Concord of|Union/.test(name)).toBe(true)
280
+ })
281
+
282
+ test('is undefined away from a world', () => {
283
+ expect(citizenryName(gameSeed, {x: 7, y: 11})).toBeUndefined()
284
+ })
285
+ })
286
+
287
+ describe('scales', () => {
288
+ test('the need scale divides out against the atomic unit', () => {
289
+ expect(NEED_FP_SCALE % INFLUENCE_ATOMIC_PER_POINT).toBe(0)
290
+ })
291
+ })
@@ -0,0 +1,25 @@
1
+ import {getRecipe} from '../data/recipes-runtime'
2
+ import {decodeStat} from '../derivation/crafting'
3
+
4
+ const CRAFTED_ITEM_ID_FLOOR = 10_000
5
+ const UNCRAFTED_STAT_COUNT = 3
6
+
7
+ export function getStatCount(itemId: number): number {
8
+ if (itemId < CRAFTED_ITEM_ID_FLOOR) return UNCRAFTED_STAT_COUNT
9
+ const recipe = getRecipe(itemId)
10
+ return recipe ? recipe.statSlots.length : UNCRAFTED_STAT_COUNT
11
+ }
12
+
13
+ export function statsSumSq(packedStats: bigint, nStats: number): number {
14
+ let sum = 0
15
+ for (let i = 0; i < nStats; i++) {
16
+ const s = decodeStat(packedStats, i)
17
+ sum += s * s
18
+ }
19
+ return sum
20
+ }
21
+
22
+ export function qualityFactor(packedStats: bigint, nStats: number, d1: number): number {
23
+ if (nStats <= 0 || d1 <= 0) throw new Error('influence: invalid quality divisor')
24
+ return statsSumSq(packedStats, nStats) / (nStats * d1)
25
+ }
@@ -0,0 +1,154 @@
1
+ import {getItem} from '../data/catalog'
2
+ import {categoryIndex} from './categories'
3
+ import {
4
+ INFLUENCE_ATOMIC_PER_POINT,
5
+ INFLUENCE_MASS_KG_MAX,
6
+ INFLUENCE_NEED_FP_MAX,
7
+ INFLUENCE_WEIGHT_FP_MAX,
8
+ MASS_KG_PER_TONNE,
9
+ NEED_FP_SCALE,
10
+ WEIGHT_FP_SCALE,
11
+ W_EFF_ATOMIC_PER_KG,
12
+ } from './constants'
13
+ import {findDecomp} from './decomp'
14
+ import type {DemandView} from './demand'
15
+ import {needForCategory} from './demand'
16
+ import {getStatCount, statsSumSq} from './quality'
17
+
18
+ const UINT64_MAX = (1n << 64n) - 1n
19
+
20
+ export interface InfluencePricing {
21
+ d1: number
22
+ weight(category: number, tier: number): bigint
23
+ }
24
+
25
+ export interface ValuedItem {
26
+ itemId: number
27
+ quantity: number
28
+ stats: bigint
29
+ }
30
+
31
+ export function weightedQualityAtomic(
32
+ weightedKgFp: bigint,
33
+ sumSq: number,
34
+ nStats: number,
35
+ d1: number,
36
+ needFp: bigint
37
+ ): bigint {
38
+ if (nStats <= 0 || d1 <= 0) throw new Error('influence: invalid quality divisor')
39
+ const num = weightedKgFp * BigInt(sumSq) * needFp
40
+ const den =
41
+ (BigInt(MASS_KG_PER_TONNE) *
42
+ BigInt(WEIGHT_FP_SCALE) *
43
+ BigInt(nStats) *
44
+ BigInt(d1) *
45
+ BigInt(NEED_FP_SCALE)) /
46
+ BigInt(INFLUENCE_ATOMIC_PER_POINT)
47
+ return num / den
48
+ }
49
+
50
+ export function resourceValueAtomic(
51
+ massKg: bigint,
52
+ weightFp: bigint,
53
+ sumSq: number,
54
+ nStats: number,
55
+ d1: number,
56
+ needFp: bigint
57
+ ): bigint {
58
+ if (massKg > INFLUENCE_MASS_KG_MAX) throw new Error('influence: mass out of range')
59
+ if (weightFp > INFLUENCE_WEIGHT_FP_MAX) throw new Error('influence: weight out of range')
60
+ if (needFp > INFLUENCE_NEED_FP_MAX) throw new Error('influence: need out of range')
61
+ return weightedQualityAtomic(massKg * weightFp, sumSq, nStats, d1, needFp)
62
+ }
63
+
64
+ export function componentBaseAtomic(
65
+ weightedRawKgFp: bigint,
66
+ sumSq: number,
67
+ nStats: number,
68
+ d1: number
69
+ ): bigint {
70
+ return weightedQualityAtomic(weightedRawKgFp, sumSq, nStats, d1, BigInt(NEED_FP_SCALE))
71
+ }
72
+
73
+ export function componentEffortAtomic(processedKgPerUnit: number, quantity: number): bigint {
74
+ const processed = BigInt(processedKgPerUnit) * BigInt(quantity)
75
+ if (processed > INFLUENCE_MASS_KG_MAX) {
76
+ throw new Error('influence: processed mass out of range')
77
+ }
78
+ return processed * W_EFF_ATOMIC_PER_KG
79
+ }
80
+
81
+ export function valueCargoItem(
82
+ item: ValuedItem,
83
+ demand: DemandView,
84
+ pricing: InfluencePricing
85
+ ): bigint {
86
+ const def = getItem(item.itemId)
87
+ if (def.type !== 'resource' && def.type !== 'component') {
88
+ throw new Error('item class cannot be valued')
89
+ }
90
+
91
+ const nStats = getStatCount(item.itemId)
92
+ const sumSq = statsSumSq(item.stats, nStats)
93
+
94
+ if (def.type === 'resource') {
95
+ if (def.category === undefined) throw new Error('resource has no category')
96
+ const category = categoryIndex(def.category)
97
+ const weightFp = pricing.weight(category, def.tier)
98
+ const needFp = needForCategory(demand, category)
99
+ const perUnit = resourceValueAtomic(
100
+ BigInt(def.mass),
101
+ weightFp,
102
+ sumSq,
103
+ nStats,
104
+ pricing.d1,
105
+ needFp
106
+ )
107
+ const total = perUnit * BigInt(item.quantity)
108
+ if (total > UINT64_MAX) throw new Error('influence: resource value overflow')
109
+ return total
110
+ }
111
+
112
+ const entry = findDecomp(item.itemId)
113
+ if (!entry) throw new Error('item has no decomposition')
114
+
115
+ let weighted = 0n
116
+ for (const bucket of entry.buckets) {
117
+ weighted += BigInt(bucket.rawKg) * pricing.weight(bucket.category, bucket.tier)
118
+ }
119
+
120
+ const perUnit = componentBaseAtomic(weighted, sumSq, nStats, pricing.d1)
121
+ const total = perUnit * BigInt(item.quantity)
122
+ if (total > UINT64_MAX) throw new Error('influence: component value overflow')
123
+
124
+ const withEffort = total + componentEffortAtomic(entry.processedKg, item.quantity)
125
+ if (withEffort > UINT64_MAX) throw new Error('influence: component value overflow')
126
+ return withEffort
127
+ }
128
+
129
+ export function valueContribution(
130
+ bundle: ValuedItem[],
131
+ demand: DemandView,
132
+ pricing: InfluencePricing
133
+ ): bigint {
134
+ let total = 0n
135
+ for (const item of bundle) total += valueCargoItem(item, demand, pricing)
136
+ if (total > UINT64_MAX) throw new Error('influence: contribution value overflow')
137
+ return total
138
+ }
139
+
140
+ export function pricingFromWeights(
141
+ d1: number,
142
+ weights: Iterable<{category: number; tier: number; weightFp: bigint | number | string}>
143
+ ): InfluencePricing {
144
+ const table = new Map<number, bigint>()
145
+ for (const row of weights) {
146
+ table.set(row.category * 16 + row.tier, BigInt(row.weightFp.toString()))
147
+ }
148
+ return {
149
+ d1,
150
+ weight(category: number, tier: number): bigint {
151
+ return table.get(category * 16 + tier) ?? 0n
152
+ },
153
+ }
154
+ }
@@ -315,6 +315,30 @@ export class ActionsManager extends BaseManager {
315
315
  })
316
316
  }
317
317
 
318
+ depotstore(
319
+ shipId: UInt64Type,
320
+ depotId: UInt64Type,
321
+ items: ServerContract.ActionParams.Type.cargo_item[]
322
+ ): Action {
323
+ return this.server.action('depotstore', {
324
+ ship_id: UInt64.from(shipId),
325
+ depot_id: UInt64.from(depotId),
326
+ items,
327
+ })
328
+ }
329
+
330
+ depottake(
331
+ shipId: UInt64Type,
332
+ depotId: UInt64Type,
333
+ items: ServerContract.ActionParams.Type.cargo_item[]
334
+ ): Action {
335
+ return this.server.action('depottake', {
336
+ ship_id: UInt64.from(shipId),
337
+ depot_id: UInt64.from(depotId),
338
+ items,
339
+ })
340
+ }
341
+
318
342
  shuttle(
319
343
  carrierId: UInt64Type,
320
344
  fromId: UInt64Type,
@@ -511,6 +535,35 @@ export class ActionsManager extends BaseManager {
511
535
  return this.server.action('canceljob', params)
512
536
  }
513
537
 
538
+ upgradejob(
539
+ targetId: UInt64Type,
540
+ dockId: UInt64Type,
541
+ targetItemId: UInt16Type,
542
+ inputs: ServerContract.ActionParams.Type.cargo_item[]
543
+ ): Action {
544
+ const params: ServerContract.ActionParams.upgradejob = {
545
+ target_id: UInt64.from(targetId),
546
+ dock_id: UInt64.from(dockId),
547
+ target_item_id: UInt16.from(targetItemId),
548
+ inputs,
549
+ }
550
+ return this.server.action('upgradejob', params)
551
+ }
552
+
553
+ claimupgrade(jobId: UInt64Type): Action {
554
+ const params: ServerContract.ActionParams.claimupgrade = {
555
+ job_id: UInt64.from(jobId),
556
+ }
557
+ return this.server.action('claimupgrade', params)
558
+ }
559
+
560
+ cancelupgrade(jobId: UInt64Type): Action {
561
+ const params: ServerContract.ActionParams.cancelupgrade = {
562
+ job_id: UInt64.from(jobId),
563
+ }
564
+ return this.server.action('cancelupgrade', params)
565
+ }
566
+
514
567
  setsocket(workshopId: UInt64Type, slot: UInt8Type, open: boolean): Action {
515
568
  const params: ServerContract.ActionParams.setsocket = {
516
569
  workshop_id: UInt64.from(workshopId),
@@ -599,6 +652,12 @@ export class ActionsManager extends BaseManager {
599
652
  })
600
653
  }
601
654
 
655
+ abandonplot(plotId: UInt64Type): Action {
656
+ return this.server.action('abandonplot', {
657
+ plot_id: UInt64.from(plotId),
658
+ })
659
+ }
660
+
602
661
  addmodule(
603
662
  entityId: UInt64Type,
604
663
  moduleIndex: number,
@@ -835,4 +894,80 @@ export class ActionsManager extends BaseManager {
835
894
  max_rows: UInt64.from(maxRows),
836
895
  })
837
896
  }
897
+
898
+ found(player: NameType, entityId: UInt64Type, location: CoordinatesType): Action {
899
+ return this.server.action('found', {
900
+ player: Name.from(player),
901
+ entity_id: UInt64.from(entityId),
902
+ x: Int64.from(location.x),
903
+ y: Int64.from(location.y),
904
+ })
905
+ }
906
+
907
+ naturalize(player: NameType, location: CoordinatesType): Action {
908
+ return this.server.action('naturalize', {
909
+ player: Name.from(player),
910
+ x: Int64.from(location.x),
911
+ y: Int64.from(location.y),
912
+ })
913
+ }
914
+
915
+ contribute(
916
+ player: NameType,
917
+ entityId: UInt64Type,
918
+ location: CoordinatesType,
919
+ bundle: ServerContract.ActionParams.Type.cargo_item[]
920
+ ): Action {
921
+ return this.platform.action('contribute', {
922
+ game: this.server.account,
923
+ player: Name.from(player),
924
+ entity_id: UInt64.from(entityId),
925
+ x: Int64.from(location.x),
926
+ y: Int64.from(location.y),
927
+ bundle,
928
+ })
929
+ }
930
+
931
+ // addcontrib asserts the citizenship rows exist, so naturalize must precede contribute.
932
+ firstContribution(
933
+ player: NameType,
934
+ entityId: UInt64Type,
935
+ location: CoordinatesType,
936
+ bundle: ServerContract.ActionParams.Type.cargo_item[]
937
+ ): Action[] {
938
+ return [
939
+ this.naturalize(player, location),
940
+ this.contribute(player, entityId, location, bundle),
941
+ ]
942
+ }
943
+
944
+ mintready(maxMints?: UInt32Type): Action {
945
+ const params: {max_mints?: UInt32} = {}
946
+ if (maxMints !== undefined) {
947
+ params.max_mints = UInt32.from(maxMints)
948
+ }
949
+ return this.server.action('mintready', params)
950
+ }
951
+
952
+ charterready(location: CoordinatesType): Action {
953
+ return this.server.action('charterready', {
954
+ x: Int64.from(location.x),
955
+ y: Int64.from(location.y),
956
+ })
957
+ }
958
+
959
+ votecharter(player: NameType, location: CoordinatesType, nodeId: UInt32Type): Action {
960
+ return this.server.action('votecharter', {
961
+ player: Name.from(player),
962
+ x: Int64.from(location.x),
963
+ y: Int64.from(location.y),
964
+ node_id: UInt32.from(nodeId),
965
+ })
966
+ }
967
+
968
+ voteready(maxBallots: UInt32Type = UInt32.from(0)): Action {
969
+ return this.server.action('voteready', {
970
+ max_ballots: UInt32.from(maxBallots),
971
+ })
972
+ }
838
973
  }
@@ -4,7 +4,7 @@ import type {ServerContract} from '../contracts'
4
4
  import {PlotManager} from './plot'
5
5
  import {getItem} from '../data/catalog'
6
6
  import {calc_build_duration} from '../capabilities/crafting'
7
- import {getLanes, getTasks} from '../scheduling/schedule'
7
+ import {getLanes, getTasks, isIdle} from '../scheduling/schedule'
8
8
  import {HoldKind, TaskType} from '../types'
9
9
  import type {
10
10
  BuildableTarget,
@@ -34,6 +34,16 @@ export class ConstructionManager extends BaseManager {
34
34
  return null
35
35
  }
36
36
 
37
+ canAbandon(
38
+ entity: ServerContract.Types.entity_row,
39
+ cargo: ServerContract.Types.cargo_row[]
40
+ ): boolean {
41
+ if (entity.kind.toString() !== 'plot') return false
42
+ if (!isIdle(entity)) return false
43
+ if (Number(entity.cargomass) !== 0) return false
44
+ return !cargo.some((c) => c.entity_id.equals(entity.id))
45
+ }
46
+
37
47
  eligibleSources(
38
48
  target: BuildableTarget,
39
49
  entities: ServerContract.Types.entity_info[],
@@ -12,6 +12,7 @@ import {ActionsManager} from './actions'
12
12
  import {ClusterManager} from './cluster'
13
13
  import {NftManager} from './nft'
14
14
  import {JobsManager} from './jobs'
15
+ import {InfluenceManager} from './influence'
15
16
  import {SubscriptionsManager} from '../subscriptions/manager'
16
17
 
17
18
  export class GameContext {
@@ -24,6 +25,7 @@ export class GameContext {
24
25
  private _clusters?: ClusterManager
25
26
  private _nft?: NftManager
26
27
  private _jobs?: JobsManager
28
+ private _influence?: InfluenceManager
27
29
  private _subscriptions?: SubscriptionsManager
28
30
  private _subscriptionsUrl?: string
29
31
 
@@ -100,6 +102,13 @@ export class GameContext {
100
102
  return this._jobs
101
103
  }
102
104
 
105
+ get influence(): InfluenceManager {
106
+ if (!this._influence) {
107
+ this._influence = new InfluenceManager(this)
108
+ }
109
+ return this._influence
110
+ }
111
+
103
112
  setSubscriptionsUrl(url: string) {
104
113
  this._subscriptionsUrl = url
105
114
  }
@@ -55,6 +55,17 @@ export class EntitiesManager extends BaseManager {
55
55
  return result as ServerContract.Types.entity_summary[]
56
56
  }
57
57
 
58
+ async getDepot(
59
+ depotId: UInt64Type,
60
+ owner: NameType | ServerContract.Types.player_row
61
+ ): Promise<ServerContract.Types.depot_view> {
62
+ const result = await this.server.readonly('getdepot', {
63
+ depot_id: depotId,
64
+ owner: this.resolveOwner(owner),
65
+ })
66
+ return result as ServerContract.Types.depot_view
67
+ }
68
+
58
69
  private resolveOwner(owner: NameType | ServerContract.Types.player_row): Name {
59
70
  if (typeof owner === 'object' && owner !== null && 'owner' in owner) {
60
71
  return owner.owner