@shipload/sdk 1.0.0-next.61 → 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.
- package/lib/shipload.d.ts +1243 -59
- package/lib/shipload.js +3911 -1261
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +3812 -1265
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +391 -13
- package/lib/testing.js +1554 -114
- package/lib/testing.js.map +1 -1
- package/lib/testing.m.js +1555 -115
- package/lib/testing.m.js.map +1 -1
- package/package.json +2 -2
- package/src/contracts/fund.ts +201 -0
- package/src/contracts/platform.ts +73 -1
- package/src/contracts/server.ts +946 -26
- package/src/contracts.ts +1 -0
- package/src/data/charters.json +198 -0
- package/src/data/entities.json +36 -0
- package/src/data/item-ids.ts +2 -0
- package/src/data/items.json +12 -0
- package/src/data/kind-registry.json +20 -3
- package/src/data/kind-registry.ts +5 -0
- package/src/data/metadata.ts +14 -0
- package/src/data/recipes.json +0 -48
- package/src/derivation/capabilities.ts +14 -0
- package/src/derivation/recipe-usage.test.ts +0 -2
- package/src/format.ts +9 -0
- package/src/index-module.ts +34 -3
- package/src/influence/categories.ts +32 -0
- package/src/influence/charters.test.ts +126 -0
- package/src/influence/charters.ts +120 -0
- package/src/influence/citizenry.ts +31 -0
- package/src/influence/constants.ts +63 -0
- package/src/influence/decay.ts +20 -0
- package/src/influence/decomp.ts +95 -0
- package/src/influence/demand.ts +106 -0
- package/src/influence/duration.ts +36 -0
- package/src/influence/index.ts +63 -0
- package/src/influence/influence.test.ts +291 -0
- package/src/influence/quality.ts +25 -0
- package/src/influence/valuation.ts +154 -0
- package/src/managers/actions.ts +135 -0
- package/src/managers/construction.ts +11 -1
- package/src/managers/context.ts +18 -0
- package/src/managers/entities.ts +11 -0
- package/src/managers/index.ts +13 -0
- package/src/managers/influence.test.ts +58 -0
- package/src/managers/influence.ts +347 -0
- package/src/managers/jobs.test.ts +62 -0
- package/src/managers/jobs.ts +52 -0
- package/src/nft/description.ts +7 -0
- package/src/scheduling/jobs.test.ts +32 -0
- package/src/scheduling/jobs.ts +37 -0
- package/src/scheduling/task-cargo.ts +1 -0
- package/src/shipload.ts +10 -0
- package/src/types.ts +23 -4
package/src/managers/entities.ts
CHANGED
|
@@ -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
|
package/src/managers/index.ts
CHANGED
|
@@ -12,6 +12,19 @@ export {ClusterManager, computeFreeCells} from './cluster'
|
|
|
12
12
|
export type {GridCell, ClusterCell, Cluster} from './cluster'
|
|
13
13
|
export {NftManager} from './nft'
|
|
14
14
|
export type {NftConfigForItem} from './nft'
|
|
15
|
+
export {JobsManager} from './jobs'
|
|
16
|
+
export {InfluenceManager} from './influence'
|
|
17
|
+
export type {
|
|
18
|
+
CharterProgress,
|
|
19
|
+
ContributePreview,
|
|
20
|
+
ContributePreviewRow,
|
|
21
|
+
FoundedWorld,
|
|
22
|
+
InfluenceStanding,
|
|
23
|
+
PendingBallot,
|
|
24
|
+
VoteCast,
|
|
25
|
+
VoteOption,
|
|
26
|
+
VoteStandings,
|
|
27
|
+
} from './influence'
|
|
15
28
|
export {PlotManager} from './plot'
|
|
16
29
|
export type {PlotProgress, PlotProgressInputRow} from './plot'
|
|
17
30
|
export {ConstructionManager} from './construction'
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {describe, expect, it} from 'bun:test'
|
|
2
|
+
import {Name, UInt64} from '@wharfkit/antelope'
|
|
3
|
+
import {InfluenceManager} from './influence'
|
|
4
|
+
import {coordsToLocationId} from '../types'
|
|
5
|
+
|
|
6
|
+
const LOCATION = {x: 12, y: 34}
|
|
7
|
+
|
|
8
|
+
function managerWith(rowsByTable: Record<string, unknown[]>) {
|
|
9
|
+
const calls: {table: string; scope: unknown}[] = []
|
|
10
|
+
const table = (name: string, scope: unknown) => {
|
|
11
|
+
calls.push({table: name, scope})
|
|
12
|
+
return {all: async () => rowsByTable[name] ?? []}
|
|
13
|
+
}
|
|
14
|
+
const ctx = {server: {table}} as never
|
|
15
|
+
return {manager: new InfluenceManager(ctx), calls}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('InfluenceManager scoped table reads', () => {
|
|
19
|
+
it('getVoteCasts scopes infvote by location and maps fields', async () => {
|
|
20
|
+
const {manager, calls} = managerWith({
|
|
21
|
+
infvote: [{account: Name.from('eggmaple.gm'), epoch: 5, node_id: 2, weight: '100'}],
|
|
22
|
+
})
|
|
23
|
+
const rows = await manager.getVoteCasts(LOCATION)
|
|
24
|
+
expect(calls).toHaveLength(1)
|
|
25
|
+
expect(calls[0].table).toBe('infvote')
|
|
26
|
+
expect(UInt64.from(calls[0].scope as never).equals(coordsToLocationId(LOCATION))).toBe(true)
|
|
27
|
+
expect(rows).toEqual([
|
|
28
|
+
{account: Name.from('eggmaple.gm'), epoch: 5, nodeId: 2, weight: 100n},
|
|
29
|
+
])
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('getVoteTallies scopes inftally by location and maps fields', async () => {
|
|
33
|
+
const {manager, calls} = managerWith({
|
|
34
|
+
inftally: [{node_id: 3, total: '250'}],
|
|
35
|
+
})
|
|
36
|
+
const rows = await manager.getVoteTallies(LOCATION)
|
|
37
|
+
expect(calls[0].table).toBe('inftally')
|
|
38
|
+
expect(UInt64.from(calls[0].scope as never).equals(coordsToLocationId(LOCATION))).toBe(true)
|
|
39
|
+
expect(rows).toEqual([{nodeId: 3, total: 250n}])
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('getBuiltCharters scopes charters by location and maps fields', async () => {
|
|
43
|
+
const {manager, calls} = managerWith({
|
|
44
|
+
charters: [{node_id: 1, entity_id: '9001'}],
|
|
45
|
+
})
|
|
46
|
+
const rows = await manager.getBuiltCharters(LOCATION)
|
|
47
|
+
expect(calls[0].table).toBe('charters')
|
|
48
|
+
expect(UInt64.from(calls[0].scope as never).equals(coordsToLocationId(LOCATION))).toBe(true)
|
|
49
|
+
expect(rows).toEqual([{nodeId: 1, entityId: 9001n}])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('returns an empty array when the scope has no rows', async () => {
|
|
53
|
+
const {manager} = managerWith({})
|
|
54
|
+
expect(await manager.getVoteCasts(LOCATION)).toEqual([])
|
|
55
|
+
expect(await manager.getVoteTallies(LOCATION)).toEqual([])
|
|
56
|
+
expect(await manager.getBuiltCharters(LOCATION)).toEqual([])
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import {Int64, Name, type NameType, UInt64} from '@wharfkit/antelope'
|
|
2
|
+
import {BaseManager} from './base'
|
|
3
|
+
import type {ServerContract} from '../contracts'
|
|
4
|
+
import {coordsToLocationId, locationIdToCoords, type CoordinatesType} from '../types'
|
|
5
|
+
import {
|
|
6
|
+
type BuiltCharter,
|
|
7
|
+
citizenryName,
|
|
8
|
+
contributeDuration,
|
|
9
|
+
decayActive,
|
|
10
|
+
deriveDemand,
|
|
11
|
+
pricingFromWeights,
|
|
12
|
+
valueCargoItem,
|
|
13
|
+
type DemandTriple,
|
|
14
|
+
type DemandView,
|
|
15
|
+
type ValuedItem,
|
|
16
|
+
} from '../influence'
|
|
17
|
+
import {getItem} from '../data/catalog'
|
|
18
|
+
|
|
19
|
+
export interface InfluenceStanding {
|
|
20
|
+
epoch: number
|
|
21
|
+
playerLifetime: bigint
|
|
22
|
+
playerActive: bigint
|
|
23
|
+
standingLifetime: bigint
|
|
24
|
+
standingActive: bigint
|
|
25
|
+
locationLifetime: bigint
|
|
26
|
+
locationActive: bigint
|
|
27
|
+
watermark: bigint
|
|
28
|
+
mandate: number
|
|
29
|
+
founder: Name
|
|
30
|
+
founded: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CharterProgress {
|
|
34
|
+
locationId: bigint
|
|
35
|
+
lifetime: bigint
|
|
36
|
+
watermark: bigint
|
|
37
|
+
surplus: bigint
|
|
38
|
+
mandate: number
|
|
39
|
+
nextCost: bigint
|
|
40
|
+
prereqsMet: boolean
|
|
41
|
+
buildable: boolean
|
|
42
|
+
epoch: number
|
|
43
|
+
built: {nodeId: number; completedEpoch: number; entityId: bigint}[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ContributePreviewRow {
|
|
47
|
+
itemId: number
|
|
48
|
+
quantity: number
|
|
49
|
+
stats: bigint
|
|
50
|
+
valueAtomic: bigint
|
|
51
|
+
massKg: number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ContributePreview {
|
|
55
|
+
rows: ContributePreviewRow[]
|
|
56
|
+
totalAtomic: bigint
|
|
57
|
+
totalMassKg: number
|
|
58
|
+
durationSeconds: number
|
|
59
|
+
demand: DemandView
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface FoundedWorld {
|
|
63
|
+
locationId: bigint
|
|
64
|
+
x: number
|
|
65
|
+
y: number
|
|
66
|
+
lifetime: bigint
|
|
67
|
+
active: bigint
|
|
68
|
+
watermark: bigint
|
|
69
|
+
chosen: number
|
|
70
|
+
chosenEpoch: number
|
|
71
|
+
mandate: number | undefined
|
|
72
|
+
founder: Name
|
|
73
|
+
founded: number
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface VoteOption {
|
|
77
|
+
nodeId: number
|
|
78
|
+
cost: bigint
|
|
79
|
+
eligible: boolean
|
|
80
|
+
total: bigint | undefined
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface VoteStandings {
|
|
84
|
+
locationId: bigint
|
|
85
|
+
epoch: number
|
|
86
|
+
chosen: number
|
|
87
|
+
chosenEpoch: number
|
|
88
|
+
mandate: number
|
|
89
|
+
ballotEpoch: number | undefined
|
|
90
|
+
talliesSuppressed: boolean
|
|
91
|
+
options: VoteOption[]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface VoteCast {
|
|
95
|
+
account: Name
|
|
96
|
+
epoch: number
|
|
97
|
+
nodeId: number
|
|
98
|
+
weight: bigint
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface PendingBallot {
|
|
102
|
+
locationId: bigint
|
|
103
|
+
x: number
|
|
104
|
+
y: number
|
|
105
|
+
epoch: number
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function big(value: unknown): bigint {
|
|
109
|
+
return BigInt(String(value))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export class InfluenceManager extends BaseManager {
|
|
113
|
+
async getStanding(owner: NameType, location: CoordinatesType): Promise<InfluenceStanding> {
|
|
114
|
+
const result = (await this.server.readonly('getinfluence', {
|
|
115
|
+
owner: Name.from(owner),
|
|
116
|
+
x: Int64.from(location.x),
|
|
117
|
+
y: Int64.from(location.y),
|
|
118
|
+
})) as ServerContract.Types.influence_totals
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
epoch: Number(result.epoch),
|
|
122
|
+
playerLifetime: big(result.player_lifetime),
|
|
123
|
+
playerActive: big(result.player_active),
|
|
124
|
+
standingLifetime: big(result.standing_lifetime),
|
|
125
|
+
standingActive: big(result.standing_active),
|
|
126
|
+
locationLifetime: big(result.location_lifetime),
|
|
127
|
+
locationActive: big(result.location_active),
|
|
128
|
+
watermark: big(result.watermark),
|
|
129
|
+
mandate: Number(result.mandate),
|
|
130
|
+
founder: Name.from(result.founder),
|
|
131
|
+
founded: Number(result.founded),
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async getDemandConfig(): Promise<DemandTriple> {
|
|
136
|
+
const row = (await this.server.table('infdemand').get()) as
|
|
137
|
+
| ServerContract.Types.infdemand_row
|
|
138
|
+
| undefined
|
|
139
|
+
if (!row) throw new Error('influence demand config is unset')
|
|
140
|
+
return {peak: big(row.peak), base: big(row.base), floor: big(row.floor)}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async getQualityDivisor(): Promise<number> {
|
|
144
|
+
const row = (await this.server.table('infquality').get()) as
|
|
145
|
+
| ServerContract.Types.infquality_row
|
|
146
|
+
| undefined
|
|
147
|
+
if (!row) throw new Error('influence quality config is unset')
|
|
148
|
+
return Number(row.d1)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async getWeights(): Promise<ServerContract.Types.infweight_row[]> {
|
|
152
|
+
return (await this.server.table('infweight').all()) as ServerContract.Types.infweight_row[]
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async getDemand(location: CoordinatesType): Promise<DemandView> {
|
|
156
|
+
const game = await this.getGame()
|
|
157
|
+
const state = await this.getState()
|
|
158
|
+
const triple = await this.getDemandConfig()
|
|
159
|
+
return deriveDemand({
|
|
160
|
+
gameSeed: game.config.seed,
|
|
161
|
+
epochSeed: state.epochSeed,
|
|
162
|
+
coordinates: location,
|
|
163
|
+
triple,
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async getCitizenryName(location: CoordinatesType): Promise<string | undefined> {
|
|
168
|
+
const game = await this.getGame()
|
|
169
|
+
return citizenryName(game.config.seed, location)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async previewContribution(
|
|
173
|
+
location: CoordinatesType,
|
|
174
|
+
bundle: ValuedItem[],
|
|
175
|
+
opts: {altitudeZ?: number} = {}
|
|
176
|
+
): Promise<ContributePreview> {
|
|
177
|
+
const [demand, d1, weights] = await Promise.all([
|
|
178
|
+
this.getDemand(location),
|
|
179
|
+
this.getQualityDivisor(),
|
|
180
|
+
this.getWeights(),
|
|
181
|
+
])
|
|
182
|
+
const pricing = pricingFromWeights(
|
|
183
|
+
d1,
|
|
184
|
+
weights.map((w) => ({
|
|
185
|
+
category: Number(w.category),
|
|
186
|
+
tier: Number(w.tier),
|
|
187
|
+
weightFp: big(w.weight_fp),
|
|
188
|
+
}))
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
let totalAtomic = 0n
|
|
192
|
+
let totalMassKg = 0
|
|
193
|
+
const rows = bundle.map((item) => {
|
|
194
|
+
const valueAtomic = valueCargoItem(item, demand, pricing)
|
|
195
|
+
const massKg = getItem(item.itemId).mass * item.quantity
|
|
196
|
+
totalAtomic += valueAtomic
|
|
197
|
+
totalMassKg += massKg
|
|
198
|
+
return {...item, valueAtomic, massKg}
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
rows,
|
|
203
|
+
totalAtomic,
|
|
204
|
+
totalMassKg,
|
|
205
|
+
durationSeconds: contributeDuration(totalMassKg, opts.altitudeZ ?? 0),
|
|
206
|
+
demand,
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async getCharter(location: CoordinatesType): Promise<CharterProgress> {
|
|
211
|
+
const result = (await this.server.readonly('getcharter', {
|
|
212
|
+
x: Int64.from(location.x),
|
|
213
|
+
y: Int64.from(location.y),
|
|
214
|
+
})) as ServerContract.Types.charter_result
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
locationId: big(result.location_id),
|
|
218
|
+
lifetime: big(result.lifetime),
|
|
219
|
+
watermark: big(result.watermark),
|
|
220
|
+
surplus: big(result.surplus),
|
|
221
|
+
mandate: Number(result.mandate),
|
|
222
|
+
nextCost: big(result.next_cost),
|
|
223
|
+
prereqsMet: Boolean(result.prereqs_met),
|
|
224
|
+
buildable: Boolean(result.buildable),
|
|
225
|
+
epoch: Number(result.epoch),
|
|
226
|
+
built: result.built.map((b) => ({
|
|
227
|
+
nodeId: Number(b.node_id),
|
|
228
|
+
completedEpoch: Number(b.completed_epoch),
|
|
229
|
+
entityId: big(b.entity_id),
|
|
230
|
+
})),
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async getPools(): Promise<ServerContract.Types.pool_view[]> {
|
|
235
|
+
const result = (await this.server.readonly('getpools')) as ServerContract.Types.pools_result
|
|
236
|
+
return result.pools
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async getPool(category: number, tier: number): Promise<ServerContract.Types.pool_pair_result> {
|
|
240
|
+
return (await this.server.readonly('getpool', {
|
|
241
|
+
category,
|
|
242
|
+
tier,
|
|
243
|
+
})) as ServerContract.Types.pool_pair_result
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async getMintConfig(): Promise<ServerContract.Types.mintcfg_result> {
|
|
247
|
+
return (await this.server.readonly('getmintcfg')) as ServerContract.Types.mintcfg_result
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async getVotes(location: CoordinatesType): Promise<VoteStandings> {
|
|
251
|
+
const result = (await this.server.readonly('getvotes', {
|
|
252
|
+
x: Int64.from(location.x),
|
|
253
|
+
y: Int64.from(location.y),
|
|
254
|
+
})) as ServerContract.Types.vote_result
|
|
255
|
+
|
|
256
|
+
const epoch = Number(result.epoch)
|
|
257
|
+
const rawBallotEpoch = Number(result.ballot_epoch)
|
|
258
|
+
const ballotEpoch = rawBallotEpoch === 0 ? undefined : rawBallotEpoch
|
|
259
|
+
const talliesSuppressed = ballotEpoch !== undefined && ballotEpoch < epoch
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
locationId: big(result.location_id),
|
|
263
|
+
epoch,
|
|
264
|
+
chosen: Number(result.chosen),
|
|
265
|
+
chosenEpoch: Number(result.chosen_epoch),
|
|
266
|
+
mandate: Number(result.effective),
|
|
267
|
+
ballotEpoch,
|
|
268
|
+
talliesSuppressed,
|
|
269
|
+
options: result.options.map((option) => ({
|
|
270
|
+
nodeId: Number(option.node_id),
|
|
271
|
+
cost: big(option.cost),
|
|
272
|
+
eligible: Boolean(option.eligible),
|
|
273
|
+
total: talliesSuppressed ? undefined : big(option.total),
|
|
274
|
+
})),
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async getVoteCasts(location: CoordinatesType): Promise<VoteCast[]> {
|
|
279
|
+
const rows = (await this.server
|
|
280
|
+
.table('infvote', coordsToLocationId(location))
|
|
281
|
+
.all()) as ServerContract.Types.infvote_row[]
|
|
282
|
+
return rows.map((row) => ({
|
|
283
|
+
account: Name.from(row.account),
|
|
284
|
+
epoch: Number(row.epoch),
|
|
285
|
+
nodeId: Number(row.node_id),
|
|
286
|
+
weight: big(row.weight),
|
|
287
|
+
}))
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async getVoteTallies(location: CoordinatesType): Promise<{nodeId: number; total: bigint}[]> {
|
|
291
|
+
const rows = (await this.server
|
|
292
|
+
.table('inftally', coordsToLocationId(location))
|
|
293
|
+
.all()) as ServerContract.Types.inftally_row[]
|
|
294
|
+
return rows.map((row) => ({nodeId: Number(row.node_id), total: big(row.total)}))
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async getBuiltCharters(location: CoordinatesType): Promise<BuiltCharter[]> {
|
|
298
|
+
const rows = (await this.server
|
|
299
|
+
.table('charters', coordsToLocationId(location))
|
|
300
|
+
.all()) as ServerContract.Types.charters_row[]
|
|
301
|
+
return rows.map((row) => ({nodeId: Number(row.node_id), entityId: big(row.entity_id)}))
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async getBallotQueue(): Promise<PendingBallot[]> {
|
|
305
|
+
const rows = (await this.server
|
|
306
|
+
.table('infballot')
|
|
307
|
+
.all()) as ServerContract.Types.infballot_row[]
|
|
308
|
+
return rows.map((row) => ({
|
|
309
|
+
locationId: big(row.location_id),
|
|
310
|
+
...locationIdToCoords(row.location_id),
|
|
311
|
+
epoch: Number(row.epoch),
|
|
312
|
+
}))
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async getFoundedWorlds(opts: {withMandate?: boolean} = {}): Promise<FoundedWorld[]> {
|
|
316
|
+
const rows = (await this.server.table('infloc').all()) as ServerContract.Types.infloc_row[]
|
|
317
|
+
const epoch = (await this.getState()).epoch
|
|
318
|
+
const worlds = rows.map((row) => ({
|
|
319
|
+
locationId: big(row.location_id),
|
|
320
|
+
...locationIdToCoords(row.location_id),
|
|
321
|
+
lifetime: big(row.lifetime),
|
|
322
|
+
active: decayActive(
|
|
323
|
+
big(row.active),
|
|
324
|
+
Math.max(0, Number(epoch) - Number(row.last_update_epoch))
|
|
325
|
+
),
|
|
326
|
+
watermark: big(row.watermark),
|
|
327
|
+
chosen: Number(row.chosen),
|
|
328
|
+
chosenEpoch: Number(row.chosen_epoch),
|
|
329
|
+
mandate: undefined as number | undefined,
|
|
330
|
+
founder: Name.from(row.founder),
|
|
331
|
+
founded: Number(row.founded),
|
|
332
|
+
}))
|
|
333
|
+
|
|
334
|
+
if (!opts.withMandate) return worlds
|
|
335
|
+
|
|
336
|
+
for (const world of worlds) {
|
|
337
|
+
world.mandate = (await this.getCharter({x: world.x, y: world.y})).mandate
|
|
338
|
+
}
|
|
339
|
+
return worlds
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async isFounded(location: CoordinatesType): Promise<boolean> {
|
|
343
|
+
const id = coordsToLocationId(location)
|
|
344
|
+
const row = await this.server.table('infloc').get(UInt64.from(id))
|
|
345
|
+
return row !== undefined
|
|
346
|
+
}
|
|
347
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {describe, expect, it} from 'bun:test'
|
|
2
|
+
import {Name} from '@wharfkit/antelope'
|
|
3
|
+
import {JobsManager} from './jobs'
|
|
4
|
+
|
|
5
|
+
const OWNER = 'eggmaple.gm'
|
|
6
|
+
const row = (over: Record<string, unknown> = {}) => ({
|
|
7
|
+
id: {toNumber: () => 7},
|
|
8
|
+
workshop: {toNumber: () => 42},
|
|
9
|
+
socket: {toNumber: () => 0},
|
|
10
|
+
owner: Name.from(OWNER),
|
|
11
|
+
ship_id: {toNumber: () => 100},
|
|
12
|
+
coords: {x: {toNumber: () => 12}, y: {toNumber: () => 34}},
|
|
13
|
+
starts_at: {toDate: () => new Date('2026-07-26T10:00:00Z')},
|
|
14
|
+
completes_at: {toDate: () => new Date('2026-07-26T11:00:00Z')},
|
|
15
|
+
recipe_id: {toNumber: () => 10001},
|
|
16
|
+
quantity: {toNumber: () => 5},
|
|
17
|
+
energy_paid: {toNumber: () => 0},
|
|
18
|
+
cargo: [{item: 'in'}, {item: 'out'}],
|
|
19
|
+
...over,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
function managerWith(queryImpl: () => Promise<unknown[]>, allImpl: () => Promise<unknown[]>) {
|
|
23
|
+
const table = () => ({query: () => ({all: queryImpl}), all: allImpl})
|
|
24
|
+
const ctx = {server: {table}} as never
|
|
25
|
+
return new JobsManager(ctx)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('JobsManager.getOwnedJobs', () => {
|
|
29
|
+
it('parses owner rows: output = last cargo, inputs = rest, status derived', async () => {
|
|
30
|
+
const m = managerWith(
|
|
31
|
+
async () => [row()],
|
|
32
|
+
async () => []
|
|
33
|
+
)
|
|
34
|
+
const jobs = await m.getOwnedJobs(OWNER, {now: new Date('2026-07-26T11:30:00Z')})
|
|
35
|
+
expect(jobs).toHaveLength(1)
|
|
36
|
+
expect(jobs[0]).toMatchObject({id: 7, workshop: 42, quantity: 5, status: 'ready'})
|
|
37
|
+
expect(jobs[0].coords).toEqual({x: 12, y: 34})
|
|
38
|
+
expect(jobs[0].output).toEqual({item: 'out'} as never)
|
|
39
|
+
expect(jobs[0].inputs).toEqual([{item: 'in'}] as never)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('drops rows whose owner does not match (positional-index safety re-filter)', async () => {
|
|
43
|
+
const other = row({owner: Name.from('someoneelse.gm')})
|
|
44
|
+
const m = managerWith(
|
|
45
|
+
async () => [row(), other],
|
|
46
|
+
async () => []
|
|
47
|
+
)
|
|
48
|
+
const jobs = await m.getOwnedJobs(OWNER)
|
|
49
|
+
expect(jobs).toHaveLength(1)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('falls back to a full-scan .all() when the positional query throws', async () => {
|
|
53
|
+
const m = managerWith(
|
|
54
|
+
async () => {
|
|
55
|
+
throw new Error('bad index_position')
|
|
56
|
+
},
|
|
57
|
+
async () => [row()]
|
|
58
|
+
)
|
|
59
|
+
const jobs = await m.getOwnedJobs(OWNER)
|
|
60
|
+
expect(jobs).toHaveLength(1)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {Name, type NameType} from '@wharfkit/antelope'
|
|
2
|
+
import {BaseManager} from './base'
|
|
3
|
+
import {jobStatus, splitJobCargo, type OwnedJob} from '../scheduling/jobs'
|
|
4
|
+
import type {ServerContract} from '../contracts'
|
|
5
|
+
|
|
6
|
+
type JobRow = ServerContract.Types.job_row
|
|
7
|
+
|
|
8
|
+
export class JobsManager extends BaseManager {
|
|
9
|
+
async getOwnedJobs(owner: NameType, opts?: {now?: Date}): Promise<OwnedJob[]> {
|
|
10
|
+
const ownerName = Name.from(owner)
|
|
11
|
+
const now = opts?.now ?? new Date()
|
|
12
|
+
|
|
13
|
+
let rows: JobRow[]
|
|
14
|
+
try {
|
|
15
|
+
// index_position 'tertiary' = nodeos slot 3 = owner's secondary index; no ABI metadata for it.
|
|
16
|
+
rows = (await this.server
|
|
17
|
+
.table('jobs')
|
|
18
|
+
.query({
|
|
19
|
+
index_position: 'tertiary',
|
|
20
|
+
key_type: 'i64',
|
|
21
|
+
from: ownerName.value,
|
|
22
|
+
to: ownerName.value,
|
|
23
|
+
})
|
|
24
|
+
.all()) as JobRow[]
|
|
25
|
+
} catch {
|
|
26
|
+
rows = (await this.server.table('jobs').all()) as JobRow[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return rows.filter((r) => ownerName.equals(r.owner)).map((r) => this.parseOwnedJob(r, now))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private parseOwnedJob(r: JobRow, now: Date): OwnedJob {
|
|
33
|
+
const startsAt = r.starts_at.toDate()
|
|
34
|
+
const completesAt = r.completes_at.toDate()
|
|
35
|
+
const {output, inputs} = splitJobCargo(r.cargo)
|
|
36
|
+
return {
|
|
37
|
+
id: r.id.toNumber(),
|
|
38
|
+
workshop: r.workshop.toNumber(),
|
|
39
|
+
socket: r.socket.toNumber(),
|
|
40
|
+
shipId: r.ship_id.toNumber(),
|
|
41
|
+
coords: {x: r.coords.x.toNumber(), y: r.coords.y.toNumber()},
|
|
42
|
+
startsAt,
|
|
43
|
+
completesAt,
|
|
44
|
+
recipeId: r.recipe_id.toNumber(),
|
|
45
|
+
quantity: r.quantity.toNumber(),
|
|
46
|
+
status: jobStatus({startsAt, completesAt}, now),
|
|
47
|
+
output,
|
|
48
|
+
inputs,
|
|
49
|
+
outputStats: output?.stats === undefined ? undefined : BigInt(output.stats.toString()),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/nft/description.ts
CHANGED
|
@@ -85,6 +85,11 @@ export function computeBaseCapacityWorkshop(stats: bigint): number {
|
|
|
85
85
|
return Math.floor(5_000_000 * 6 ** (s / 1998))
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
export function computeBaseCapacityDepot(stats: bigint): number {
|
|
89
|
+
const s = decodeStat(stats, 0) + decodeStat(stats, 2)
|
|
90
|
+
return Math.floor(50_000_000 * 6 ** (s / 1998))
|
|
91
|
+
}
|
|
92
|
+
|
|
88
93
|
const CAPACITY_FN_BY_KIND: Record<string, (stats: bigint) => number> = {
|
|
89
94
|
ship: computeBaseCapacityShip,
|
|
90
95
|
warehouse: computeBaseCapacityWarehouse,
|
|
@@ -95,6 +100,8 @@ const CAPACITY_FN_BY_KIND: Record<string, (stats: bigint) => number> = {
|
|
|
95
100
|
mdriver: computeBaseCapacityContainer,
|
|
96
101
|
mcatcher: computeBaseCapacityContainer,
|
|
97
102
|
container: computeBaseCapacityContainer,
|
|
103
|
+
nexus: computeBaseCapacityContainer,
|
|
104
|
+
depot: computeBaseCapacityDepot,
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
export function computeBaseCapacityForEntity(itemId: number, stats: bigint): number {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {describe, expect, it} from 'bun:test'
|
|
2
|
+
import {jobStatus, splitJobCargo} from './jobs'
|
|
3
|
+
|
|
4
|
+
const at = (s: string) => new Date(s)
|
|
5
|
+
|
|
6
|
+
describe('jobStatus', () => {
|
|
7
|
+
const job = {startsAt: at('2026-07-26T10:00:00Z'), completesAt: at('2026-07-26T11:00:00Z')}
|
|
8
|
+
it('is waiting before startsAt', () => {
|
|
9
|
+
expect(jobStatus(job, at('2026-07-26T09:59:59Z'))).toBe('waiting')
|
|
10
|
+
})
|
|
11
|
+
it('is crafting between startsAt and completesAt', () => {
|
|
12
|
+
expect(jobStatus(job, at('2026-07-26T10:30:00Z'))).toBe('crafting')
|
|
13
|
+
})
|
|
14
|
+
it('is ready at or after completesAt', () => {
|
|
15
|
+
expect(jobStatus(job, at('2026-07-26T11:00:00Z'))).toBe('ready')
|
|
16
|
+
expect(jobStatus(job, at('2026-07-26T12:00:00Z'))).toBe('ready')
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('splitJobCargo', () => {
|
|
21
|
+
it('takes the last element as output, the rest as inputs', () => {
|
|
22
|
+
const cargo = [{n: 'a'}, {n: 'b'}, {n: 'out'}] as unknown as Parameters<
|
|
23
|
+
typeof splitJobCargo
|
|
24
|
+
>[0]
|
|
25
|
+
const {output, inputs} = splitJobCargo(cargo)
|
|
26
|
+
expect(output).toEqual({n: 'out'} as never)
|
|
27
|
+
expect(inputs).toEqual([{n: 'a'}, {n: 'b'}] as never)
|
|
28
|
+
})
|
|
29
|
+
it('returns null output for empty cargo', () => {
|
|
30
|
+
expect(splitJobCargo([])).toEqual({output: null, inputs: []})
|
|
31
|
+
})
|
|
32
|
+
})
|
package/src/scheduling/jobs.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import type {ServerContract} from '../contracts'
|
|
2
|
+
|
|
3
|
+
type CargoItem = ServerContract.Types.cargo_item
|
|
4
|
+
|
|
1
5
|
export interface JobWindow {
|
|
2
6
|
id: number
|
|
3
7
|
socket: number
|
|
@@ -6,6 +10,8 @@ export interface JobWindow {
|
|
|
6
10
|
completesAt: Date
|
|
7
11
|
recipeId: number
|
|
8
12
|
quantity: number
|
|
13
|
+
/** Packed stat roll of the job's output, when the source carried the job's cargo. */
|
|
14
|
+
outputStats?: bigint
|
|
9
15
|
}
|
|
10
16
|
|
|
11
17
|
export interface JobLaneEntry {
|
|
@@ -69,3 +75,34 @@ export function pickFabricator(
|
|
|
69
75
|
}
|
|
70
76
|
return best
|
|
71
77
|
}
|
|
78
|
+
|
|
79
|
+
export type JobStatus = 'waiting' | 'crafting' | 'ready'
|
|
80
|
+
|
|
81
|
+
export function jobStatus(job: {startsAt: Date; completesAt: Date}, now: Date): JobStatus {
|
|
82
|
+
if (now < job.startsAt) return 'waiting'
|
|
83
|
+
if (now < job.completesAt) return 'crafting'
|
|
84
|
+
return 'ready'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Generic in the element so raw chain JSON (readonly actions) splits by the same rule as decoded rows.
|
|
88
|
+
export function splitJobCargo<T>(cargo: readonly T[]): {output: T | null; inputs: T[]} {
|
|
89
|
+
if (cargo.length === 0) return {output: null, inputs: []}
|
|
90
|
+
return {output: cargo[cargo.length - 1], inputs: cargo.slice(0, -1)}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface OwnedJob {
|
|
94
|
+
id: number
|
|
95
|
+
workshop: number
|
|
96
|
+
socket: number
|
|
97
|
+
shipId: number
|
|
98
|
+
coords: {x: number; y: number}
|
|
99
|
+
startsAt: Date
|
|
100
|
+
completesAt: Date
|
|
101
|
+
recipeId: number
|
|
102
|
+
quantity: number
|
|
103
|
+
status: JobStatus
|
|
104
|
+
output: CargoItem | null
|
|
105
|
+
inputs: CargoItem[]
|
|
106
|
+
/** Packed stat roll of `output`, so every job shape answers this the same way. */
|
|
107
|
+
outputStats?: bigint
|
|
108
|
+
}
|
|
@@ -34,6 +34,7 @@ export function taskCargoChanges(task: ServerContract.Types.task): TaskCargoChan
|
|
|
34
34
|
case TaskType.GATHER:
|
|
35
35
|
return task.couplings.length > 0 ? [] : items.map((i) => toChange(i, 'in'))
|
|
36
36
|
case TaskType.UNLOAD:
|
|
37
|
+
case TaskType.CONTRIBUTE:
|
|
37
38
|
return items.map((i) => toChange(i, 'out'))
|
|
38
39
|
case TaskType.CRAFT:
|
|
39
40
|
return [
|