@takaro/gameserver 0.0.0-next.09a7ca1

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 (106) hide show
  1. package/README.md +36 -0
  2. package/dist/TakaroEmitter.d.ts +30 -0
  3. package/dist/TakaroEmitter.d.ts.map +1 -0
  4. package/dist/TakaroEmitter.js +101 -0
  5. package/dist/TakaroEmitter.js.map +1 -0
  6. package/dist/config.d.ts +9 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/config.js +13 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/gameservers/7d2d/apiResponses.d.ts +194 -0
  11. package/dist/gameservers/7d2d/apiResponses.d.ts.map +1 -0
  12. package/dist/gameservers/7d2d/apiResponses.js +2 -0
  13. package/dist/gameservers/7d2d/apiResponses.js.map +1 -0
  14. package/dist/gameservers/7d2d/connectionInfo.d.ts +38 -0
  15. package/dist/gameservers/7d2d/connectionInfo.d.ts.map +1 -0
  16. package/dist/gameservers/7d2d/connectionInfo.js +66 -0
  17. package/dist/gameservers/7d2d/connectionInfo.js.map +1 -0
  18. package/dist/gameservers/7d2d/emitter.d.ts +32 -0
  19. package/dist/gameservers/7d2d/emitter.d.ts.map +1 -0
  20. package/dist/gameservers/7d2d/emitter.js +273 -0
  21. package/dist/gameservers/7d2d/emitter.js.map +1 -0
  22. package/dist/gameservers/7d2d/index.d.ts +34 -0
  23. package/dist/gameservers/7d2d/index.d.ts.map +1 -0
  24. package/dist/gameservers/7d2d/index.js +304 -0
  25. package/dist/gameservers/7d2d/index.js.map +1 -0
  26. package/dist/gameservers/7d2d/itemWorker.d.ts +2 -0
  27. package/dist/gameservers/7d2d/itemWorker.d.ts.map +1 -0
  28. package/dist/gameservers/7d2d/itemWorker.js +36 -0
  29. package/dist/gameservers/7d2d/itemWorker.js.map +1 -0
  30. package/dist/gameservers/7d2d/items-7d2d.json +25051 -0
  31. package/dist/gameservers/7d2d/sdtdAPIClient.d.ts +19 -0
  32. package/dist/gameservers/7d2d/sdtdAPIClient.d.ts.map +1 -0
  33. package/dist/gameservers/7d2d/sdtdAPIClient.js +57 -0
  34. package/dist/gameservers/7d2d/sdtdAPIClient.js.map +1 -0
  35. package/dist/gameservers/generic/connectionInfo.d.ts +16 -0
  36. package/dist/gameservers/generic/connectionInfo.d.ts.map +1 -0
  37. package/dist/gameservers/generic/connectionInfo.js +29 -0
  38. package/dist/gameservers/generic/connectionInfo.js.map +1 -0
  39. package/dist/gameservers/generic/connectorClient.d.ts +7 -0
  40. package/dist/gameservers/generic/connectorClient.d.ts.map +1 -0
  41. package/dist/gameservers/generic/connectorClient.js +60 -0
  42. package/dist/gameservers/generic/connectorClient.js.map +1 -0
  43. package/dist/gameservers/generic/emitter.d.ts +13 -0
  44. package/dist/gameservers/generic/emitter.d.ts.map +1 -0
  45. package/dist/gameservers/generic/emitter.js +31 -0
  46. package/dist/gameservers/generic/emitter.js.map +1 -0
  47. package/dist/gameservers/generic/index.d.ts +36 -0
  48. package/dist/gameservers/generic/index.d.ts.map +1 -0
  49. package/dist/gameservers/generic/index.js +170 -0
  50. package/dist/gameservers/generic/index.js.map +1 -0
  51. package/dist/gameservers/rust/connectionInfo.d.ts +29 -0
  52. package/dist/gameservers/rust/connectionInfo.d.ts.map +1 -0
  53. package/dist/gameservers/rust/connectionInfo.js +51 -0
  54. package/dist/gameservers/rust/connectionInfo.js.map +1 -0
  55. package/dist/gameservers/rust/emitter.d.ts +32 -0
  56. package/dist/gameservers/rust/emitter.d.ts.map +1 -0
  57. package/dist/gameservers/rust/emitter.js +160 -0
  58. package/dist/gameservers/rust/emitter.js.map +1 -0
  59. package/dist/gameservers/rust/index.d.ts +35 -0
  60. package/dist/gameservers/rust/index.d.ts.map +1 -0
  61. package/dist/gameservers/rust/index.js +217 -0
  62. package/dist/gameservers/rust/index.js.map +1 -0
  63. package/dist/gameservers/rust/items-rust.json +20771 -0
  64. package/dist/getGame.d.ts +10 -0
  65. package/dist/getGame.d.ts.map +1 -0
  66. package/dist/getGame.js +27 -0
  67. package/dist/getGame.js.map +1 -0
  68. package/dist/interfaces/GameServer.d.ts +99 -0
  69. package/dist/interfaces/GameServer.d.ts.map +1 -0
  70. package/dist/interfaces/GameServer.js +235 -0
  71. package/dist/interfaces/GameServer.js.map +1 -0
  72. package/dist/main.d.ts +12 -0
  73. package/dist/main.d.ts.map +1 -0
  74. package/dist/main.js +12 -0
  75. package/dist/main.js.map +1 -0
  76. package/package.json +30 -0
  77. package/src/TakaroEmitter.ts +146 -0
  78. package/src/TakaroEmitter.unit.test.ts +201 -0
  79. package/src/__tests__/gameEventEmitter.test.ts +29 -0
  80. package/src/config.ts +20 -0
  81. package/src/gameservers/7d2d/__tests__/7d2dActions.unit.test.ts +96 -0
  82. package/src/gameservers/7d2d/__tests__/7d2dEventDetection.unit.test.ts +424 -0
  83. package/src/gameservers/7d2d/apiResponses.ts +213 -0
  84. package/src/gameservers/7d2d/connectionInfo.ts +46 -0
  85. package/src/gameservers/7d2d/emitter.ts +334 -0
  86. package/src/gameservers/7d2d/emitter.unit.test.ts +117 -0
  87. package/src/gameservers/7d2d/index.ts +367 -0
  88. package/src/gameservers/7d2d/itemWorker.ts +41 -0
  89. package/src/gameservers/7d2d/items-7d2d.json +25051 -0
  90. package/src/gameservers/7d2d/sdtdAPIClient.ts +82 -0
  91. package/src/gameservers/generic/connectionInfo.ts +19 -0
  92. package/src/gameservers/generic/connectorClient.ts +73 -0
  93. package/src/gameservers/generic/emitter.ts +36 -0
  94. package/src/gameservers/generic/index.ts +193 -0
  95. package/src/gameservers/rust/__tests__/rustActions.unit.test.ts +141 -0
  96. package/src/gameservers/rust/connectionInfo.ts +35 -0
  97. package/src/gameservers/rust/emitter.ts +198 -0
  98. package/src/gameservers/rust/emitter.unit.test.ts +95 -0
  99. package/src/gameservers/rust/index.ts +270 -0
  100. package/src/gameservers/rust/items-rust.json +20771 -0
  101. package/src/getGame.ts +34 -0
  102. package/src/interfaces/GameServer.ts +215 -0
  103. package/src/main.ts +16 -0
  104. package/tsconfig.build.json +16 -0
  105. package/tsconfig.json +9 -0
  106. package/typedoc.json +3 -0
@@ -0,0 +1,367 @@
1
+ import { errors, logger, traceableClass } from '@takaro/util';
2
+ import { IGamePlayer, IPosition } from '@takaro/modules';
3
+ import {
4
+ BanDTO,
5
+ CommandOutput,
6
+ IEntityDTO,
7
+ IGameServer,
8
+ IItemDTO,
9
+ IMessageOptsDTO,
10
+ IPlayerReferenceDTO,
11
+ TestReachabilityOutputDTO,
12
+ ILocationDTO,
13
+ } from '../../interfaces/GameServer.js';
14
+ import { SevenDaysToDieEmitter } from './emitter.js';
15
+ import { SdtdApiClient } from './sdtdAPIClient.js';
16
+ import { Settings } from '@takaro/apiclient';
17
+
18
+ import { SdtdConnectionInfo } from './connectionInfo.js';
19
+ import { InventoryItem } from './apiResponses.js';
20
+ import { Worker } from 'worker_threads';
21
+ import path from 'path';
22
+ import * as url from 'url';
23
+ import { DateTime, Duration, DurationLikeObject } from 'luxon';
24
+
25
+ const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
26
+
27
+ @traceableClass('game:7d2d')
28
+ export class SevenDaysToDie implements IGameServer {
29
+ private logger = logger('7D2D');
30
+ private apiClient: SdtdApiClient;
31
+ connectionInfo: SdtdConnectionInfo;
32
+
33
+ constructor(
34
+ config: SdtdConnectionInfo,
35
+ private settings: Partial<Settings> = {},
36
+ ) {
37
+ this.connectionInfo = config;
38
+ this.apiClient = new SdtdApiClient(this.connectionInfo);
39
+ }
40
+
41
+ getEventEmitter() {
42
+ const emitter = new SevenDaysToDieEmitter(this.connectionInfo);
43
+ return emitter;
44
+ }
45
+
46
+ async getPlayer(player: IPlayerReferenceDTO): Promise<IGamePlayer | null> {
47
+ const players = await this.getPlayers();
48
+ return players.find((p) => p.gameId === player.gameId) || null;
49
+ }
50
+
51
+ async getPlayers(): Promise<IGamePlayer[]> {
52
+ const onlinePlayersRes = await this.apiClient.getOnlinePlayers();
53
+
54
+ const players = await Promise.all(
55
+ onlinePlayersRes.data.map((p) => {
56
+ const data: Partial<IGamePlayer> = {
57
+ gameId: p.crossplatformid.replace('EOS_', ''),
58
+ ip: p.ip,
59
+ name: p.name,
60
+ epicOnlineServicesId: p.crossplatformid.replace('EOS_', ''),
61
+ ping: p.ping,
62
+ };
63
+
64
+ if (p.steamid.startsWith('XBL_')) {
65
+ data.xboxLiveId = p.steamid.replace('XBL_', '');
66
+ }
67
+
68
+ if (p.steamid.startsWith('Steam_')) {
69
+ data.steamId = p.steamid.replace('Steam_', '');
70
+ }
71
+
72
+ return new IGamePlayer(data);
73
+ }),
74
+ );
75
+
76
+ return players;
77
+ }
78
+
79
+ async steamIdOrXboxToGameId(id: string): Promise<IGamePlayer | undefined> {
80
+ if (!id) return undefined;
81
+ if (id.startsWith('Steam_')) id = id.replace('Steam_', '');
82
+ if (id.startsWith('XBL_')) id = id.replace('XBL_', '');
83
+ const players = await this.getPlayers();
84
+ const player = players.find((p) => p.steamId === id || p.epicOnlineServicesId === id || p.xboxLiveId === id);
85
+ return player;
86
+ }
87
+
88
+ async getPlayerLocation(player: IPlayerReferenceDTO): Promise<IPosition | null> {
89
+ const locations = await this.apiClient.getPlayersLocation();
90
+ const playerLocation = locations.data.find((location) => location.crossplatformid === `EOS_${player.gameId}`);
91
+
92
+ if (!playerLocation) {
93
+ return null;
94
+ }
95
+
96
+ return new IPosition({
97
+ x: playerLocation.position.x,
98
+ y: playerLocation.position.y,
99
+ z: playerLocation.position.z,
100
+ });
101
+ }
102
+
103
+ async giveItem(player: IPlayerReferenceDTO, item: string, amount: number = 1, quality?: string): Promise<void> {
104
+ const command = this.connectionInfo.useCPM
105
+ ? `giveplus EOS_${player.gameId} ${item} ${amount} ${quality ? quality + ' 0' : ''}`
106
+ : `give EOS_${player.gameId} ${item} ${amount} ${quality ?? ''}`;
107
+ const res = await this.executeConsoleCommand(command);
108
+
109
+ if (this.connectionInfo.useCPM && !res.rawResult.includes('Item(s) given')) {
110
+ this.logger.error('Failed to give item', { player, item, amount, quality, rawResult: res.rawResult });
111
+
112
+ if (res.rawResult.includes('does not support quality')) {
113
+ this.logger.warn('Item does not support quality, retrying without quality');
114
+ return this.giveItem(player, item, amount);
115
+ }
116
+
117
+ throw new errors.BadRequestError(`Failed to give item. Result: "${res.rawResult}"`);
118
+ }
119
+ }
120
+
121
+ async testReachability(): Promise<TestReachabilityOutputDTO> {
122
+ const start = Date.now();
123
+ try {
124
+ const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Request timed out')), 10000));
125
+
126
+ await Promise.all([
127
+ Promise.race([this.apiClient.getStats(), timeout]),
128
+ Promise.race([this.executeConsoleCommand('version'), timeout]),
129
+ ]);
130
+ } catch (error) {
131
+ let reason = 'Unexpected error, this might be a bug';
132
+ this.logger.warn('Reachability test requests failed', error);
133
+
134
+ if (error instanceof Object && 'details' in error) {
135
+ reason =
136
+ 'Did not receive a response, please check that the server is running, the IP/port is correct and that it is not firewalled';
137
+ if (error.details instanceof Object) {
138
+ if ('status' in error.details) {
139
+ if (error.details.status === 403 || error.details.status === 401) {
140
+ reason = 'Unauthorized, please check that the admin user and token are correct';
141
+ }
142
+ }
143
+ }
144
+ } else if (error instanceof Object && 'message' in error && error.message === 'Request timed out') {
145
+ reason = 'Request timed out, the server did not respond in the allocated time';
146
+ }
147
+
148
+ return new TestReachabilityOutputDTO({
149
+ connectable: false,
150
+ reason,
151
+ });
152
+ }
153
+
154
+ const end = Date.now();
155
+ return new TestReachabilityOutputDTO({
156
+ connectable: true,
157
+ latency: end - start,
158
+ });
159
+ }
160
+
161
+ async executeConsoleCommand(rawCommand: string) {
162
+ const encodedCommand = encodeURIComponent(rawCommand);
163
+ const result = await this.apiClient.executeConsoleCommand(encodedCommand);
164
+
165
+ this.logger.debug(`Executed command: "${rawCommand}"`, { rawCommand, result: result.data.result.slice(0, 1000) });
166
+ return new CommandOutput({
167
+ rawResult: result.data.result,
168
+ success: true,
169
+ });
170
+ }
171
+
172
+ async sendMessage(message: string, opts?: IMessageOptsDTO) {
173
+ const escapedMessage = message.replaceAll(/"/g, "'");
174
+
175
+ let command = `say "${escapedMessage}"`;
176
+
177
+ if (opts?.recipient?.gameId) {
178
+ command = `sayplayer "EOS_${opts.recipient.gameId}" "${escapedMessage}"`;
179
+ }
180
+
181
+ if (this.connectionInfo.useCPM) {
182
+ const sender = opts?.senderNameOverride || this.settings.serverChatName || 'Takaro';
183
+ command = `say2 "${sender}" "${escapedMessage}"`;
184
+
185
+ if (opts?.recipient?.gameId) {
186
+ command = `pm2 "${sender}" "EOS_${opts.recipient.gameId}" "${escapedMessage}"`;
187
+ }
188
+ }
189
+
190
+ await this.executeConsoleCommand(command);
191
+ }
192
+
193
+ async teleportPlayer(player: IGamePlayer, x: number, y: number, z: number, _dimension?: string) {
194
+ // 7D2D doesn't support dimensions, so we ignore the dimension parameter
195
+ const command = `teleportplayer EOS_${player.gameId} ${x} ${y} ${z}`;
196
+ await this.executeConsoleCommand(command);
197
+ }
198
+
199
+ async kickPlayer(player: IPlayerReferenceDTO, reason: string) {
200
+ const command = `kick "EOS_${player.gameId}" "${reason}"`;
201
+ await this.executeConsoleCommand(command);
202
+ }
203
+
204
+ async banPlayer(options: BanDTO) {
205
+ // If no expiresAt is provided, assume 'permanent'. 500 years is pretty long ;)
206
+ const expiresAt = options.expiresAt ?? '2521-01-01T00:00:00.000';
207
+
208
+ const expiresAtDate = DateTime.fromISO(expiresAt);
209
+ const now = DateTime.local();
210
+ let duration = Duration.fromMillis(expiresAtDate.diff(now).milliseconds);
211
+
212
+ let unit: keyof DurationLikeObject = 'minute';
213
+ duration = duration.shiftTo('minutes'); // Convert to minutes
214
+
215
+ if (duration.minutes >= 60) {
216
+ unit = 'hour';
217
+ duration = duration.shiftTo('hours'); // Convert to hours
218
+ }
219
+
220
+ if (duration.hours >= 24) {
221
+ unit = 'day';
222
+ duration = duration.shiftTo('days'); // Convert to days
223
+ }
224
+
225
+ if (duration.days >= 7) {
226
+ unit = 'week';
227
+ duration = duration.shiftTo('weeks'); // Convert to weeks
228
+ }
229
+
230
+ if (duration.weeks >= 4) {
231
+ unit = 'month';
232
+ duration = duration.shiftTo('months'); // Convert to months
233
+ }
234
+
235
+ if (duration.months >= 12) {
236
+ unit = 'year';
237
+ duration = duration.shiftTo('years'); // Convert to years
238
+ }
239
+
240
+ const command = `ban add EOS_${options.player.gameId} ${Math.round(duration.as(unit))} ${unit} "${options.reason}"`;
241
+ await this.executeConsoleCommand(command);
242
+ }
243
+
244
+ async unbanPlayer(player: IPlayerReferenceDTO) {
245
+ const command = `ban remove EOS_${player.gameId}`;
246
+ await this.executeConsoleCommand(command);
247
+ }
248
+
249
+ async listBans(): Promise<BanDTO[]> {
250
+ // Execute the console command and get the raw result.
251
+ const bansRes = await this.executeConsoleCommand('ban list');
252
+
253
+ // Check if the command was successful and if there is a raw result.
254
+ if (!bansRes.success || !bansRes.rawResult) {
255
+ throw new Error('Failed to retrieve ban list.');
256
+ }
257
+
258
+ // Extract and parse the bans from the raw result.
259
+ const banEntries = bansRes.rawResult.split('\n').slice(1); // Skip the header line
260
+ const bans: BanDTO[] = [];
261
+
262
+ for (const entry of banEntries) {
263
+ const match = entry.match(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) - (\S+) \(([^)]*)\) - (.*)/);
264
+
265
+ // If the entry is valid, extract the details and push to the bans array.
266
+ if (match) {
267
+ const [, date, gameId, _displayName, reason] = match;
268
+ const expiresAt = date.replace(' ', 'T') + '.000Z'; // Keep the time in its original form
269
+ // If the saved ban isn't saved with EOS, we cannot resolve to gameId, so skip these.
270
+ if (!gameId.includes('EOS_')) continue;
271
+ bans.push(
272
+ new BanDTO({
273
+ player: new IGamePlayer({
274
+ gameId: gameId.replace('EOS_', ''),
275
+ epicOnlineServicesId: gameId.replace('EOS_', ''),
276
+ }),
277
+ reason,
278
+ expiresAt,
279
+ }),
280
+ );
281
+ }
282
+ }
283
+
284
+ return bans;
285
+ }
286
+
287
+ async listItems(): Promise<IItemDTO[]> {
288
+ const itemsRes = await this.executeConsoleCommand('li *');
289
+ const itemLines = itemsRes.rawResult.split('\n').slice(0, -2);
290
+
291
+ return new Promise((resolve, reject) => {
292
+ const workerPath = path.join(__dirname, 'itemWorker.js');
293
+
294
+ const worker = new Worker(workerPath);
295
+ worker.postMessage(itemLines);
296
+
297
+ worker.on('message', (parsedItems) => {
298
+ if (parsedItems.error) {
299
+ reject(parsedItems.error);
300
+ } else {
301
+ resolve(parsedItems);
302
+ }
303
+ worker.terminate();
304
+ });
305
+
306
+ worker.on('error', reject);
307
+ worker.on('exit', (code) => {
308
+ if (code !== 0) {
309
+ reject(new Error(`Worker stopped with exit code ${code}`));
310
+ }
311
+ });
312
+ });
313
+ }
314
+
315
+ async getPlayerInventory(player: IPlayerReferenceDTO): Promise<IItemDTO[]> {
316
+ const inventoryRes = await this.apiClient.getPlayerInventory(`EOS_${player.gameId}`);
317
+ const resp: IItemDTO[] = [];
318
+
319
+ const mapSdtdItemToDto = async (item: InventoryItem | null) => {
320
+ if (!item) return null;
321
+ return item.quality
322
+ ? new IItemDTO({ code: item.name, amount: item.count, quality: item.quality })
323
+ : new IItemDTO({ code: item.name, amount: item.count });
324
+ };
325
+
326
+ const dtos = await Promise.all([
327
+ ...inventoryRes.data.bag.map(mapSdtdItemToDto),
328
+ ...inventoryRes.data.belt.map(mapSdtdItemToDto),
329
+ ]);
330
+
331
+ const filteredDTOs = dtos.filter((item) => item !== null) as IItemDTO[];
332
+ resp.push(...filteredDTOs);
333
+
334
+ for (const slot in inventoryRes.data.equipment) {
335
+ if (Object.prototype.hasOwnProperty.call(inventoryRes.data.equipment, slot)) {
336
+ const element = inventoryRes.data.equipment[slot];
337
+ if (element) resp.push(new IItemDTO({ code: element.name, amount: element.count }));
338
+ }
339
+ }
340
+
341
+ return resp;
342
+ }
343
+
344
+ async shutdown(): Promise<void> {
345
+ if (this.connectionInfo.useCPM) {
346
+ await this.executeConsoleCommand('shutdownba 0');
347
+ } else {
348
+ await this.executeConsoleCommand('shutdown');
349
+ }
350
+ }
351
+
352
+ async getMapInfo() {
353
+ return this.apiClient.getMapInfo();
354
+ }
355
+
356
+ async getMapTile(x: number, y: number, z: number) {
357
+ return this.apiClient.getMapTile(x, y, z);
358
+ }
359
+
360
+ async listEntities(): Promise<IEntityDTO[]> {
361
+ throw new errors.NotImplementedError();
362
+ }
363
+
364
+ async listLocations(): Promise<ILocationDTO[]> {
365
+ throw new errors.NotImplementedError();
366
+ }
367
+ }
@@ -0,0 +1,41 @@
1
+ import { parentPort } from 'worker_threads';
2
+ import type { IItemDTO } from '../../interfaces/GameServer.js';
3
+ import path from 'path';
4
+ import { readFile } from 'fs/promises';
5
+ import * as url from 'url';
6
+
7
+ const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
8
+ const itemsJsonPath = path.join(__dirname, 'items-7d2d.json');
9
+
10
+ if (!parentPort) {
11
+ throw new Error('This file must be run as a worker thread');
12
+ }
13
+
14
+ parentPort.on('message', async (itemLines) => {
15
+ if (!parentPort) {
16
+ throw new Error('This file must be run as a worker thread');
17
+ }
18
+
19
+ const vanillaItems = JSON.parse(await readFile(itemsJsonPath, 'utf8'));
20
+
21
+ try {
22
+ const parsedItems = [];
23
+ for (const line of itemLines) {
24
+ const trimmed = line.trim();
25
+ const dto: Partial<IItemDTO> = { code: trimmed };
26
+
27
+ if (trimmed in vanillaItems) {
28
+ dto.name = vanillaItems[trimmed].name;
29
+ dto.description = vanillaItems[trimmed].description;
30
+ }
31
+
32
+ if (!dto.name) dto.name = dto.code;
33
+
34
+ parsedItems.push(dto);
35
+ }
36
+ parentPort.postMessage(parsedItems);
37
+ } catch (error) {
38
+ if (!error) throw new Error('Error parsing items');
39
+ parentPort.postMessage({ error });
40
+ }
41
+ });