@shipload/sdk 1.0.0-next.66 → 1.0.0-next.68

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,202 @@
1
+ import {UInt8, UInt16, UInt32, UInt64} from '@wharfkit/antelope'
2
+ import {expect, test} from 'bun:test'
3
+ import type {ServerContract} from '../contracts'
4
+ import {encodeStats} from '../derivation/crafting'
5
+ import {ITEM_DEPOT_T1_PACKED, ITEM_LOADER_T1, ITEM_STORAGE_T1} from '../data/item-ids'
6
+ import {JOB_QUEUE_CAP} from '../scheduling/jobs'
7
+ import {HoldKind, TaskType} from '../types'
8
+ import {
9
+ civicShuttleBays,
10
+ civicTransfersAtCap,
11
+ civicTransfersFrom,
12
+ pendingCivicTransfers,
13
+ selectCivicShuttleBay,
14
+ CIVIC_TRANSFER_PER_PLAYER_CAP,
15
+ } from './civic-shuttle'
16
+
17
+ type Lane = ServerContract.Types.lane
18
+ type ModuleEntry = ServerContract.Types.module_entry
19
+ type Task = ServerContract.Types.task
20
+
21
+ const T0 = new Date('2026-01-01T00:00:00.000Z')
22
+ const loaderStats = encodeStats([300, 500])
23
+ const storageStats = encodeStats([300, 300])
24
+
25
+ function makeModuleEntry(itemId: number, stats: bigint): ModuleEntry {
26
+ return {
27
+ type: UInt8.from(0),
28
+ installed: {item_id: UInt16.from(itemId), stats: UInt64.from(stats)},
29
+ } as unknown as ModuleEntry
30
+ }
31
+
32
+ // The live Depot layout: two loader bays then four storage.
33
+ function depotModules(): ModuleEntry[] {
34
+ return [
35
+ makeModuleEntry(ITEM_LOADER_T1, loaderStats),
36
+ makeModuleEntry(ITEM_LOADER_T1, loaderStats),
37
+ makeModuleEntry(ITEM_STORAGE_T1, storageStats),
38
+ makeModuleEntry(ITEM_STORAGE_T1, storageStats),
39
+ makeModuleEntry(ITEM_STORAGE_T1, storageStats),
40
+ makeModuleEntry(ITEM_STORAGE_T1, storageStats),
41
+ ]
42
+ }
43
+
44
+ function shuttleTask(senderId: number, receiverId: number, group?: number): Task {
45
+ return {
46
+ type: UInt8.from(TaskType.SHUTTLE),
47
+ duration: UInt32.from(60),
48
+ cancelable: UInt8.from(2),
49
+ cargo: [],
50
+ couplings: [
51
+ {
52
+ counterpart: {entity_id: UInt64.from(senderId), entity_type: UInt8.from(1)},
53
+ hold: UInt64.from(1),
54
+ kind: UInt8.from(HoldKind.PULL),
55
+ },
56
+ {
57
+ counterpart: {entity_id: UInt64.from(receiverId), entity_type: UInt8.from(1)},
58
+ hold: UInt64.from(1),
59
+ kind: UInt8.from(HoldKind.PUSH),
60
+ },
61
+ ],
62
+ entitygroup: group === undefined ? undefined : UInt64.from(group),
63
+ } as unknown as Task
64
+ }
65
+
66
+ function makeLane(laneKey: number, tasks: Task[]): Lane {
67
+ return {
68
+ lane_key: UInt8.from(laneKey),
69
+ schedule: {started: {toDate: () => T0}, tasks},
70
+ } as unknown as Lane
71
+ }
72
+
73
+ test('civicShuttleBays lists both Depot bays with their queue depth', () => {
74
+ const lanes = [makeLane(1, [shuttleTask(20, 21)])]
75
+ const bays = civicShuttleBays(depotModules(), ITEM_DEPOT_T1_PACKED, lanes, T0)
76
+ expect(bays.map((b) => b.laneKey)).toEqual([1, 2])
77
+ expect(bays.map((b) => b.slotIndex)).toEqual([0, 1])
78
+ expect(bays[0].queued).toBe(1)
79
+ expect(bays[1].queued).toBe(0)
80
+ expect(bays.every((b) => !b.full)).toBe(true)
81
+ expect(bays.every((b) => b.thrust > 0 && b.mass > 0)).toBe(true)
82
+ })
83
+
84
+ test('a bay is full at the job queue cap', () => {
85
+ const filled = Array.from({length: JOB_QUEUE_CAP}, () => shuttleTask(20, 21))
86
+ const bays = civicShuttleBays(depotModules(), ITEM_DEPOT_T1_PACKED, [makeLane(1, filled)], T0)
87
+ expect(bays[0].full).toBe(true)
88
+ expect(bays[1].full).toBe(false)
89
+ })
90
+
91
+ const AFTER = new Date(T0.getTime() + 3_600_000)
92
+
93
+ test('a landed booking leaves the bay free, since booking clears it', () => {
94
+ const filled = Array.from({length: JOB_QUEUE_CAP}, () => shuttleTask(20, 21))
95
+ const lanes = [makeLane(1, filled)]
96
+ const bays = civicShuttleBays(depotModules(), ITEM_DEPOT_T1_PACKED, lanes, AFTER)
97
+ expect(bays[0].queued).toBe(0)
98
+ expect(bays[0].full).toBe(false)
99
+ expect(selectCivicShuttleBay(depotModules(), ITEM_DEPOT_T1_PACKED, lanes, AFTER)?.laneKey).toBe(
100
+ 1
101
+ )
102
+ })
103
+
104
+ test('a bay part-way through its queue counts only what is still unfinished', () => {
105
+ const lanes = [makeLane(1, [shuttleTask(20, 21), shuttleTask(22, 23), shuttleTask(24, 25)])]
106
+ const bays = civicShuttleBays(
107
+ depotModules(),
108
+ ITEM_DEPOT_T1_PACKED,
109
+ lanes,
110
+ new Date(T0.getTime() + 90_000)
111
+ )
112
+ expect(bays[0].queued).toBe(2)
113
+ })
114
+
115
+ test('a landed transfer is listed for settling but is not callable', () => {
116
+ const lanes = [makeLane(1, [shuttleTask(20, 21)])]
117
+ const [transfer] = pendingCivicTransfers({lanes} as never, AFTER)
118
+ expect(transfer.isTail).toBe(true)
119
+ expect(transfer.complete).toBe(true)
120
+ expect(transfer.callable).toBe(false)
121
+ })
122
+
123
+ test('landed transfers do not count against the per-player cap', () => {
124
+ const mine = Array.from({length: CIVIC_TRANSFER_PER_PLAYER_CAP}, (_, i) =>
125
+ shuttleTask(20 + i, 50)
126
+ )
127
+ const senderIds = mine.map((_, i) => 20 + i)
128
+ const at = {lanes: [makeLane(1, mine)]} as never
129
+ expect(civicTransfersAtCap(at, senderIds, T0)).toBe(true)
130
+ expect(civicTransfersAtCap(at, senderIds, AFTER)).toBe(false)
131
+ expect(civicTransfersFrom(at, senderIds, AFTER).length).toBe(CIVIC_TRANSFER_PER_PLAYER_CAP)
132
+ })
133
+
134
+ test('selectCivicShuttleBay takes the first free bay, then the lowest occupied one', () => {
135
+ const modules = depotModules()
136
+ expect(selectCivicShuttleBay(modules, ITEM_DEPOT_T1_PACKED, [], T0)?.laneKey).toBe(1)
137
+
138
+ const firstBusy = [makeLane(1, [shuttleTask(20, 21)])]
139
+ expect(selectCivicShuttleBay(modules, ITEM_DEPOT_T1_PACKED, firstBusy, T0)?.laneKey).toBe(2)
140
+
141
+ const bothBusy = [makeLane(1, [shuttleTask(20, 21)]), makeLane(2, [shuttleTask(22, 23)])]
142
+ expect(selectCivicShuttleBay(modules, ITEM_DEPOT_T1_PACKED, bothBusy, T0)?.laneKey).toBe(1)
143
+ })
144
+
145
+ test('selectCivicShuttleBay returns nothing when the building has no loaders', () => {
146
+ const modules = [makeModuleEntry(ITEM_STORAGE_T1, storageStats)]
147
+ expect(selectCivicShuttleBay(modules, ITEM_DEPOT_T1_PACKED, [], T0)).toBeUndefined()
148
+ })
149
+
150
+ test('pendingCivicTransfers reports the bay, both counterparties, and the tail', () => {
151
+ const lanes = [
152
+ makeLane(1, [shuttleTask(20, 21), shuttleTask(22, 23)]),
153
+ makeLane(2, [shuttleTask(24, 25)]),
154
+ ]
155
+ const transfers = pendingCivicTransfers({lanes} as never, T0)
156
+ expect(transfers.length).toBe(3)
157
+
158
+ const first = transfers.find((t) => String(t.senderId) === '20')!
159
+ expect(first.laneKey).toBe(1)
160
+ expect(String(first.receiverId)).toBe('21')
161
+ expect(first.isTail).toBe(false)
162
+ expect(first.callable).toBe(false)
163
+ expect(first.startsAt).toEqual(T0)
164
+ expect(first.completesAt).toEqual(new Date(T0.getTime() + 60_000))
165
+
166
+ const second = transfers.find((t) => String(t.senderId) === '22')!
167
+ expect(second.isTail).toBe(true)
168
+ expect(second.callable).toBe(true)
169
+
170
+ const onBayTwo = transfers.find((t) => String(t.senderId) === '24')!
171
+ expect(onBayTwo.laneKey).toBe(2)
172
+ expect(onBayTwo.callable).toBe(true)
173
+ })
174
+
175
+ test('a grouped tail transfer is not callable', () => {
176
+ const lanes = [makeLane(1, [shuttleTask(20, 21, 7)])]
177
+ const [transfer] = pendingCivicTransfers({lanes} as never, T0)
178
+ expect(transfer.isTail).toBe(true)
179
+ expect(transfer.callable).toBe(false)
180
+ })
181
+
182
+ test('civicTransfersFrom counts only the ships a player sends from', () => {
183
+ const lanes = [
184
+ makeLane(1, [shuttleTask(20, 21), shuttleTask(30, 31)]),
185
+ makeLane(2, [shuttleTask(22, 23)]),
186
+ ]
187
+ const entity = {lanes} as never
188
+ expect(civicTransfersFrom(entity, [20, 22], T0).length).toBe(2)
189
+ expect(civicTransfersFrom(entity, [30], T0).length).toBe(1)
190
+ expect(civicTransfersFrom(entity, [99], T0).length).toBe(0)
191
+ })
192
+
193
+ test('civicTransfersAtCap trips at the per-player cap', () => {
194
+ const mine = Array.from({length: CIVIC_TRANSFER_PER_PLAYER_CAP}, (_, i) =>
195
+ shuttleTask(20 + i, 50)
196
+ )
197
+ const senderIds = mine.map((_, i) => 20 + i)
198
+ const under = {lanes: [makeLane(1, mine.slice(0, -1))]} as never
199
+ const at = {lanes: [makeLane(1, mine)]} as never
200
+ expect(civicTransfersAtCap(under, senderIds, T0)).toBe(false)
201
+ expect(civicTransfersAtCap(at, senderIds, T0)).toBe(true)
202
+ })
@@ -0,0 +1,142 @@
1
+ import type {UInt64} from '@wharfkit/antelope'
2
+ import type {ServerContract} from '../contracts'
3
+ import {getItem} from '../data/catalog'
4
+ import {JOB_QUEUE_CAP} from '../scheduling/jobs'
5
+ import {laneKeyForModule, resolveLaneLoader} from '../scheduling/lanes'
6
+ import {orderedTasks, type ScheduleData} from '../scheduling/schedule'
7
+ import {HoldKind, TaskType} from '../types'
8
+
9
+ type CargoItem = ServerContract.Types.cargo_item
10
+ type Lane = ServerContract.Types.lane
11
+ type ModuleEntry = ServerContract.Types.module_entry
12
+
13
+ export const CIVIC_TRANSFER_PER_PLAYER_CAP = 4
14
+
15
+ export interface CivicShuttleBay {
16
+ laneKey: number
17
+ slotIndex: number
18
+ thrust: number
19
+ mass: number
20
+ outputPct: number
21
+ queued: number
22
+ full: boolean
23
+ }
24
+
25
+ // a landed booking clears during the next civicshuttle, so only unfinished tasks occupy a bay
26
+ function unfinishedTasks(lane: Lane | undefined, now: Date): number {
27
+ if (!lane) return 0
28
+ const startedMs = lane.schedule.started.toDate().getTime()
29
+ const nowMs = now.getTime()
30
+ let endSec = 0
31
+ let count = 0
32
+ for (const task of lane.schedule.tasks) {
33
+ endSec += task.duration.toNumber()
34
+ if (startedMs + endSec * 1000 > nowMs) count++
35
+ }
36
+ return count
37
+ }
38
+
39
+ export function civicShuttleBays(
40
+ modules: ModuleEntry[],
41
+ entityItemId: number,
42
+ lanes: Lane[],
43
+ now: Date = new Date()
44
+ ): CivicShuttleBay[] {
45
+ const bays: CivicShuttleBay[] = []
46
+ for (let slotIndex = 0; slotIndex < modules.length; slotIndex++) {
47
+ const installed = modules[slotIndex].installed
48
+ if (!installed) continue
49
+ if (getItem(installed.item_id).moduleType !== 'loader') continue
50
+ const laneKey = laneKeyForModule(slotIndex)
51
+ const loader = resolveLaneLoader(modules, entityItemId, laneKey)
52
+ const lane = lanes.find((l) => l.lane_key.toNumber() === laneKey)
53
+ const queued = unfinishedTasks(lane, now)
54
+ bays.push({
55
+ laneKey,
56
+ slotIndex,
57
+ thrust: loader.thrust,
58
+ mass: loader.mass,
59
+ outputPct: loader.outputPct,
60
+ queued,
61
+ full: queued >= JOB_QUEUE_CAP,
62
+ })
63
+ }
64
+ return bays
65
+ }
66
+
67
+ // Mirrors worker_lane_key_or_mobility for loaders: first free bay, else the lowest bay.
68
+ export function selectCivicShuttleBay(
69
+ modules: ModuleEntry[],
70
+ entityItemId: number,
71
+ lanes: Lane[],
72
+ now: Date = new Date()
73
+ ): CivicShuttleBay | undefined {
74
+ const bays = civicShuttleBays(modules, entityItemId, lanes, now)
75
+ return bays.find((bay) => bay.queued === 0) ?? bays[0]
76
+ }
77
+
78
+ export interface PendingCivicTransfer {
79
+ laneKey: number
80
+ taskIndex: number
81
+ senderId: UInt64
82
+ receiverId: UInt64
83
+ cargo: CargoItem[]
84
+ startsAt: Date
85
+ completesAt: Date
86
+ isTail: boolean
87
+ complete: boolean
88
+ callable: boolean
89
+ }
90
+
91
+ export function pendingCivicTransfers(
92
+ entity: ScheduleData,
93
+ now: Date = new Date()
94
+ ): PendingCivicTransfer[] {
95
+ const tailIndex = new Map<number, number>()
96
+ for (const l of entity.lanes ?? []) {
97
+ tailIndex.set(l.lane_key.toNumber(), l.schedule.tasks.length - 1)
98
+ }
99
+ const out: PendingCivicTransfer[] = []
100
+ for (const entry of orderedTasks(entity)) {
101
+ if (entry.task.type.toNumber() !== TaskType.SHUTTLE) continue
102
+ const pull = entry.task.couplings.find((c) => c.kind.toNumber() === HoldKind.PULL)
103
+ const push = entry.task.couplings.find((c) => c.kind.toNumber() === HoldKind.PUSH)
104
+ if (!pull || !push) continue
105
+ const isTail = tailIndex.get(entry.laneKey) === entry.taskIndex
106
+ const complete = entry.completesAt.getTime() <= now.getTime()
107
+ out.push({
108
+ laneKey: entry.laneKey,
109
+ taskIndex: entry.taskIndex,
110
+ senderId: pull.counterpart.entity_id,
111
+ receiverId: push.counterpart.entity_id,
112
+ cargo: entry.task.cargo,
113
+ startsAt: entry.startsAt,
114
+ completesAt: entry.completesAt,
115
+ isTail,
116
+ complete,
117
+ callable: isTail && !complete && !entry.task.entitygroup,
118
+ })
119
+ }
120
+ return out
121
+ }
122
+
123
+ // Mirrors civic_transfers_for: the sender side of every shuttle the building holds, by owner.
124
+ export function civicTransfersFrom(
125
+ entity: ScheduleData,
126
+ senderIds: Array<UInt64 | string | number>,
127
+ now: Date = new Date()
128
+ ): PendingCivicTransfer[] {
129
+ const owned = new Set(senderIds.map((id) => String(id)))
130
+ return pendingCivicTransfers(entity, now).filter((t) => owned.has(String(t.senderId)))
131
+ }
132
+
133
+ export function civicTransfersAtCap(
134
+ entity: ScheduleData,
135
+ senderIds: Array<UInt64 | string | number>,
136
+ now: Date = new Date()
137
+ ): boolean {
138
+ return (
139
+ civicTransfersFrom(entity, senderIds, now).filter((t) => !t.complete).length >=
140
+ CIVIC_TRANSFER_PER_PLAYER_CAP
141
+ )
142
+ }
@@ -54,6 +54,8 @@ const NODE_DOCK_TUNEUPS = [30100301, 30100302, 30100303, 30100304]
54
54
  const NODE_DEPOT = 40100001
55
55
  const NODE_DEPOT_BAYS = [40100101, 40100102, 40100103, 40100104]
56
56
  const NODE_DEPOT_LOADERS = [40100401, 40100402, 40100403, 40100404]
57
+ const DEPOT_LOADER_SLOTS = [0, 1]
58
+ const DEPOT_BAY_SLOTS = [2, 3, 4, 5]
57
59
 
58
60
  function node(nodeId: number): CharterNode {
59
61
  const found = charterNode(nodeId)
@@ -168,14 +170,16 @@ describe('the fourteen nodes reproduce the chain-fitted buildings', () => {
168
170
  expect(raised).toBe(400)
169
171
  })
170
172
 
171
- test('the Depot node fits a baseline loader and leaves the bays empty', () => {
173
+ test('the Depot node fits two baseline loaders and leaves the bays empty', () => {
172
174
  const standing = complete(complete({}, NODE_WORKSHOP), NODE_DEPOT)
173
175
  const depot = {modules: standing[CIVIC_DEPOT]!}
174
- expect(stats(depot, 0)).toEqual({
175
- itemId: ITEM_LOADER_T1,
176
- stats: [CHARTER_BASELINE_STAT, CHARTER_BASELINE_STAT],
177
- })
178
- for (let slot = 1; slot <= 4; slot++) expect(depot.modules[slot].installed).toBeUndefined()
176
+ for (const slot of DEPOT_LOADER_SLOTS) {
177
+ expect(stats(depot, slot)).toEqual({
178
+ itemId: ITEM_LOADER_T1,
179
+ stats: [CHARTER_BASELINE_STAT, CHARTER_BASELINE_STAT],
180
+ })
181
+ }
182
+ for (const slot of DEPOT_BAY_SLOTS) expect(depot.modules[slot].installed).toBeUndefined()
179
183
  })
180
184
 
181
185
  test('the four bays fill the storage slots in order at the baseline stat', () => {
@@ -183,8 +187,9 @@ describe('the fourteen nodes reproduce the chain-fitted buildings', () => {
183
187
  for (let i = 0; i < NODE_DEPOT_BAYS.length; i++) {
184
188
  standing = complete(standing, NODE_DEPOT_BAYS[i])
185
189
  const depot = {modules: standing[CIVIC_DEPOT]!}
186
- for (let slot = 1; slot <= 4; slot++) {
187
- if (slot <= i + 1) {
190
+ for (let bay = 0; bay < DEPOT_BAY_SLOTS.length; bay++) {
191
+ const slot = DEPOT_BAY_SLOTS[bay]
192
+ if (bay <= i) {
188
193
  expect(stats(depot, slot)).toEqual({
189
194
  itemId: ITEM_STORAGE_T1,
190
195
  stats: [
@@ -201,14 +206,16 @@ describe('the fourteen nodes reproduce the chain-fitted buildings', () => {
201
206
  }
202
207
  })
203
208
 
204
- test('the four loader rungs step the slot-0 loader by one rung each, reaching 400', () => {
209
+ test('the four loader rungs step both loaders by one rung each, reaching 400', () => {
205
210
  let standing = complete(complete({}, NODE_WORKSHOP), NODE_DEPOT)
206
211
  for (let i = 0; i < NODE_DEPOT_LOADERS.length; i++) {
207
212
  standing = complete(standing, NODE_DEPOT_LOADERS[i])
208
213
  const depot = {modules: standing[CIVIC_DEPOT]!}
209
214
  const stat = CHARTER_BASELINE_STAT + CHARTER_RUNG_STEP * (i + 1)
210
- expect(stats(depot, 0)).toEqual({itemId: ITEM_LOADER_T1, stats: [stat, stat]})
211
- for (let slot = 1; slot <= 4; slot++)
215
+ for (const slot of DEPOT_LOADER_SLOTS) {
216
+ expect(stats(depot, slot)).toEqual({itemId: ITEM_LOADER_T1, stats: [stat, stat]})
217
+ }
218
+ for (const slot of DEPOT_BAY_SLOTS)
212
219
  expect(depot.modules[slot].installed).toBeUndefined()
213
220
  }
214
221
  })
@@ -223,8 +230,10 @@ describe('grant mechanics', () => {
223
230
  stat: CIVIC_STAT_TRANSFER_SPEED,
224
231
  value: 50,
225
232
  })
226
- const withBay = fitCivicModules(raised, 1)
227
- expect(stats({modules: withBay}, 1).stats).toEqual([0, 0, 0, 0])
233
+ const withSecondLoader = fitCivicModules(raised, 1)
234
+ expect(stats({modules: withSecondLoader}, 1).stats).toEqual([50, 50])
235
+ const withBay = fitCivicModules(withSecondLoader, 1)
236
+ expect(stats({modules: withBay}, 2).stats).toEqual([0, 0, 0, 0])
228
237
  const withSecondBay = fitCivicModules(
229
238
  raiseCivicStat(withBay, {
230
239
  kind: CIVIC_GRANT_RUNG,
@@ -234,7 +243,7 @@ describe('grant mechanics', () => {
234
243
  }),
235
244
  1
236
245
  )
237
- expect(stats({modules: withSecondBay}, 2).stats).toEqual([7, 7, 7, 7])
246
+ expect(stats({modules: withSecondBay}, 3).stats).toEqual([7, 7, 7, 7])
238
247
  })
239
248
 
240
249
  test('a rung caps at the civic speed cap', () => {
@@ -43,6 +43,16 @@ export {
43
43
  type CivicLoaderStats,
44
44
  type DepotTransferParams,
45
45
  } from './duration'
46
+ export {
47
+ civicShuttleBays,
48
+ civicTransfersAtCap,
49
+ civicTransfersFrom,
50
+ pendingCivicTransfers,
51
+ selectCivicShuttleBay,
52
+ CIVIC_TRANSFER_PER_PLAYER_CAP,
53
+ type CivicShuttleBay,
54
+ type PendingCivicTransfer,
55
+ } from './civic-shuttle'
46
56
  export {citizenryName, citizenryPatternCount} from './citizenry'
47
57
  export {
48
58
  charterBuildingEntity,
@@ -366,6 +366,28 @@ export class ActionsManager extends BaseManager {
366
366
  })
367
367
  }
368
368
 
369
+ civicshuttle(
370
+ buildingId: UInt64Type,
371
+ fromId: UInt64Type,
372
+ toId: UInt64Type,
373
+ items: ServerContract.ActionParams.Type.cargo_item[]
374
+ ): Action {
375
+ return this.server.action('civicshuttle', {
376
+ building: UInt64.from(buildingId),
377
+ from_id: UInt64.from(fromId),
378
+ to_id: UInt64.from(toId),
379
+ items,
380
+ })
381
+ }
382
+
383
+ cancelcivic(buildingId: UInt64Type, laneKey: UInt8Type, fromId: UInt64Type): Action {
384
+ return this.server.action('cancelcivic', {
385
+ building: UInt64.from(buildingId),
386
+ lane_key: UInt8.from(laneKey),
387
+ from_id: UInt64.from(fromId),
388
+ })
389
+ }
390
+
369
391
  launch(
370
392
  launcherId: UInt64Type,
371
393
  catcherId: UInt64Type,
@@ -982,6 +1004,18 @@ export class ActionsManager extends BaseManager {
982
1004
  })
983
1005
  }
984
1006
 
1007
+ setdeadline(deadline: UInt32Type): Action {
1008
+ return this.server.action('setdeadline', {
1009
+ deadline: UInt32.from(deadline),
1010
+ })
1011
+ }
1012
+
1013
+ closeepoch(epoch: UInt64Type): Action {
1014
+ return this.server.action('closeepoch', {
1015
+ epoch: UInt64.from(epoch),
1016
+ })
1017
+ }
1018
+
985
1019
  cleanrsvp(epoch: UInt64Type, maxRows: UInt64Type): Action {
986
1020
  return this.server.action('cleanrsvp', {
987
1021
  epoch: UInt64.from(epoch),
@@ -1,8 +1,17 @@
1
- import {UInt64, type UInt64Type} from '@wharfkit/antelope'
1
+ import {Checksum256, UInt64, type UInt64Type} from '@wharfkit/antelope'
2
2
  import {BaseManager} from './base'
3
3
  import {type EpochInfo, getCurrentEpoch, getEpochInfo} from '../scheduling/epoch'
4
4
  import type {ServerContract} from '../contracts'
5
5
 
6
+ export const DEFAULT_EPOCH_DEADLINE = 10800
7
+
8
+ const EMPTY_SEED = Checksum256.from('0'.repeat(64))
9
+
10
+ export interface EpochQuorumState {
11
+ threshold: number
12
+ finalized: boolean
13
+ }
14
+
6
15
  export class EpochsManager extends BaseManager {
7
16
  async getCurrentHeight(): Promise<UInt64> {
8
17
  const game = await this.getGame()
@@ -56,6 +65,12 @@ export class EpochsManager extends BaseManager {
56
65
  return this.server.table('epoch').get(target)
57
66
  }
58
67
 
68
+ async getEpochState(epoch: UInt64Type): Promise<EpochQuorumState> {
69
+ const row = await this.getEpochRow(epoch)
70
+ if (!row) return {threshold: 0, finalized: false}
71
+ return {threshold: Number(row.threshold), finalized: !row.seed.equals(EMPTY_SEED)}
72
+ }
73
+
59
74
  async getActiveEpochInfo(): Promise<ServerContract.Types.epoch_row | undefined> {
60
75
  const rows = await this.server.table('epoch').all()
61
76
  if (rows.length === 0) {
@@ -73,6 +88,22 @@ export class EpochsManager extends BaseManager {
73
88
  return cfg ? Number(cfg.threshold) : 0
74
89
  }
75
90
 
91
+ async getDeadline(): Promise<number> {
92
+ const cfg = await this.server.table('oraclecfg').get()
93
+ return cfg ? Number(cfg.deadline) : DEFAULT_EPOCH_DEADLINE
94
+ }
95
+
96
+ async getCloseDueAt(epoch: UInt64Type): Promise<Date> {
97
+ const info = await this.getByHeight(epoch)
98
+ const deadline = await this.getDeadline()
99
+ return new Date(info.start.getTime() + deadline * 1000)
100
+ }
101
+
102
+ async getSecondsUntilClose(epoch: UInt64Type): Promise<number> {
103
+ const dueAt = await this.getCloseDueAt(epoch)
104
+ return Math.ceil((dueAt.getTime() - Date.now()) / 1000)
105
+ }
106
+
76
107
  async getCommitsFor(epoch: UInt64Type): Promise<ServerContract.Types.commit_row[]> {
77
108
  const target = UInt64.from(epoch)
78
109
  const rows = await this.server.table('commit').all()
@@ -10,6 +10,7 @@ export interface EpochInfo {
10
10
  export function getCurrentEpoch(game: PlatformContract.Types.game_row): UInt64 {
11
11
  const current = new Date().getTime()
12
12
  const difference = (current - game.config.start.toMilliseconds()) / 1000
13
+ if (difference < 0) return UInt64.from(0)
13
14
  const epoch = Math.floor(difference / Number(game.config.epochtime)) + 1
14
15
  return UInt64.from(epoch)
15
16
  }
@@ -161,7 +161,12 @@ export function isLocationBuildable(
161
161
  gameSeed: Checksum256Type,
162
162
  coordinates: CoordinatesType
163
163
  ): boolean {
164
- return getLocationType(gameSeed, coordinates) === LocationType.PLANET
164
+ const type = getLocationType(gameSeed, coordinates)
165
+ return (
166
+ type === LocationType.ASTEROID ||
167
+ type === LocationType.NEBULA ||
168
+ type === LocationType.ICE_FIELD
169
+ )
165
170
  }
166
171
 
167
172
  export function deriveLocation(