@takaro/gameserver 0.0.0-next.0da151e

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 +16 -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 +9 -0
  105. package/tsconfig.json +9 -0
  106. package/typedoc.json +3 -0
@@ -0,0 +1,198 @@
1
+ import WebSocket from 'ws';
2
+ import { logger, errors } from '@takaro/util';
3
+ import { RustConnectionInfo } from './connectionInfo.js';
4
+ import { TakaroEmitter } from '../../TakaroEmitter.js';
5
+ import {
6
+ EventChatMessage,
7
+ EventEntityKilled,
8
+ EventLogLine,
9
+ EventPlayerConnected,
10
+ EventPlayerDeath,
11
+ EventPlayerDisconnected,
12
+ GameEvents,
13
+ GameEventsMapping,
14
+ } from '@takaro/modules';
15
+ import { ValueOf } from 'type-fest';
16
+
17
+ export enum RustEventType {
18
+ DEFAULT = 'Generic',
19
+ WARNING = 'Warning',
20
+ CHAT = 'Chat',
21
+ }
22
+
23
+ export interface RustEvent {
24
+ Message: string;
25
+ Identifier: number;
26
+ Type: RustEventType;
27
+ Stacktrace: string;
28
+ }
29
+
30
+ enum RustTypesType {
31
+ PLAYER_CONNECTED = 0,
32
+ PLAYER_DISCONNECTED = 1,
33
+ CHAT_MESSAGE = 2,
34
+ PLAYER_DEATH = 3,
35
+ ENTITY_KILLED = 4,
36
+ }
37
+
38
+ export class RustEmitter extends TakaroEmitter {
39
+ private ws: WebSocket | null = null;
40
+ private log = logger('rust:ws');
41
+ private boundListener = (m: Buffer) => this.listener(m.toString());
42
+
43
+ constructor(private config: RustConnectionInfo) {
44
+ super();
45
+ }
46
+
47
+ static async getClient(config: RustConnectionInfo) {
48
+ const log = logger('rust:ws');
49
+
50
+ const protocol = config.useTls ? 'wss' : 'ws';
51
+ const client = new WebSocket(`${protocol}://${config.host}:${config.rconPort}/${config.rconPassword}`);
52
+
53
+ log.debug('getClient', {
54
+ host: config.host,
55
+ port: config.rconPort,
56
+ });
57
+
58
+ return Promise.race([
59
+ new Promise<WebSocket>((resolve, reject) => {
60
+ client?.on('error', (err) => {
61
+ log.warn('getClient', err);
62
+ client?.close();
63
+ return reject(err);
64
+ });
65
+ client?.on('unexpected-response', (req, res) => {
66
+ log.debug('unexpected-response', {
67
+ req,
68
+ res,
69
+ });
70
+ reject(new errors.InternalServerError());
71
+ });
72
+ client?.on('open', () => {
73
+ log.debug('Connection opened');
74
+ if (client) {
75
+ return resolve(client);
76
+ }
77
+ });
78
+ }),
79
+ new Promise<WebSocket>((_, reject) => {
80
+ setTimeout(() => reject(new errors.WsTimeOutError('Timeout')), 5000);
81
+ }),
82
+ ]);
83
+ }
84
+
85
+ async start(): Promise<void> {
86
+ this.ws = await RustEmitter.getClient(this.config);
87
+
88
+ this.ws?.on('message', this.boundListener);
89
+ }
90
+
91
+ async stop(): Promise<void> {
92
+ this.ws?.off('message', this.boundListener);
93
+ this.ws?.close();
94
+ this.log.debug('Websocket connection has been closed');
95
+ return;
96
+ }
97
+
98
+ async parseMessage(e: RustEvent) {
99
+ if (e.Message.includes('[Hook Parser]')) {
100
+ const parsed = JSON.parse(e.Message.replace('[Hook Parser]', ''));
101
+ let dto: InstanceType<ValueOf<typeof GameEventsMapping>> | null = null;
102
+ switch (parsed.type) {
103
+ case RustTypesType.PLAYER_CONNECTED:
104
+ dto = await this.handlePlayerConnected(parsed.data);
105
+ break;
106
+ case RustTypesType.PLAYER_DISCONNECTED:
107
+ dto = await this.handlePlayerDisconnected(parsed.data);
108
+ break;
109
+ case RustTypesType.CHAT_MESSAGE:
110
+ dto = await this.handleChatMessage(parsed.data);
111
+ break;
112
+ case RustTypesType.PLAYER_DEATH:
113
+ dto = await this.handlePlayerDeath(parsed.data);
114
+ break;
115
+ case RustTypesType.ENTITY_KILLED:
116
+ dto = await this.handleEntityKilled(parsed.data);
117
+ break;
118
+ default:
119
+ this.log.warn('Unknown event type', parsed);
120
+ break;
121
+ }
122
+
123
+ if (!dto) {
124
+ this.log.warn('dto undefined, could not determine type?', parsed);
125
+ return;
126
+ }
127
+
128
+ await dto.validate({
129
+ whitelist: false,
130
+ });
131
+ this.emit(dto.type, dto);
132
+ }
133
+
134
+ this.emit(
135
+ GameEvents.LOG_LINE,
136
+ new EventLogLine({
137
+ msg: e.Message,
138
+ }),
139
+ );
140
+ }
141
+
142
+ private async handlePlayerConnected(data: Record<string, unknown>): Promise<EventPlayerConnected> {
143
+ const event = new EventPlayerConnected(data);
144
+ if (event.player.steamId) {
145
+ event.player.gameId = event.player.steamId;
146
+ }
147
+ return event;
148
+ }
149
+
150
+ private async handlePlayerDisconnected(data: Record<string, unknown>): Promise<EventPlayerDisconnected> {
151
+ const event = new EventPlayerDisconnected(data);
152
+ if (event.player.steamId) {
153
+ event.player.gameId = event.player.steamId;
154
+ }
155
+ return event;
156
+ }
157
+
158
+ private async handleChatMessage(data: Record<string, string>): Promise<EventChatMessage> {
159
+ data.channel = data.channel.toLowerCase();
160
+ const event = new EventChatMessage(data);
161
+ if (event.player) {
162
+ if (event.player.steamId) {
163
+ event.player.gameId = event.player.steamId;
164
+ }
165
+ }
166
+
167
+ return event;
168
+ }
169
+
170
+ private async handlePlayerDeath(data: Record<string, unknown>): Promise<EventPlayerDeath> {
171
+ const event = new EventPlayerDeath(data);
172
+ if (event.player.steamId) {
173
+ event.player.gameId = event.player.steamId;
174
+ }
175
+ if (event.attacker && event.attacker.steamId) {
176
+ event.attacker.gameId = event.attacker.steamId;
177
+ }
178
+ return event;
179
+ }
180
+
181
+ private async handleEntityKilled(data: Record<string, unknown>): Promise<EventEntityKilled> {
182
+ const event = new EventEntityKilled(data);
183
+ if (event.player.steamId) {
184
+ event.player.gameId = event.player.steamId;
185
+ }
186
+ return event;
187
+ }
188
+
189
+ async listener(data: string) {
190
+ try {
191
+ const event = JSON.parse(data);
192
+ await this.parseMessage(event);
193
+ this.log.debug('event: ', event);
194
+ } catch (error) {
195
+ this.log.error('Error handling message from game server', error);
196
+ }
197
+ }
198
+ }
@@ -0,0 +1,95 @@
1
+ import 'reflect-metadata';
2
+ import { RustEmitter } from './emitter.js';
3
+ import { RustConnectionInfo } from './connectionInfo.js';
4
+ import { expect, sandbox } from '@takaro/test';
5
+ import { describe, it, beforeEach, afterEach } from 'node:test';
6
+ import WebSocket from 'ws';
7
+
8
+ describe('RustEmitter', () => {
9
+ let emitter: RustEmitter;
10
+
11
+ beforeEach(() => {
12
+ emitter = new RustEmitter(
13
+ new RustConnectionInfo({
14
+ host: 'localhost',
15
+ rconPort: '28016',
16
+ rconPassword: 'test',
17
+ useTls: false,
18
+ }),
19
+ );
20
+ });
21
+
22
+ afterEach(async () => {
23
+ if (emitter) {
24
+ try {
25
+ await emitter.stop();
26
+ } catch {
27
+ // Ignore errors during cleanup
28
+ }
29
+ }
30
+ sandbox.restore();
31
+ });
32
+
33
+ it('Does not accumulate listeners on start/stop/start cycle', async () => {
34
+ // Mock WebSocket to prevent actual network calls
35
+ const mockWs = {
36
+ on: sandbox.spy(),
37
+ off: sandbox.spy(),
38
+ close: sandbox.spy(),
39
+ readyState: WebSocket.OPEN,
40
+ };
41
+
42
+ // Stub getClient to return mock WebSocket
43
+ sandbox.stub(RustEmitter, 'getClient').resolves(mockWs as any);
44
+
45
+ // First start
46
+ await emitter.start();
47
+
48
+ // Verify listener was added once
49
+ expect(mockWs.on).to.have.been.calledOnce;
50
+ expect(mockWs.on).to.have.been.calledWith('message');
51
+
52
+ const firstAddedListener = mockWs.on.getCall(0).args[1];
53
+
54
+ // Stop
55
+ await emitter.stop();
56
+
57
+ // Verify listener was removed
58
+ expect(mockWs.off).to.have.been.calledOnce;
59
+ expect(mockWs.off).to.have.been.calledWith('message');
60
+
61
+ const removedListener = mockWs.off.getCall(0).args[1];
62
+
63
+ // Verify the same function reference was used for on and off
64
+ expect(firstAddedListener).to.equal(
65
+ removedListener,
66
+ 'on() and off() must use the same function reference to properly remove the listener',
67
+ );
68
+
69
+ expect(mockWs.close).to.have.been.calledOnce;
70
+
71
+ // Reset the mock for second start
72
+ mockWs.on.resetHistory();
73
+ mockWs.off.resetHistory();
74
+
75
+ // Second start
76
+ await emitter.start();
77
+
78
+ // Verify listener was added again
79
+ expect(mockWs.on).to.have.been.calledOnce;
80
+ expect(mockWs.on).to.have.been.calledWith('message');
81
+
82
+ const secondAddedListener = mockWs.on.getCall(0).args[1];
83
+
84
+ // Verify the same listener reference is reused
85
+ expect(firstAddedListener).to.equal(
86
+ secondAddedListener,
87
+ 'The same listener reference must be reused across restarts to prevent accumulation',
88
+ );
89
+ });
90
+
91
+ it('Uses the same function reference for on and off', () => {
92
+ // Verify boundListener is defined as a class field
93
+ expect((emitter as any).boundListener).to.be.a('function');
94
+ });
95
+ });
@@ -0,0 +1,270 @@
1
+ import { errors, logger, traceableClass } from '@takaro/util';
2
+ import WebSocket from 'ws';
3
+ import { IGamePlayer, IPosition } from '@takaro/modules';
4
+ import {
5
+ BanDTO,
6
+ CommandOutput,
7
+ IEntityDTO,
8
+ IGameServer,
9
+ IItemDTO,
10
+ ILocationDTO,
11
+ IMessageOptsDTO,
12
+ IPlayerReferenceDTO,
13
+ MapInfoDTO,
14
+ TestReachabilityOutputDTO,
15
+ } from '../../interfaces/GameServer.js';
16
+ import { RustConnectionInfo } from './connectionInfo.js';
17
+ import { RustEmitter } from './emitter.js';
18
+ import { Settings } from '@takaro/apiclient';
19
+
20
+ const itemsJson = (await import('./items-rust.json', { with: { type: 'json' } })).default;
21
+
22
+ @traceableClass('game:rust')
23
+ export class Rust implements IGameServer {
24
+ private log = logger('rust');
25
+ connectionInfo: RustConnectionInfo;
26
+ private client: WebSocket | null;
27
+
28
+ constructor(
29
+ config: RustConnectionInfo,
30
+ private settings: Partial<Settings> = {},
31
+ ) {
32
+ this.connectionInfo = config;
33
+ }
34
+
35
+ private getRequestId(): number {
36
+ return Math.floor(Math.random() * 100000000);
37
+ }
38
+
39
+ private async getClient() {
40
+ if (this.client && this.client.readyState === WebSocket.OPEN) {
41
+ return this.client;
42
+ }
43
+
44
+ this.client = await RustEmitter.getClient(this.connectionInfo);
45
+ return this.client;
46
+ }
47
+
48
+ getEventEmitter() {
49
+ const emitter = new RustEmitter(this.connectionInfo);
50
+ return emitter;
51
+ }
52
+
53
+ async getPlayer(player: IPlayerReferenceDTO): Promise<IGamePlayer | null> {
54
+ const players = await this.getPlayers();
55
+ return players.find((p) => p.gameId === player.gameId) || null;
56
+ }
57
+
58
+ async getPlayers(): Promise<IGamePlayer[]> {
59
+ const response = await this.executeConsoleCommand('playerlist');
60
+ const rustPlayers = JSON.parse(response.rawResult);
61
+ return Promise.all(
62
+ rustPlayers.map((player: any) => {
63
+ return new IGamePlayer({
64
+ gameId: player.SteamID,
65
+ steamId: player.SteamID,
66
+ ip: player.Address.split(':')[0],
67
+ name: player.DisplayName,
68
+ ping: player.Ping,
69
+ });
70
+ }),
71
+ );
72
+ }
73
+
74
+ async giveItem(player: IPlayerReferenceDTO, item: string, amount: number = 1, _quality?: string): Promise<void> {
75
+ await this.executeConsoleCommand(`inventory.giveid ${player.gameId} ${item} ${amount}`);
76
+ }
77
+
78
+ async getPlayerLocation(player: IPlayerReferenceDTO): Promise<IPosition | null> {
79
+ const rawResponse = await this.executeConsoleCommand('playerlistpos');
80
+ const lines = rawResponse.rawResult.split('\n');
81
+
82
+ for (const line of lines) {
83
+ const matches = /(\d{17}) \w+\s+\(([-\d\.]+), ([-\d\.]+), ([-\d\.]+)\)/.exec(line);
84
+
85
+ if (matches) {
86
+ const steamId = matches[1];
87
+ const x = matches[2].replace('(', '');
88
+ const y = matches[3].replace(',', '');
89
+ const z = matches[4].replace(')', '');
90
+
91
+ if (steamId === player.gameId) {
92
+ return new IPosition({
93
+ x: parseInt(x, 10),
94
+ y: parseInt(y, 10),
95
+ z: parseInt(z, 10),
96
+ });
97
+ }
98
+ }
99
+ }
100
+
101
+ return null;
102
+ }
103
+
104
+ async testReachability(): Promise<TestReachabilityOutputDTO> {
105
+ const start = Date.now();
106
+ try {
107
+ await this.executeConsoleCommand('serverinfo');
108
+ const end = Date.now();
109
+ return new TestReachabilityOutputDTO({ connectable: true, latency: end - start });
110
+ } catch (error) {
111
+ this.log.warn('testReachability', error);
112
+ return new TestReachabilityOutputDTO({ connectable: false });
113
+ }
114
+ }
115
+
116
+ async executeConsoleCommand(rawCommand: string) {
117
+ const client = await this.getClient();
118
+ return new Promise<CommandOutput>((resolve, reject) => {
119
+ const command = rawCommand.trim();
120
+ const requestId = this.getRequestId();
121
+
122
+ const timeout = setTimeout(() => reject(), 5000);
123
+
124
+ client.on('message', (data) => {
125
+ const parsed = JSON.parse(data.toString());
126
+
127
+ if (parsed.Identifier !== requestId) {
128
+ return;
129
+ }
130
+
131
+ const commandResult = parsed.Message;
132
+ clearTimeout(timeout);
133
+ return resolve(new CommandOutput({ rawResult: commandResult }));
134
+ });
135
+
136
+ this.log.debug('executeConsoleCommand - sending command', { command });
137
+ client.send(
138
+ JSON.stringify({
139
+ Message: command,
140
+ Identifier: requestId,
141
+ Name: 'Takaro',
142
+ }),
143
+ );
144
+ });
145
+ }
146
+
147
+ async sendMessage(message: string, _opts?: IMessageOptsDTO) {
148
+ // Note: Rust's say command doesn't support custom sender names
149
+ // The senderNameOverride parameter is ignored for Rust servers
150
+ await this.executeConsoleCommand(`say "${message}"`);
151
+ }
152
+
153
+ async teleportPlayer(player: IGamePlayer, x: number, y: number, z: number, _dimension?: string) {
154
+ // Rust doesn't support dimensions, so we ignore the dimension parameter
155
+ await this.executeConsoleCommand(`teleportplayer.pos ${player.gameId} ${x} ${y} ${z}`);
156
+ }
157
+
158
+ async kickPlayer(player: IGamePlayer, reason: string) {
159
+ await this.executeConsoleCommand(`kick "${player.gameId}" "${reason}"`);
160
+ }
161
+
162
+ async banPlayer(options: BanDTO) {
163
+ // 'find banid'
164
+ // Variables: Commands: global.banid( ) banid <steamid> <username> <reason> [optional duration]
165
+ // This optional duration is an integer, seems to be hours.
166
+
167
+ if (!options.expiresAt) {
168
+ await this.executeConsoleCommand(`banid ${options.player.gameId} "" "${options.reason}"`);
169
+ return;
170
+ }
171
+
172
+ const timeDiff = new Date(options.expiresAt).valueOf() - Date.now();
173
+ const hours = Math.floor(timeDiff / 1000 / 60 / 60);
174
+
175
+ await this.executeConsoleCommand(`banid ${options.player.gameId} "" "${options.reason}" ${hours}`);
176
+ }
177
+
178
+ async unbanPlayer(player: IPlayerReferenceDTO) {
179
+ await this.executeConsoleCommand(`unban ${player.gameId}`);
180
+ }
181
+
182
+ async listBans(): Promise<BanDTO[]> {
183
+ const response = await this.executeConsoleCommand('banlistex');
184
+
185
+ if (!response.rawResult) {
186
+ return [];
187
+ }
188
+
189
+ const lines = response.rawResult.split('\n');
190
+ const pattern =
191
+ /(?:'|^)(?<number>\d+)\s+(?<gameId>\d+)\s+"(?<username>.*?)"\s+"(?<reason>.*?)"\s+(?<expiration>-?\d+)\s*(?:'|\n|$)/g;
192
+ const bans = [];
193
+
194
+ for (const line of lines) {
195
+ let match;
196
+ while ((match = pattern.exec(line)) !== null) {
197
+ if (!match.groups) {
198
+ this.log.warn('listBans - line did not match regex', { match, line });
199
+ continue;
200
+ }
201
+ const { gameId, expiration } = match.groups;
202
+
203
+ let expiresAt = null;
204
+ if (expiration !== '-1') {
205
+ expiresAt = new Date(parseInt(expiration) * 1000).toISOString();
206
+ }
207
+
208
+ const ban = new BanDTO({
209
+ reason: match.groups.reason,
210
+ player: new IGamePlayer({
211
+ gameId,
212
+ steamId: gameId,
213
+ }),
214
+ expiresAt,
215
+ });
216
+
217
+ bans.push(ban);
218
+ }
219
+ }
220
+
221
+ return bans;
222
+ }
223
+
224
+ async listItems(): Promise<IItemDTO[]> {
225
+ return Promise.all(
226
+ Object.values(itemsJson).map((item) => {
227
+ return new IItemDTO({
228
+ code: item.shortname,
229
+ name: item.Name,
230
+ description: item.Description,
231
+ });
232
+ }),
233
+ );
234
+ }
235
+
236
+ async getPlayerInventory(player: IPlayerReferenceDTO): Promise<IItemDTO[]> {
237
+ const res = await this.executeConsoleCommand(`viewinventory ${player.gameId}`);
238
+ const toParse = res.rawResult.replace('[ViewInventory] ', '');
239
+ const parsed = JSON.parse(toParse);
240
+ const items = JSON.parse(parsed.Inventory);
241
+ return await Promise.all(items.map((i: any) => new IItemDTO({ code: i.itemName, amount: i.amount })));
242
+ }
243
+
244
+ async shutdown(): Promise<void> {
245
+ await this.executeConsoleCommand('quit');
246
+ }
247
+
248
+ async getMapInfo(): Promise<MapInfoDTO> {
249
+ return new MapInfoDTO({
250
+ enabled: false,
251
+ mapBlockSize: 0,
252
+ maxZoom: 0,
253
+ mapSizeX: 0,
254
+ mapSizeY: 0,
255
+ mapSizeZ: 0,
256
+ });
257
+ }
258
+
259
+ async getMapTile(_x: number, _y: number, _z: number): Promise<string> {
260
+ throw new Error('Not implemented');
261
+ }
262
+
263
+ async listEntities(): Promise<IEntityDTO[]> {
264
+ throw new errors.NotImplementedError();
265
+ }
266
+
267
+ async listLocations(): Promise<ILocationDTO[]> {
268
+ throw new errors.NotImplementedError();
269
+ }
270
+ }