@takaro/gameserver 0.0.1
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 +26 -0
- package/dist/TakaroEmitter.js +97 -0
- package/dist/TakaroEmitter.js.map +1 -0
- package/dist/gameservers/7d2d/apiResponses.d.ts +193 -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 +32 -0
- package/dist/gameservers/7d2d/connectionInfo.js +58 -0
- package/dist/gameservers/7d2d/connectionInfo.js.map +1 -0
- package/dist/gameservers/7d2d/emitter.d.ts +30 -0
- package/dist/gameservers/7d2d/emitter.js +261 -0
- package/dist/gameservers/7d2d/emitter.js.map +1 -0
- package/dist/gameservers/7d2d/index.d.ts +28 -0
- package/dist/gameservers/7d2d/index.js +267 -0
- package/dist/gameservers/7d2d/index.js.map +1 -0
- package/dist/gameservers/7d2d/itemWorker.d.ts +1 -0
- package/dist/gameservers/7d2d/itemWorker.js +31 -0
- package/dist/gameservers/7d2d/itemWorker.js.map +1 -0
- package/dist/gameservers/7d2d/items-7d2d.json +17705 -0
- package/dist/gameservers/7d2d/sdtdAPIClient.d.ts +14 -0
- package/dist/gameservers/7d2d/sdtdAPIClient.js +60 -0
- package/dist/gameservers/7d2d/sdtdAPIClient.js.map +1 -0
- package/dist/gameservers/mock/connectionInfo.d.ts +20 -0
- package/dist/gameservers/mock/connectionInfo.js +37 -0
- package/dist/gameservers/mock/connectionInfo.js.map +1 -0
- package/dist/gameservers/mock/emitter.d.ts +12 -0
- package/dist/gameservers/mock/emitter.js +33 -0
- package/dist/gameservers/mock/emitter.js.map +1 -0
- package/dist/gameservers/mock/index.d.ts +31 -0
- package/dist/gameservers/mock/index.js +135 -0
- package/dist/gameservers/mock/index.js.map +1 -0
- package/dist/gameservers/rust/connectionInfo.d.ts +28 -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 +30 -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 +29 -0
- package/dist/gameservers/rust/index.js +189 -0
- package/dist/gameservers/rust/index.js.map +1 -0
- package/dist/gameservers/rust/items-rust.json +20771 -0
- package/dist/getGame.d.ts +8 -0
- package/dist/getGame.js +26 -0
- package/dist/getGame.js.map +1 -0
- package/dist/interfaces/GameServer.d.ts +57 -0
- package/dist/interfaces/GameServer.js +95 -0
- package/dist/interfaces/GameServer.js.map +1 -0
- package/dist/main.d.ts +9 -0
- package/dist/main.js +10 -0
- package/dist/main.js.map +1 -0
- package/package.json +26 -0
- package/src/TakaroEmitter.ts +138 -0
- package/src/TakaroEmitter.unit.test.ts +125 -0
- package/src/__tests__/gameEventEmitter.test.ts +36 -0
- package/src/gameservers/7d2d/__tests__/7d2dActions.unit.test.ts +91 -0
- package/src/gameservers/7d2d/__tests__/7d2dEventDetection.unit.test.ts +324 -0
- package/src/gameservers/7d2d/apiResponses.ts +214 -0
- package/src/gameservers/7d2d/connectionInfo.ts +40 -0
- package/src/gameservers/7d2d/emitter.ts +325 -0
- package/src/gameservers/7d2d/index.ts +318 -0
- package/src/gameservers/7d2d/itemWorker.ts +34 -0
- package/src/gameservers/7d2d/items-7d2d.json +17705 -0
- package/src/gameservers/7d2d/sdtdAPIClient.ts +82 -0
- package/src/gameservers/mock/connectionInfo.ts +25 -0
- package/src/gameservers/mock/emitter.ts +37 -0
- package/src/gameservers/mock/index.ts +156 -0
- package/src/gameservers/rust/__tests__/rustActions.unit.test.ts +140 -0
- package/src/gameservers/rust/connectionInfo.ts +35 -0
- package/src/gameservers/rust/emitter.ts +198 -0
- package/src/gameservers/rust/index.ts +230 -0
- package/src/gameservers/rust/items-rust.json +20771 -0
- package/src/getGame.ts +32 -0
- package/src/interfaces/GameServer.ts +95 -0
- package/src/main.ts +14 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.json +9 -0
- package/typedoc.json +3 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Axios, AxiosResponse } from 'axios';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import { SdtdConnectionInfo } from './connectionInfo.js';
|
|
4
|
+
import {
|
|
5
|
+
CommandResponse,
|
|
6
|
+
InventoryResponse,
|
|
7
|
+
OnlinePlayerResponse,
|
|
8
|
+
PlayerLocation,
|
|
9
|
+
StatsResponse,
|
|
10
|
+
} from './apiResponses.js';
|
|
11
|
+
import { addCounterToAxios, errors } from '@takaro/util';
|
|
12
|
+
|
|
13
|
+
export class SdtdApiClient {
|
|
14
|
+
private client: Axios;
|
|
15
|
+
|
|
16
|
+
constructor(private config: SdtdConnectionInfo) {
|
|
17
|
+
this.client = axios.create({
|
|
18
|
+
baseURL: this.url,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
addCounterToAxios(this.client, {
|
|
22
|
+
name: 'sdtd_api_requests_total',
|
|
23
|
+
help: 'Total number of requests to the 7D2D API',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
this.client.interceptors.request.use((req) => {
|
|
27
|
+
req.headers['X-SDTD-API-TOKENNAME'] = config.adminUser;
|
|
28
|
+
req.headers['X-SDTD-API-SECRET'] = config.adminToken;
|
|
29
|
+
|
|
30
|
+
return req;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
this.client.interceptors.response.use(
|
|
34
|
+
function (response) {
|
|
35
|
+
return response;
|
|
36
|
+
},
|
|
37
|
+
function (error) {
|
|
38
|
+
// Any status codes that falls outside the range of 2xx cause this function to trigger
|
|
39
|
+
if (error.response) {
|
|
40
|
+
const simplifiedError = new errors.BadRequestError('Axios error', {
|
|
41
|
+
extra: 'A request to the 7D2D server failed',
|
|
42
|
+
status: error.response.status,
|
|
43
|
+
statusText: error.response.statusText,
|
|
44
|
+
url: error.config.url,
|
|
45
|
+
});
|
|
46
|
+
return Promise.reject(simplifiedError);
|
|
47
|
+
} else {
|
|
48
|
+
const simplifiedError = new errors.BadRequestError('Axios error', {
|
|
49
|
+
extra: 'A request to the 7D2D server failed',
|
|
50
|
+
message: error.message,
|
|
51
|
+
url: error.config.url,
|
|
52
|
+
});
|
|
53
|
+
return Promise.reject(simplifiedError);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private get url() {
|
|
60
|
+
return `${this.config.useTls ? 'https' : 'http'}://${this.config.host}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async getStats(): Promise<AxiosResponse<StatsResponse>> {
|
|
64
|
+
return this.client.get('/api/getstats');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async executeConsoleCommand(command: string): Promise<AxiosResponse<CommandResponse>> {
|
|
68
|
+
return this.client.get(`/api/executeconsolecommand?command=${command}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async getPlayersLocation(): Promise<AxiosResponse<Array<PlayerLocation>>> {
|
|
72
|
+
return this.client.get('/api/getplayerslocation');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async getOnlinePlayers(): Promise<AxiosResponse<Array<OnlinePlayerResponse>>> {
|
|
76
|
+
return this.client.get('/api/getplayersonline');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async getPlayerInventory(id: string): Promise<AxiosResponse<InventoryResponse>> {
|
|
80
|
+
return this.client.get(`/api/getplayerinventory?userid=${id}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { IsString } from 'class-validator';
|
|
2
|
+
import { TakaroDTO } from '@takaro/util';
|
|
3
|
+
|
|
4
|
+
export class MockConnectionInfo extends TakaroDTO<MockConnectionInfo> {
|
|
5
|
+
@IsString()
|
|
6
|
+
public host!: string;
|
|
7
|
+
@IsString()
|
|
8
|
+
public name!: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const mockJsonSchema = {
|
|
12
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
13
|
+
title: 'MockConnectionInfo',
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
host: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
},
|
|
19
|
+
name: {
|
|
20
|
+
type: 'string',
|
|
21
|
+
default: 'mock',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
required: ['host'],
|
|
25
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { logger } from '@takaro/util';
|
|
2
|
+
import { MockConnectionInfo } from './connectionInfo.js';
|
|
3
|
+
import { TakaroEmitter } from '../../TakaroEmitter.js';
|
|
4
|
+
import { Socket } from 'socket.io-client';
|
|
5
|
+
import { EventMapping, GameEventTypes } from '@takaro/modules';
|
|
6
|
+
|
|
7
|
+
const log = logger('Mock');
|
|
8
|
+
export class MockEmitter extends TakaroEmitter {
|
|
9
|
+
private scopedListener = this.listener.bind(this);
|
|
10
|
+
|
|
11
|
+
constructor(private config: MockConnectionInfo, private io: Socket) {
|
|
12
|
+
super();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async start(): Promise<void> {
|
|
16
|
+
this.io.onAny(this.scopedListener);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async stop(): Promise<void> {
|
|
20
|
+
this.io.offAny(this.scopedListener);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private async listener(event: GameEventTypes, args: any) {
|
|
24
|
+
if (this.config.name !== args.name) {
|
|
25
|
+
// This event is not for us
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
log.debug(`Transmitting event ${event}`);
|
|
29
|
+
const dto = EventMapping[event];
|
|
30
|
+
|
|
31
|
+
if (dto) {
|
|
32
|
+
this.emit(event, new dto(args));
|
|
33
|
+
} else {
|
|
34
|
+
this.emit(event, args);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { logger, traceableClass } from '@takaro/util';
|
|
2
|
+
import { IGamePlayer, IPosition } from '@takaro/modules';
|
|
3
|
+
import {
|
|
4
|
+
BanDTO,
|
|
5
|
+
CommandOutput,
|
|
6
|
+
IGameServer,
|
|
7
|
+
IItemDTO,
|
|
8
|
+
IMessageOptsDTO,
|
|
9
|
+
IPlayerReferenceDTO,
|
|
10
|
+
TestReachabilityOutputDTO,
|
|
11
|
+
} from '../../interfaces/GameServer.js';
|
|
12
|
+
import { MockEmitter } from './emitter.js';
|
|
13
|
+
import { Socket, io } from 'socket.io-client';
|
|
14
|
+
import assert from 'assert';
|
|
15
|
+
import { MockConnectionInfo } from './connectionInfo.js';
|
|
16
|
+
import { Settings } from '@takaro/apiclient';
|
|
17
|
+
|
|
18
|
+
@traceableClass('game:mock')
|
|
19
|
+
export class Mock implements IGameServer {
|
|
20
|
+
private logger = logger('Mock');
|
|
21
|
+
connectionInfo: MockConnectionInfo;
|
|
22
|
+
emitter: MockEmitter;
|
|
23
|
+
io: Socket;
|
|
24
|
+
|
|
25
|
+
constructor(config: MockConnectionInfo, private settings: Partial<Settings> = {}) {
|
|
26
|
+
this.connectionInfo = config;
|
|
27
|
+
if (!this.connectionInfo.name) this.connectionInfo.name = 'default';
|
|
28
|
+
this.io = io(this.connectionInfo.host, {
|
|
29
|
+
query: {
|
|
30
|
+
name: config.name,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
this.emitter = new MockEmitter(this.connectionInfo, this.io);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
getEventEmitter() {
|
|
37
|
+
return this.emitter;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private async getClient(timeout = 2500): Promise<Socket> {
|
|
41
|
+
if (this.io.connected) {
|
|
42
|
+
return this.io;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return Promise.race([
|
|
46
|
+
new Promise<Socket>((resolve, reject) => {
|
|
47
|
+
const onConnect = () => {
|
|
48
|
+
this.io.off('connect_error', onConnectError);
|
|
49
|
+
resolve(this.io);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const onConnectError = (err: Error) => {
|
|
53
|
+
this.io.off('connect', onConnect);
|
|
54
|
+
reject(err);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
this.io.on('connect', onConnect);
|
|
58
|
+
this.io.on('connect_error', onConnectError);
|
|
59
|
+
}),
|
|
60
|
+
new Promise<Socket>((_, reject) => {
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
this.io.off('connect');
|
|
63
|
+
this.io.off('connect_error');
|
|
64
|
+
reject(new Error(`Connection timed out after ${timeout}ms`));
|
|
65
|
+
}, timeout);
|
|
66
|
+
}),
|
|
67
|
+
]);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private async requestFromServer(event: string, ...args: any[]) {
|
|
71
|
+
const client = await this.getClient();
|
|
72
|
+
return client.timeout(30000).emitWithAck(event, this.connectionInfo.name, ...args);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async getPlayer(player: IPlayerReferenceDTO): Promise<IGamePlayer | null> {
|
|
76
|
+
return this.requestFromServer('getPlayer', player);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async getPlayers(): Promise<IGamePlayer[]> {
|
|
80
|
+
return this.requestFromServer('getPlayers');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async getPlayerLocation(player: IPlayerReferenceDTO): Promise<IPosition | null> {
|
|
84
|
+
return this.requestFromServer('getPlayerLocation', player);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async testReachability(): Promise<TestReachabilityOutputDTO> {
|
|
88
|
+
try {
|
|
89
|
+
const data = await this.requestFromServer('ping');
|
|
90
|
+
assert(data === 'pong');
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (!error || !(error instanceof Error)) {
|
|
93
|
+
return new TestReachabilityOutputDTO({
|
|
94
|
+
connectable: false,
|
|
95
|
+
reason: 'Unknown error',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (error.name === 'AssertionError') {
|
|
100
|
+
return new TestReachabilityOutputDTO({
|
|
101
|
+
connectable: false,
|
|
102
|
+
reason: 'Server responded with invalid data',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return new TestReachabilityOutputDTO({
|
|
107
|
+
connectable: false,
|
|
108
|
+
reason: 'Unable to connect to server',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return new TestReachabilityOutputDTO({
|
|
113
|
+
connectable: true,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async executeConsoleCommand(rawCommand: string): Promise<CommandOutput> {
|
|
118
|
+
return this.requestFromServer('executeConsoleCommand', rawCommand);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async sendMessage(message: string, opts: IMessageOptsDTO) {
|
|
122
|
+
return this.requestFromServer('sendMessage', message, opts);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async teleportPlayer(player: IGamePlayer, x: number, y: number, z: number) {
|
|
126
|
+
return this.requestFromServer('teleportPlayer', player, x, y, z);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async kickPlayer(player: IGamePlayer, reason: string) {
|
|
130
|
+
return this.requestFromServer('kickPlayer', player, reason);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async banPlayer(options: BanDTO) {
|
|
134
|
+
return this.requestFromServer('banPlayer', options);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async unbanPlayer(player: IGamePlayer) {
|
|
138
|
+
return this.requestFromServer('unbanPlayer', player);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async listBans(): Promise<BanDTO[]> {
|
|
142
|
+
return this.requestFromServer('listBans');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async giveItem(player: IPlayerReferenceDTO, item: string, amount: number): Promise<void> {
|
|
146
|
+
return this.requestFromServer('giveItem', player, item, amount);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async listItems(): Promise<IItemDTO[]> {
|
|
150
|
+
return this.requestFromServer('listItems');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async getPlayerInventory(player: IPlayerReferenceDTO): Promise<IItemDTO[]> {
|
|
154
|
+
return this.requestFromServer('getPlayerInventory', player);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { expect, sandbox } from '@takaro/test';
|
|
2
|
+
|
|
3
|
+
import { RustConnectionInfo } from '../connectionInfo.js';
|
|
4
|
+
import { CommandOutput } from '../../../interfaces/GameServer.js';
|
|
5
|
+
import { Rust } from '../index.js';
|
|
6
|
+
import { IGamePlayer } from '@takaro/modules';
|
|
7
|
+
|
|
8
|
+
const MOCK_PLAYER = new IGamePlayer({
|
|
9
|
+
ip: '169.169.169.80',
|
|
10
|
+
name: 'brunkel',
|
|
11
|
+
gameId: '76561198021481871',
|
|
12
|
+
steamId: '76561198021481871',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const testData = {
|
|
16
|
+
oneBan: '\'1 76561198028175941 "" "no reason" -1\n\'',
|
|
17
|
+
oneBanWithTime: '\'1 76561198028175941 "cata" "naughty" 1688173252\n\'',
|
|
18
|
+
twoBans: '1 76561198028175941 "" "stout" 1688173252\n2 76561198035925898 "Emiel" "filthy hacker >:(" -1\n',
|
|
19
|
+
noBans: '',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const mockRustConnectionInfo = new RustConnectionInfo({
|
|
23
|
+
host: 'localhost',
|
|
24
|
+
rconPassword: 'aaa',
|
|
25
|
+
rconPort: '28016',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('rust actions', () => {
|
|
29
|
+
describe('listBans', () => {
|
|
30
|
+
it('Can parse ban list with a single ban', async () => {
|
|
31
|
+
sandbox.stub(Rust.prototype, 'executeConsoleCommand').resolves(
|
|
32
|
+
new CommandOutput({
|
|
33
|
+
rawResult: testData.oneBan,
|
|
34
|
+
success: true,
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const result = await new Rust(await mockRustConnectionInfo).listBans();
|
|
39
|
+
|
|
40
|
+
expect(result).to.be.an('array');
|
|
41
|
+
expect(result).to.have.lengthOf(1);
|
|
42
|
+
expect(result[0].player.gameId).to.equal('76561198028175941');
|
|
43
|
+
expect(result[0].expiresAt).to.equal(null);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('Can parse expiry time', async () => {
|
|
47
|
+
sandbox.stub(Rust.prototype, 'executeConsoleCommand').resolves(
|
|
48
|
+
new CommandOutput({
|
|
49
|
+
rawResult: testData.oneBanWithTime,
|
|
50
|
+
success: true,
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const result = await new Rust(await mockRustConnectionInfo).listBans();
|
|
55
|
+
|
|
56
|
+
expect(result).to.be.an('array');
|
|
57
|
+
expect(result).to.have.lengthOf(1);
|
|
58
|
+
expect(result[0].player.gameId).to.equal('76561198028175941');
|
|
59
|
+
expect(result[0].expiresAt).to.equal('2023-07-01T01:00:52.000Z');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('Can parse ban list with two bans', async () => {
|
|
63
|
+
sandbox.stub(Rust.prototype, 'executeConsoleCommand').resolves(
|
|
64
|
+
new CommandOutput({
|
|
65
|
+
rawResult: testData.twoBans,
|
|
66
|
+
success: true,
|
|
67
|
+
})
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const result = await new Rust(await mockRustConnectionInfo).listBans();
|
|
71
|
+
|
|
72
|
+
expect(result).to.be.an('array');
|
|
73
|
+
expect(result).to.have.lengthOf(2);
|
|
74
|
+
|
|
75
|
+
expect(result[0].player.gameId).to.equal('76561198028175941');
|
|
76
|
+
expect(result[0].expiresAt).to.equal('2023-07-01T01:00:52.000Z');
|
|
77
|
+
|
|
78
|
+
expect(result[1].player.gameId).to.equal('76561198035925898');
|
|
79
|
+
expect(result[1].expiresAt).to.equal(null);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('Can parse ban list with no bans', async () => {
|
|
83
|
+
sandbox.stub(Rust.prototype, 'executeConsoleCommand').resolves(
|
|
84
|
+
new CommandOutput({
|
|
85
|
+
rawResult: testData.noBans,
|
|
86
|
+
success: true,
|
|
87
|
+
})
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const result = await new Rust(await mockRustConnectionInfo).listBans();
|
|
91
|
+
|
|
92
|
+
expect(result).to.be.an('array');
|
|
93
|
+
expect(result).to.have.lengthOf(0);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('[getPlayerLocation]', () => {
|
|
98
|
+
it('Works for a single player', async () => {
|
|
99
|
+
const res = new CommandOutput({
|
|
100
|
+
rawResult: `SteamID DisplayName POS ROT \n${
|
|
101
|
+
(await MOCK_PLAYER).gameId
|
|
102
|
+
} Catalysm (-770.0, 1.0, -1090.7) (1.0, -0.1, -0.1) \n`,
|
|
103
|
+
success: undefined,
|
|
104
|
+
errorMessage: undefined,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const rustInstance = new Rust({} as RustConnectionInfo);
|
|
108
|
+
sandbox.stub(rustInstance, 'executeConsoleCommand').resolves(res);
|
|
109
|
+
|
|
110
|
+
const location = await rustInstance.getPlayerLocation(await MOCK_PLAYER);
|
|
111
|
+
|
|
112
|
+
expect(location).to.deep.equal({
|
|
113
|
+
x: -770.0,
|
|
114
|
+
y: 1.0,
|
|
115
|
+
z: -1090,
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('When output has multiple players', async () => {
|
|
120
|
+
const res = new CommandOutput({
|
|
121
|
+
rawResult: `SteamID DisplayName POS ROT \nfake_steam_id Catalysm (-123.0, 1.0, -1090.7) (1.0, -0.1, -0.1) \n${
|
|
122
|
+
(await MOCK_PLAYER).gameId
|
|
123
|
+
} Player2 (-780.0, 2.0, -1100.7) (1.1, -0.2, -0.2) \n76561198028175943 Player3 (-790.0, 3.0, -1110.7) (1.2, -0.3, -0.3) \n`,
|
|
124
|
+
success: undefined,
|
|
125
|
+
errorMessage: undefined,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const rustInstance = new Rust({} as RustConnectionInfo);
|
|
129
|
+
sandbox.stub(rustInstance, 'executeConsoleCommand').resolves(res);
|
|
130
|
+
|
|
131
|
+
const location = await rustInstance.getPlayerLocation(await MOCK_PLAYER);
|
|
132
|
+
|
|
133
|
+
expect(location).to.deep.equal({
|
|
134
|
+
x: -780.0,
|
|
135
|
+
y: 2.0,
|
|
136
|
+
z: -1100,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { IsString, IsNumber, IsBoolean } from 'class-validator';
|
|
2
|
+
import { TakaroDTO } from '@takaro/util';
|
|
3
|
+
|
|
4
|
+
export class RustConnectionInfo extends TakaroDTO<RustConnectionInfo> {
|
|
5
|
+
@IsString()
|
|
6
|
+
public readonly host!: string;
|
|
7
|
+
@IsNumber()
|
|
8
|
+
public readonly rconPort!: string;
|
|
9
|
+
@IsString()
|
|
10
|
+
public readonly rconPassword!: string;
|
|
11
|
+
@IsBoolean()
|
|
12
|
+
public readonly useTls!: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const rustJsonSchema = {
|
|
16
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
17
|
+
title: 'RustConnectionInfo',
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
host: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
},
|
|
23
|
+
rconPort: {
|
|
24
|
+
type: 'number',
|
|
25
|
+
},
|
|
26
|
+
rconPassword: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
},
|
|
29
|
+
useTls: {
|
|
30
|
+
type: 'boolean',
|
|
31
|
+
default: false,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
required: ['host', 'rconPort', 'rconPassword', 'useTls'],
|
|
35
|
+
};
|
|
@@ -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
|
+
|
|
42
|
+
constructor(private config: RustConnectionInfo) {
|
|
43
|
+
super();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static async getClient(config: RustConnectionInfo) {
|
|
47
|
+
const log = logger('rust:ws');
|
|
48
|
+
|
|
49
|
+
const protocol = config.useTls ? 'wss' : 'ws';
|
|
50
|
+
const client = new WebSocket(`${protocol}://${config.host}:${config.rconPort}/${config.rconPassword}`);
|
|
51
|
+
|
|
52
|
+
log.debug('getClient', {
|
|
53
|
+
host: config.host,
|
|
54
|
+
port: config.rconPort,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return Promise.race([
|
|
58
|
+
new Promise<WebSocket>((resolve, reject) => {
|
|
59
|
+
client?.on('error', (err) => {
|
|
60
|
+
log.warn('getClient', err);
|
|
61
|
+
client?.close();
|
|
62
|
+
return reject(err);
|
|
63
|
+
});
|
|
64
|
+
client?.on('unexpected-response', (req, res) => {
|
|
65
|
+
log.debug('unexpected-response', {
|
|
66
|
+
req,
|
|
67
|
+
res,
|
|
68
|
+
});
|
|
69
|
+
reject(new errors.InternalServerError());
|
|
70
|
+
});
|
|
71
|
+
client?.on('open', () => {
|
|
72
|
+
log.debug('Connection opened');
|
|
73
|
+
if (client) {
|
|
74
|
+
return resolve(client);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}),
|
|
78
|
+
new Promise<WebSocket>((_, reject) => {
|
|
79
|
+
setTimeout(() => reject(new errors.WsTimeOutError('Timeout')), 5000);
|
|
80
|
+
}),
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async start(): Promise<void> {
|
|
85
|
+
this.ws = await RustEmitter.getClient(this.config);
|
|
86
|
+
|
|
87
|
+
this.ws?.on('message', (m: Buffer) => {
|
|
88
|
+
this.listener(m.toString());
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async stop(): Promise<void> {
|
|
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, unknown>): Promise<EventChatMessage> {
|
|
159
|
+
delete data.channel;
|
|
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
|
+
}
|