@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.
- package/README.md +36 -0
- package/dist/TakaroEmitter.d.ts +30 -0
- package/dist/TakaroEmitter.d.ts.map +1 -0
- package/dist/TakaroEmitter.js +101 -0
- package/dist/TakaroEmitter.js.map +1 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +13 -0
- package/dist/config.js.map +1 -0
- package/dist/gameservers/7d2d/apiResponses.d.ts +194 -0
- package/dist/gameservers/7d2d/apiResponses.d.ts.map +1 -0
- package/dist/gameservers/7d2d/apiResponses.js +2 -0
- package/dist/gameservers/7d2d/apiResponses.js.map +1 -0
- package/dist/gameservers/7d2d/connectionInfo.d.ts +38 -0
- package/dist/gameservers/7d2d/connectionInfo.d.ts.map +1 -0
- package/dist/gameservers/7d2d/connectionInfo.js +66 -0
- package/dist/gameservers/7d2d/connectionInfo.js.map +1 -0
- package/dist/gameservers/7d2d/emitter.d.ts +32 -0
- package/dist/gameservers/7d2d/emitter.d.ts.map +1 -0
- package/dist/gameservers/7d2d/emitter.js +273 -0
- package/dist/gameservers/7d2d/emitter.js.map +1 -0
- package/dist/gameservers/7d2d/index.d.ts +34 -0
- package/dist/gameservers/7d2d/index.d.ts.map +1 -0
- package/dist/gameservers/7d2d/index.js +304 -0
- package/dist/gameservers/7d2d/index.js.map +1 -0
- package/dist/gameservers/7d2d/itemWorker.d.ts +2 -0
- package/dist/gameservers/7d2d/itemWorker.d.ts.map +1 -0
- package/dist/gameservers/7d2d/itemWorker.js +36 -0
- package/dist/gameservers/7d2d/itemWorker.js.map +1 -0
- package/dist/gameservers/7d2d/items-7d2d.json +25051 -0
- package/dist/gameservers/7d2d/sdtdAPIClient.d.ts +19 -0
- package/dist/gameservers/7d2d/sdtdAPIClient.d.ts.map +1 -0
- package/dist/gameservers/7d2d/sdtdAPIClient.js +57 -0
- package/dist/gameservers/7d2d/sdtdAPIClient.js.map +1 -0
- package/dist/gameservers/generic/connectionInfo.d.ts +16 -0
- package/dist/gameservers/generic/connectionInfo.d.ts.map +1 -0
- package/dist/gameservers/generic/connectionInfo.js +29 -0
- package/dist/gameservers/generic/connectionInfo.js.map +1 -0
- package/dist/gameservers/generic/connectorClient.d.ts +7 -0
- package/dist/gameservers/generic/connectorClient.d.ts.map +1 -0
- package/dist/gameservers/generic/connectorClient.js +60 -0
- package/dist/gameservers/generic/connectorClient.js.map +1 -0
- package/dist/gameservers/generic/emitter.d.ts +13 -0
- package/dist/gameservers/generic/emitter.d.ts.map +1 -0
- package/dist/gameservers/generic/emitter.js +31 -0
- package/dist/gameservers/generic/emitter.js.map +1 -0
- package/dist/gameservers/generic/index.d.ts +36 -0
- package/dist/gameservers/generic/index.d.ts.map +1 -0
- package/dist/gameservers/generic/index.js +170 -0
- package/dist/gameservers/generic/index.js.map +1 -0
- package/dist/gameservers/rust/connectionInfo.d.ts +29 -0
- package/dist/gameservers/rust/connectionInfo.d.ts.map +1 -0
- package/dist/gameservers/rust/connectionInfo.js +51 -0
- package/dist/gameservers/rust/connectionInfo.js.map +1 -0
- package/dist/gameservers/rust/emitter.d.ts +32 -0
- package/dist/gameservers/rust/emitter.d.ts.map +1 -0
- package/dist/gameservers/rust/emitter.js +160 -0
- package/dist/gameservers/rust/emitter.js.map +1 -0
- package/dist/gameservers/rust/index.d.ts +35 -0
- package/dist/gameservers/rust/index.d.ts.map +1 -0
- package/dist/gameservers/rust/index.js +217 -0
- package/dist/gameservers/rust/index.js.map +1 -0
- package/dist/gameservers/rust/items-rust.json +20771 -0
- package/dist/getGame.d.ts +10 -0
- package/dist/getGame.d.ts.map +1 -0
- package/dist/getGame.js +27 -0
- package/dist/getGame.js.map +1 -0
- package/dist/interfaces/GameServer.d.ts +99 -0
- package/dist/interfaces/GameServer.d.ts.map +1 -0
- package/dist/interfaces/GameServer.js +235 -0
- package/dist/interfaces/GameServer.js.map +1 -0
- package/dist/main.d.ts +12 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +12 -0
- package/dist/main.js.map +1 -0
- package/package.json +16 -0
- package/src/TakaroEmitter.ts +146 -0
- package/src/TakaroEmitter.unit.test.ts +201 -0
- package/src/__tests__/gameEventEmitter.test.ts +29 -0
- package/src/config.ts +20 -0
- package/src/gameservers/7d2d/__tests__/7d2dActions.unit.test.ts +96 -0
- package/src/gameservers/7d2d/__tests__/7d2dEventDetection.unit.test.ts +424 -0
- package/src/gameservers/7d2d/apiResponses.ts +213 -0
- package/src/gameservers/7d2d/connectionInfo.ts +46 -0
- package/src/gameservers/7d2d/emitter.ts +334 -0
- package/src/gameservers/7d2d/emitter.unit.test.ts +117 -0
- package/src/gameservers/7d2d/index.ts +367 -0
- package/src/gameservers/7d2d/itemWorker.ts +41 -0
- package/src/gameservers/7d2d/items-7d2d.json +25051 -0
- package/src/gameservers/7d2d/sdtdAPIClient.ts +82 -0
- package/src/gameservers/generic/connectionInfo.ts +19 -0
- package/src/gameservers/generic/connectorClient.ts +73 -0
- package/src/gameservers/generic/emitter.ts +36 -0
- package/src/gameservers/generic/index.ts +193 -0
- package/src/gameservers/rust/__tests__/rustActions.unit.test.ts +141 -0
- package/src/gameservers/rust/connectionInfo.ts +35 -0
- package/src/gameservers/rust/emitter.ts +198 -0
- package/src/gameservers/rust/emitter.unit.test.ts +95 -0
- package/src/gameservers/rust/index.ts +270 -0
- package/src/gameservers/rust/items-rust.json +20771 -0
- package/src/getGame.ts +34 -0
- package/src/interfaces/GameServer.ts +215 -0
- package/src/main.ts +16 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.json +9 -0
- 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
|
+
}
|