@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,82 @@
1
+ import { Axios, AxiosResponse } from 'axios';
2
+ import { SdtdConnectionInfo } from './connectionInfo.js';
3
+ import {
4
+ CommandResponse,
5
+ InventoryResponse,
6
+ OnlinePlayerResponse,
7
+ PlayerLocation,
8
+ StatsResponse,
9
+ } from './apiResponses.js';
10
+ import { addCounterToAxios, createAxios, logger } from '@takaro/util';
11
+ import { MapInfoDTO } from '../../main.js';
12
+
13
+ export class SdtdApiClient {
14
+ private client: Axios;
15
+ private log = logger('7d2d:api');
16
+
17
+ constructor(private config: SdtdConnectionInfo) {
18
+ this.client = createAxios(
19
+ {
20
+ baseURL: this.url,
21
+ },
22
+ { logger: this.log },
23
+ );
24
+
25
+ addCounterToAxios(this.client, {
26
+ name: 'sdtd_api_requests_total',
27
+ help: 'Total number of requests to the 7D2D API',
28
+ });
29
+
30
+ this.client.interceptors.request.use((req) => {
31
+ req.headers['X-SDTD-API-TOKENNAME'] = config.adminUser;
32
+ req.headers['X-SDTD-API-SECRET'] = config.adminToken;
33
+
34
+ return req;
35
+ });
36
+ }
37
+
38
+ private get url() {
39
+ return `${this.config.useTls ? 'https' : 'http'}://${this.config.host}`;
40
+ }
41
+
42
+ async getStats(): Promise<AxiosResponse<StatsResponse>> {
43
+ return this.client.get('/api/getstats');
44
+ }
45
+
46
+ async executeConsoleCommand(command: string): Promise<AxiosResponse<CommandResponse>> {
47
+ return this.client.get(`/api/executeconsolecommand?command=${command}`);
48
+ }
49
+
50
+ async getPlayersLocation(): Promise<AxiosResponse<Array<PlayerLocation>>> {
51
+ return this.client.get('/api/getplayerslocation');
52
+ }
53
+
54
+ async getOnlinePlayers(): Promise<AxiosResponse<Array<OnlinePlayerResponse>>> {
55
+ return this.client.get('/api/getplayersonline');
56
+ }
57
+
58
+ async getPlayerInventory(id: string): Promise<AxiosResponse<InventoryResponse>> {
59
+ return this.client.get(`/api/getplayerinventory?userid=${id}`);
60
+ }
61
+
62
+ async getMapInfo(): Promise<MapInfoDTO> {
63
+ const res = await this.client.get('/api/map/config');
64
+
65
+ return new MapInfoDTO({
66
+ enabled: res.data.data.enabled,
67
+ mapBlockSize: res.data.data.mapBlockSize,
68
+ maxZoom: res.data.data.maxZoom,
69
+ mapSizeX: res.data.data.mapSize.x,
70
+ mapSizeY: res.data.data.mapSize.y,
71
+ mapSizeZ: res.data.data.mapSize.z,
72
+ });
73
+ }
74
+
75
+ async getMapTile(x: number, y: number, z: number): Promise<string> {
76
+ const res = await this.client.get(`/map/${z}/${x}/${y}.png?t=${Date.now() / 1000}`, {
77
+ responseType: 'arraybuffer',
78
+ });
79
+ // Convert to b64
80
+ return Buffer.from(res.data, 'binary').toString('base64');
81
+ }
82
+ }
@@ -0,0 +1,19 @@
1
+ import { IsString } from 'class-validator';
2
+ import { TakaroDTO } from '@takaro/util';
3
+
4
+ export class GenericConnectionInfo extends TakaroDTO<GenericConnectionInfo> {
5
+ @IsString()
6
+ public identityToken: string;
7
+ }
8
+
9
+ export const genericJsonSchema = {
10
+ $schema: 'http://json-schema.org/draft-07/schema#',
11
+ title: 'GenericConnectionInfo',
12
+ type: 'object',
13
+ properties: {
14
+ identityToken: {
15
+ type: 'string',
16
+ },
17
+ },
18
+ required: ['identityToken'],
19
+ };
@@ -0,0 +1,73 @@
1
+ import { AxiosInstance, isAxiosError } from 'axios';
2
+ import { config } from '../../config.js';
3
+ import { errors, logger, createAxios } from '@takaro/util';
4
+
5
+ function getConnectorClient() {
6
+ const log = logger('client:connector');
7
+
8
+ const connectorClient = createAxios(
9
+ {
10
+ baseURL: config.get('connector.host'),
11
+ headers: {
12
+ 'Content-Type': 'application/json',
13
+ 'User-Agent': 'Takaro',
14
+ },
15
+ },
16
+ { logger: log },
17
+ );
18
+
19
+ // Add custom interceptor for operation logging
20
+ connectorClient.interceptors.request.use((request) => {
21
+ if (request.data?.operation) {
22
+ log.debug(`Operation: ${request.data.operation}`);
23
+ }
24
+ return request;
25
+ });
26
+
27
+ return connectorClient;
28
+ }
29
+
30
+ export class TakaroConnector {
31
+ private client: AxiosInstance;
32
+ constructor() {
33
+ this.client = getConnectorClient();
34
+ }
35
+
36
+ async requestFromServer(id: string, operation: string, data: string): Promise<any> {
37
+ try {
38
+ const res = await this.client.post(`/gameserver/${id}/request`, { operation, data });
39
+ return res.data.data;
40
+ } catch (error) {
41
+ if (isAxiosError(error)) {
42
+ if (error.response?.data.meta.error.code === 'ValidationError') {
43
+ throw new errors.BadRequestError(
44
+ 'The gameserver responded with bad data, please verify that the mod is up to date.',
45
+ );
46
+ }
47
+
48
+ if (error.response?.status) {
49
+ if (error.response?.status >= 400 && error.response?.status < 500) {
50
+ throw new errors.BadRequestError(error.response?.data.meta.error.message);
51
+ }
52
+ }
53
+ }
54
+ throw error;
55
+ }
56
+ }
57
+
58
+ async resetConnection(id: string): Promise<any> {
59
+ try {
60
+ await this.client.post(`/gameserver/${id}/reset`);
61
+ return;
62
+ } catch (error) {
63
+ if (isAxiosError(error)) {
64
+ if (error.response?.data.meta.error.code === 'ValidationError') {
65
+ throw new errors.BadRequestError(
66
+ 'The gameserver responded with bad data, please verify that the mod is up to date.',
67
+ );
68
+ }
69
+ }
70
+ throw error;
71
+ }
72
+ }
73
+ }
@@ -0,0 +1,36 @@
1
+ import { logger } from '@takaro/util';
2
+ import { GenericConnectionInfo } from './connectionInfo.js';
3
+ import { TakaroEmitter } from '../../TakaroEmitter.js';
4
+ import { EventMapping, GameEventTypes } from '@takaro/modules';
5
+
6
+ const log = logger('Generic');
7
+ export class GenericEmitter extends TakaroEmitter {
8
+ private scopedListener = this.privateListener.bind(this);
9
+
10
+ constructor(private config: GenericConnectionInfo) {
11
+ super();
12
+ }
13
+
14
+ async start(): Promise<void> {
15
+ // No-op, generic connectors initiate the connection
16
+ }
17
+
18
+ async stop(): Promise<void> {
19
+ // No-op, generic connectors initiate the connection
20
+ }
21
+
22
+ get listener() {
23
+ return this.scopedListener;
24
+ }
25
+
26
+ private async privateListener(event: GameEventTypes, args: any) {
27
+ log.debug(`Transmitting event ${event}`);
28
+ const dto = EventMapping[event];
29
+
30
+ if (dto) {
31
+ this.emit(event, new dto(args));
32
+ } else {
33
+ this.emit(event, args);
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,193 @@
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
+ ILocationDTO,
10
+ IMessageOptsDTO,
11
+ IPlayerReferenceDTO,
12
+ MapInfoDTO,
13
+ TestReachabilityOutputDTO,
14
+ } from '../../interfaces/GameServer.js';
15
+ import { Settings } from '@takaro/apiclient';
16
+ import { TakaroConnector } from './connectorClient.js';
17
+ import { GenericConnectionInfo } from './connectionInfo.js';
18
+ import { JsonObject } from 'type-fest';
19
+ import { GenericEmitter } from './emitter.js';
20
+
21
+ @traceableClass('game:generic')
22
+ export class Generic implements IGameServer {
23
+ private logger = logger('Generic');
24
+ connectionInfo: GenericConnectionInfo;
25
+ emitter: GenericEmitter;
26
+ private takaroConnector = new TakaroConnector();
27
+
28
+ constructor(
29
+ config: GenericConnectionInfo,
30
+ private settings: Partial<Settings> = {},
31
+ private gameServerId: string,
32
+ ) {
33
+ this.connectionInfo = config;
34
+ this.emitter = new GenericEmitter(config);
35
+ }
36
+
37
+ getEventEmitter() {
38
+ return this.emitter;
39
+ }
40
+
41
+ private async requestFromServer(operation: string, data?: JsonObject) {
42
+ if (!data) data = {};
43
+ const resp = await this.takaroConnector.requestFromServer(this.gameServerId, operation, JSON.stringify(data));
44
+
45
+ if (resp && resp.error) {
46
+ throw new errors.BadRequestError(`Error from server: ${resp.error}`);
47
+ }
48
+
49
+ return resp;
50
+ }
51
+
52
+ async getPlayer(rawPlayer: IPlayerReferenceDTO): Promise<IGamePlayer | null> {
53
+ const player = new IPlayerReferenceDTO({ gameId: rawPlayer.gameId });
54
+ const res = await this.requestFromServer('getPlayer', player.toJSON());
55
+ if (!res) return null;
56
+ const dto = new IGamePlayer(res);
57
+ await dto.validate();
58
+ return dto;
59
+ }
60
+
61
+ async getPlayers(): Promise<IGamePlayer[]> {
62
+ const res = await this.requestFromServer('getPlayers');
63
+ if (!res)
64
+ throw new errors.ValidationError('Nothing returned from server, is the server responding the right data?');
65
+ const dto: IGamePlayer[] = res.map((p: any) => new IGamePlayer(p));
66
+ await Promise.all(dto.map((p) => p.validate()));
67
+ return dto;
68
+ }
69
+
70
+ async getPlayerLocation(rawPlayer: IPlayerReferenceDTO): Promise<IPosition | null> {
71
+ const player = new IPlayerReferenceDTO({ gameId: rawPlayer.gameId });
72
+ const res = await this.requestFromServer('getPlayerLocation', player.toJSON());
73
+ if (!res) return null;
74
+ const dto = new IPosition(res);
75
+ await dto.validate();
76
+ return dto;
77
+ }
78
+
79
+ async testReachability(): Promise<TestReachabilityOutputDTO> {
80
+ try {
81
+ const response = await this.takaroConnector.requestFromServer(this.gameServerId, 'testReachability', '{}');
82
+ const dto = new TestReachabilityOutputDTO(response);
83
+ await dto.validate();
84
+ return dto;
85
+ } catch (error) {
86
+ return new TestReachabilityOutputDTO({
87
+ connectable: false,
88
+ reason: error instanceof Error ? error.message : 'Unknown error occurred',
89
+ });
90
+ }
91
+ }
92
+
93
+ async executeConsoleCommand(rawCommand: string): Promise<CommandOutput> {
94
+ const res = await this.requestFromServer('executeConsoleCommand', { command: rawCommand });
95
+ if (!res)
96
+ throw new errors.ValidationError('Nothing returned from server, is the server responding the right data?');
97
+ const dto = new CommandOutput(res);
98
+ await dto.validate();
99
+ return dto;
100
+ }
101
+
102
+ async sendMessage(message: string, opts?: IMessageOptsDTO) {
103
+ if (!opts) opts = new IMessageOptsDTO();
104
+ await this.requestFromServer('sendMessage', {
105
+ message,
106
+ opts: opts.toJSON(),
107
+ });
108
+ }
109
+
110
+ async teleportPlayer(player: IGamePlayer, x: number, y: number, z: number, dimension?: string) {
111
+ await this.requestFromServer('teleportPlayer', { player: player.toJSON(), x, y, z, dimension });
112
+ }
113
+
114
+ async kickPlayer(player: IGamePlayer, reason: string) {
115
+ await this.requestFromServer('kickPlayer', { player: player.toJSON(), reason });
116
+ }
117
+
118
+ async banPlayer(options: BanDTO) {
119
+ await this.requestFromServer('banPlayer', options.toJSON());
120
+ }
121
+
122
+ async unbanPlayer(player: IGamePlayer) {
123
+ await this.requestFromServer('unbanPlayer', player.toJSON());
124
+ }
125
+
126
+ async listBans(): Promise<BanDTO[]> {
127
+ const res = await this.requestFromServer('listBans');
128
+ if (!res)
129
+ throw new errors.ValidationError('Nothing returned from server, is the server responding the right data?');
130
+ const dto: BanDTO[] = res.map((p: any) => new BanDTO(p));
131
+ await Promise.all(dto.map((p) => p.validate()));
132
+ return dto;
133
+ }
134
+
135
+ async giveItem(rawPlayer: IPlayerReferenceDTO, item: string, amount: number, quality: string): Promise<void> {
136
+ await this.requestFromServer('giveItem', {
137
+ player: { gameId: rawPlayer.gameId },
138
+ item,
139
+ amount,
140
+ quality,
141
+ });
142
+ }
143
+
144
+ async listItems(): Promise<IItemDTO[]> {
145
+ const res = await this.requestFromServer('listItems');
146
+ if (!res)
147
+ throw new errors.ValidationError('Nothing returned from server, is the server responding the right data?');
148
+ const dto: IItemDTO[] = res.map((p: any) => new IItemDTO(p));
149
+ await Promise.all(dto.map((p) => p.validate()));
150
+ return dto;
151
+ }
152
+
153
+ async getPlayerInventory(rawPlayer: IPlayerReferenceDTO): Promise<IItemDTO[]> {
154
+ const player = new IPlayerReferenceDTO({ gameId: rawPlayer.gameId });
155
+ const res = await this.requestFromServer('getPlayerInventory', player.toJSON());
156
+ if (!res)
157
+ throw new errors.ValidationError('Nothing returned from server, is the server responding the right data?');
158
+ const dto: IItemDTO[] = res.map((p: any) => new IItemDTO(p));
159
+ await Promise.all(dto.map((p) => p.validate()));
160
+ return dto;
161
+ }
162
+
163
+ async shutdown(): Promise<void> {
164
+ await this.requestFromServer('shutdown');
165
+ }
166
+
167
+ async getMapInfo(): Promise<MapInfoDTO> {
168
+ const res = await this.requestFromServer('getMapInfo');
169
+ if (!res)
170
+ throw new errors.ValidationError('Nothing returned from server, is the server responding the right data?');
171
+ const dto = new MapInfoDTO(res);
172
+ await dto.validate();
173
+ return dto;
174
+ }
175
+
176
+ async getMapTile(_x: number, _y: number, _z: number): Promise<string> {
177
+ const res = await this.requestFromServer('getMapTile', { x: _x, y: _y, z: _z });
178
+ return res;
179
+ }
180
+
181
+ async listEntities(): Promise<IEntityDTO[]> {
182
+ const res = await this.requestFromServer('listEntities');
183
+ if (!res)
184
+ throw new errors.ValidationError('Nothing returned from server, is the server responding the right data?');
185
+ const dto: IEntityDTO[] = res.map((p: any) => new IEntityDTO(p));
186
+ await Promise.all(dto.map((p) => p.validate()));
187
+ return dto;
188
+ }
189
+
190
+ async listLocations(): Promise<ILocationDTO[]> {
191
+ throw new errors.NotImplementedError();
192
+ }
193
+ }
@@ -0,0 +1,141 @@
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
+ import { describe, it } from 'node:test';
8
+
9
+ const MOCK_PLAYER = new IGamePlayer({
10
+ ip: '169.169.169.80',
11
+ name: 'brunkel',
12
+ gameId: '76561198021481871',
13
+ steamId: '76561198021481871',
14
+ });
15
+
16
+ const testData = {
17
+ oneBan: '\'1 76561198028175941 "" "no reason" -1\n\'',
18
+ oneBanWithTime: '\'1 76561198028175941 "cata" "naughty" 1688173252\n\'',
19
+ twoBans: '1 76561198028175941 "" "stout" 1688173252\n2 76561198035925898 "Emiel" "filthy hacker >:(" -1\n',
20
+ noBans: '',
21
+ };
22
+
23
+ const mockRustConnectionInfo = new RustConnectionInfo({
24
+ host: 'localhost',
25
+ rconPassword: 'aaa',
26
+ rconPort: '28016',
27
+ });
28
+
29
+ describe('rust actions', () => {
30
+ describe('listBans', () => {
31
+ it('Can parse ban list with a single ban', async () => {
32
+ sandbox.stub(Rust.prototype, 'executeConsoleCommand').resolves(
33
+ new CommandOutput({
34
+ rawResult: testData.oneBan,
35
+ success: true,
36
+ }),
37
+ );
38
+
39
+ const result = await new Rust(await mockRustConnectionInfo).listBans();
40
+
41
+ expect(result).to.be.an('array');
42
+ expect(result).to.have.lengthOf(1);
43
+ expect(result[0].player.gameId).to.equal('76561198028175941');
44
+ expect(result[0].expiresAt).to.equal(null);
45
+ });
46
+
47
+ it('Can parse expiry time', async () => {
48
+ sandbox.stub(Rust.prototype, 'executeConsoleCommand').resolves(
49
+ new CommandOutput({
50
+ rawResult: testData.oneBanWithTime,
51
+ success: true,
52
+ }),
53
+ );
54
+
55
+ const result = await new Rust(await mockRustConnectionInfo).listBans();
56
+
57
+ expect(result).to.be.an('array');
58
+ expect(result).to.have.lengthOf(1);
59
+ expect(result[0].player.gameId).to.equal('76561198028175941');
60
+ expect(result[0].expiresAt).to.equal('2023-07-01T01:00:52.000Z');
61
+ });
62
+
63
+ it('Can parse ban list with two bans', async () => {
64
+ sandbox.stub(Rust.prototype, 'executeConsoleCommand').resolves(
65
+ new CommandOutput({
66
+ rawResult: testData.twoBans,
67
+ success: true,
68
+ }),
69
+ );
70
+
71
+ const result = await new Rust(await mockRustConnectionInfo).listBans();
72
+
73
+ expect(result).to.be.an('array');
74
+ expect(result).to.have.lengthOf(2);
75
+
76
+ expect(result[0].player.gameId).to.equal('76561198028175941');
77
+ expect(result[0].expiresAt).to.equal('2023-07-01T01:00:52.000Z');
78
+
79
+ expect(result[1].player.gameId).to.equal('76561198035925898');
80
+ expect(result[1].expiresAt).to.equal(null);
81
+ });
82
+
83
+ it('Can parse ban list with no bans', async () => {
84
+ sandbox.stub(Rust.prototype, 'executeConsoleCommand').resolves(
85
+ new CommandOutput({
86
+ rawResult: testData.noBans,
87
+ success: true,
88
+ }),
89
+ );
90
+
91
+ const result = await new Rust(await mockRustConnectionInfo).listBans();
92
+
93
+ expect(result).to.be.an('array');
94
+ expect(result).to.have.lengthOf(0);
95
+ });
96
+ });
97
+
98
+ describe('[getPlayerLocation]', () => {
99
+ it('Works for a single player', async () => {
100
+ const res = new CommandOutput({
101
+ rawResult: `SteamID DisplayName POS ROT \n${
102
+ (await MOCK_PLAYER).gameId
103
+ } Catalysm (-770.0, 1.0, -1090.7) (1.0, -0.1, -0.1) \n`,
104
+ success: undefined,
105
+ errorMessage: undefined,
106
+ });
107
+
108
+ const rustInstance = new Rust({} as RustConnectionInfo);
109
+ sandbox.stub(rustInstance, 'executeConsoleCommand').resolves(res);
110
+
111
+ const location = await rustInstance.getPlayerLocation(await MOCK_PLAYER);
112
+
113
+ expect(location).to.deep.equal({
114
+ x: -770.0,
115
+ y: 1.0,
116
+ z: -1090,
117
+ });
118
+ });
119
+
120
+ it('When output has multiple players', async () => {
121
+ const res = new CommandOutput({
122
+ rawResult: `SteamID DisplayName POS ROT \nfake_steam_id Catalysm (-123.0, 1.0, -1090.7) (1.0, -0.1, -0.1) \n${
123
+ (await MOCK_PLAYER).gameId
124
+ } 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`,
125
+ success: undefined,
126
+ errorMessage: undefined,
127
+ });
128
+
129
+ const rustInstance = new Rust({} as RustConnectionInfo);
130
+ sandbox.stub(rustInstance, 'executeConsoleCommand').resolves(res);
131
+
132
+ const location = await rustInstance.getPlayerLocation(await MOCK_PLAYER);
133
+
134
+ expect(location).to.deep.equal({
135
+ x: -780.0,
136
+ y: 2.0,
137
+ z: -1100,
138
+ });
139
+ });
140
+ });
141
+ });
@@ -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
+ };