@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.
Files changed (78) hide show
  1. package/README.md +36 -0
  2. package/dist/TakaroEmitter.d.ts +26 -0
  3. package/dist/TakaroEmitter.js +97 -0
  4. package/dist/TakaroEmitter.js.map +1 -0
  5. package/dist/gameservers/7d2d/apiResponses.d.ts +193 -0
  6. package/dist/gameservers/7d2d/apiResponses.js +2 -0
  7. package/dist/gameservers/7d2d/apiResponses.js.map +1 -0
  8. package/dist/gameservers/7d2d/connectionInfo.d.ts +32 -0
  9. package/dist/gameservers/7d2d/connectionInfo.js +58 -0
  10. package/dist/gameservers/7d2d/connectionInfo.js.map +1 -0
  11. package/dist/gameservers/7d2d/emitter.d.ts +30 -0
  12. package/dist/gameservers/7d2d/emitter.js +261 -0
  13. package/dist/gameservers/7d2d/emitter.js.map +1 -0
  14. package/dist/gameservers/7d2d/index.d.ts +28 -0
  15. package/dist/gameservers/7d2d/index.js +267 -0
  16. package/dist/gameservers/7d2d/index.js.map +1 -0
  17. package/dist/gameservers/7d2d/itemWorker.d.ts +1 -0
  18. package/dist/gameservers/7d2d/itemWorker.js +31 -0
  19. package/dist/gameservers/7d2d/itemWorker.js.map +1 -0
  20. package/dist/gameservers/7d2d/items-7d2d.json +17705 -0
  21. package/dist/gameservers/7d2d/sdtdAPIClient.d.ts +14 -0
  22. package/dist/gameservers/7d2d/sdtdAPIClient.js +60 -0
  23. package/dist/gameservers/7d2d/sdtdAPIClient.js.map +1 -0
  24. package/dist/gameservers/mock/connectionInfo.d.ts +20 -0
  25. package/dist/gameservers/mock/connectionInfo.js +37 -0
  26. package/dist/gameservers/mock/connectionInfo.js.map +1 -0
  27. package/dist/gameservers/mock/emitter.d.ts +12 -0
  28. package/dist/gameservers/mock/emitter.js +33 -0
  29. package/dist/gameservers/mock/emitter.js.map +1 -0
  30. package/dist/gameservers/mock/index.d.ts +31 -0
  31. package/dist/gameservers/mock/index.js +135 -0
  32. package/dist/gameservers/mock/index.js.map +1 -0
  33. package/dist/gameservers/rust/connectionInfo.d.ts +28 -0
  34. package/dist/gameservers/rust/connectionInfo.js +51 -0
  35. package/dist/gameservers/rust/connectionInfo.js.map +1 -0
  36. package/dist/gameservers/rust/emitter.d.ts +30 -0
  37. package/dist/gameservers/rust/emitter.js +160 -0
  38. package/dist/gameservers/rust/emitter.js.map +1 -0
  39. package/dist/gameservers/rust/index.d.ts +29 -0
  40. package/dist/gameservers/rust/index.js +189 -0
  41. package/dist/gameservers/rust/index.js.map +1 -0
  42. package/dist/gameservers/rust/items-rust.json +20771 -0
  43. package/dist/getGame.d.ts +8 -0
  44. package/dist/getGame.js +26 -0
  45. package/dist/getGame.js.map +1 -0
  46. package/dist/interfaces/GameServer.d.ts +57 -0
  47. package/dist/interfaces/GameServer.js +95 -0
  48. package/dist/interfaces/GameServer.js.map +1 -0
  49. package/dist/main.d.ts +9 -0
  50. package/dist/main.js +10 -0
  51. package/dist/main.js.map +1 -0
  52. package/package.json +26 -0
  53. package/src/TakaroEmitter.ts +138 -0
  54. package/src/TakaroEmitter.unit.test.ts +125 -0
  55. package/src/__tests__/gameEventEmitter.test.ts +36 -0
  56. package/src/gameservers/7d2d/__tests__/7d2dActions.unit.test.ts +91 -0
  57. package/src/gameservers/7d2d/__tests__/7d2dEventDetection.unit.test.ts +324 -0
  58. package/src/gameservers/7d2d/apiResponses.ts +214 -0
  59. package/src/gameservers/7d2d/connectionInfo.ts +40 -0
  60. package/src/gameservers/7d2d/emitter.ts +325 -0
  61. package/src/gameservers/7d2d/index.ts +318 -0
  62. package/src/gameservers/7d2d/itemWorker.ts +34 -0
  63. package/src/gameservers/7d2d/items-7d2d.json +17705 -0
  64. package/src/gameservers/7d2d/sdtdAPIClient.ts +82 -0
  65. package/src/gameservers/mock/connectionInfo.ts +25 -0
  66. package/src/gameservers/mock/emitter.ts +37 -0
  67. package/src/gameservers/mock/index.ts +156 -0
  68. package/src/gameservers/rust/__tests__/rustActions.unit.test.ts +140 -0
  69. package/src/gameservers/rust/connectionInfo.ts +35 -0
  70. package/src/gameservers/rust/emitter.ts +198 -0
  71. package/src/gameservers/rust/index.ts +230 -0
  72. package/src/gameservers/rust/items-rust.json +20771 -0
  73. package/src/getGame.ts +32 -0
  74. package/src/interfaces/GameServer.ts +95 -0
  75. package/src/main.ts +14 -0
  76. package/tsconfig.build.json +9 -0
  77. package/tsconfig.json +9 -0
  78. package/typedoc.json +3 -0
@@ -0,0 +1,8 @@
1
+ import { GameServerOutputDTOTypeEnum, Settings } from '@takaro/apiclient';
2
+ import { IGameServer } from './interfaces/GameServer.js';
3
+ export declare enum GAME_SERVER_TYPE {
4
+ 'MOCK' = "MOCK",
5
+ 'SEVENDAYSTODIE' = "SEVENDAYSTODIE",
6
+ 'RUST' = "RUST"
7
+ }
8
+ export declare function getGame(type: GAME_SERVER_TYPE | GameServerOutputDTOTypeEnum, connectionInfo: Record<string, unknown>, settings: Partial<Settings>): Promise<IGameServer>;
@@ -0,0 +1,26 @@
1
+ import { errors } from '@takaro/util';
2
+ import { SdtdConnectionInfo } from './gameservers/7d2d/connectionInfo.js';
3
+ import { SevenDaysToDie } from './gameservers/7d2d/index.js';
4
+ import { MockConnectionInfo } from './gameservers/mock/connectionInfo.js';
5
+ import { Mock } from './gameservers/mock/index.js';
6
+ import { RustConnectionInfo } from './gameservers/rust/connectionInfo.js';
7
+ import { Rust } from './gameservers/rust/index.js';
8
+ export var GAME_SERVER_TYPE;
9
+ (function (GAME_SERVER_TYPE) {
10
+ GAME_SERVER_TYPE["MOCK"] = "MOCK";
11
+ GAME_SERVER_TYPE["SEVENDAYSTODIE"] = "SEVENDAYSTODIE";
12
+ GAME_SERVER_TYPE["RUST"] = "RUST";
13
+ })(GAME_SERVER_TYPE || (GAME_SERVER_TYPE = {}));
14
+ export async function getGame(type, connectionInfo, settings) {
15
+ switch (type) {
16
+ case GAME_SERVER_TYPE.SEVENDAYSTODIE:
17
+ return new SevenDaysToDie(new SdtdConnectionInfo(connectionInfo), settings);
18
+ case GAME_SERVER_TYPE.RUST:
19
+ return new Rust(new RustConnectionInfo(connectionInfo), settings);
20
+ case GAME_SERVER_TYPE.MOCK:
21
+ return new Mock(new MockConnectionInfo(connectionInfo), settings);
22
+ default:
23
+ throw new errors.NotImplementedError();
24
+ }
25
+ }
26
+ //# sourceMappingURL=getGame.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getGame.js","sourceRoot":"","sources":["../src/getGame.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAEtC,OAAO,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAC1E,OAAO,EAAE,IAAI,EAAE,MAAM,6BAA6B,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAC1E,OAAO,EAAE,IAAI,EAAE,MAAM,6BAA6B,CAAC;AAGnD,MAAM,CAAN,IAAY,gBAIX;AAJD,WAAY,gBAAgB;IAC1B,iCAAe,CAAA;IACf,qDAAmC,CAAA;IACnC,iCAAe,CAAA;AACjB,CAAC,EAJW,gBAAgB,KAAhB,gBAAgB,QAI3B;AAED,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,IAAoD,EACpD,cAAuC,EACvC,QAA2B;IAE3B,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,gBAAgB,CAAC,cAAc;YAClC,OAAO,IAAI,cAAc,CAAC,IAAI,kBAAkB,CAAC,cAAc,CAAC,EAAE,QAAQ,CAAC,CAAC;QAC9E,KAAK,gBAAgB,CAAC,IAAI;YACxB,OAAO,IAAI,IAAI,CAAC,IAAI,kBAAkB,CAAC,cAAc,CAAC,EAAE,QAAQ,CAAC,CAAC;QACpE,KAAK,gBAAgB,CAAC,IAAI;YACxB,OAAO,IAAI,IAAI,CAAC,IAAI,kBAAkB,CAAC,cAAc,CAAC,EAAE,QAAQ,CAAC,CAAC;QACpE;YACE,MAAM,IAAI,MAAM,CAAC,mBAAmB,EAAE,CAAC;IAC3C,CAAC;AACH,CAAC"}
@@ -0,0 +1,57 @@
1
+ import { TakaroEmitter } from '../TakaroEmitter.js';
2
+ import { IGamePlayer, IPosition } from '@takaro/modules';
3
+ import { TakaroDTO } from '@takaro/util';
4
+ export declare class CommandOutput extends TakaroDTO<CommandOutput> {
5
+ rawResult: string;
6
+ success: boolean;
7
+ errorMessage?: string;
8
+ }
9
+ export declare class TestReachabilityOutputDTO extends TakaroDTO<TestReachabilityOutputDTO> {
10
+ connectable: boolean;
11
+ reason?: string;
12
+ }
13
+ /**
14
+ * This is used whenever we want to target a specific player
15
+ * We only allow a subset of IGamePlayer here because to work across gameservers we need to be generic
16
+ * Eg, if we allow users to reference players by Steam ID, that wont work for all gameservers. Not all gameservers have Steam integration
17
+ */
18
+ export declare class IPlayerReferenceDTO extends TakaroDTO<IPlayerReferenceDTO> {
19
+ gameId: string;
20
+ }
21
+ export declare class IItemDTO extends TakaroDTO<IItemDTO> {
22
+ name: string;
23
+ code: string;
24
+ description: string;
25
+ amount?: number;
26
+ }
27
+ export declare class IMessageOptsDTO extends TakaroDTO<IMessageOptsDTO> {
28
+ /** When specified, will send a DM to this player instead of a global message */
29
+ recipient?: IPlayerReferenceDTO;
30
+ }
31
+ export declare class BanDTO extends TakaroDTO<BanDTO> {
32
+ player: IPlayerReferenceDTO;
33
+ reason: string;
34
+ expiresAt: string | null;
35
+ }
36
+ export interface IGameServer {
37
+ connectionInfo: unknown;
38
+ getEventEmitter(): TakaroEmitter;
39
+ getPlayer(player: IPlayerReferenceDTO): Promise<IGamePlayer | null>;
40
+ getPlayers(): Promise<IGamePlayer[]>;
41
+ getPlayerLocation(player: IPlayerReferenceDTO): Promise<IPosition | null>;
42
+ getPlayerInventory(player: IPlayerReferenceDTO): Promise<IItemDTO[]>;
43
+ giveItem(player: IPlayerReferenceDTO, item: string, amount: number): Promise<void>;
44
+ listItems(): Promise<IItemDTO[]>;
45
+ executeConsoleCommand(rawCommand: string): Promise<CommandOutput>;
46
+ sendMessage(message: string, opts: IMessageOptsDTO): Promise<void>;
47
+ teleportPlayer(player: IPlayerReferenceDTO, x: number, y: number, z: number): Promise<void>;
48
+ /**
49
+ * Try and connect to the gameserver
50
+ * If anything goes wrong, this function will report a detailed reason
51
+ */
52
+ testReachability(): Promise<TestReachabilityOutputDTO>;
53
+ kickPlayer(player: IPlayerReferenceDTO, reason: string): Promise<void>;
54
+ banPlayer(options: BanDTO): Promise<void>;
55
+ unbanPlayer(player: IPlayerReferenceDTO): Promise<void>;
56
+ listBans(): Promise<BanDTO[]>;
57
+ }
@@ -0,0 +1,95 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { TakaroDTO } from '@takaro/util';
11
+ import { IsBoolean, IsISO8601, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator';
12
+ import { Type } from 'class-transformer';
13
+ export class CommandOutput extends TakaroDTO {
14
+ }
15
+ __decorate([
16
+ IsString(),
17
+ __metadata("design:type", String)
18
+ ], CommandOutput.prototype, "rawResult", void 0);
19
+ __decorate([
20
+ IsBoolean(),
21
+ __metadata("design:type", Boolean)
22
+ ], CommandOutput.prototype, "success", void 0);
23
+ __decorate([
24
+ IsOptional(),
25
+ IsString(),
26
+ __metadata("design:type", String)
27
+ ], CommandOutput.prototype, "errorMessage", void 0);
28
+ export class TestReachabilityOutputDTO extends TakaroDTO {
29
+ }
30
+ __decorate([
31
+ IsBoolean(),
32
+ __metadata("design:type", Boolean)
33
+ ], TestReachabilityOutputDTO.prototype, "connectable", void 0);
34
+ __decorate([
35
+ IsString(),
36
+ IsOptional(),
37
+ __metadata("design:type", String)
38
+ ], TestReachabilityOutputDTO.prototype, "reason", void 0);
39
+ /**
40
+ * This is used whenever we want to target a specific player
41
+ * We only allow a subset of IGamePlayer here because to work across gameservers we need to be generic
42
+ * Eg, if we allow users to reference players by Steam ID, that wont work for all gameservers. Not all gameservers have Steam integration
43
+ */
44
+ export class IPlayerReferenceDTO extends TakaroDTO {
45
+ }
46
+ __decorate([
47
+ IsString(),
48
+ __metadata("design:type", String)
49
+ ], IPlayerReferenceDTO.prototype, "gameId", void 0);
50
+ export class IItemDTO extends TakaroDTO {
51
+ }
52
+ __decorate([
53
+ IsString(),
54
+ __metadata("design:type", String)
55
+ ], IItemDTO.prototype, "name", void 0);
56
+ __decorate([
57
+ IsString(),
58
+ __metadata("design:type", String)
59
+ ], IItemDTO.prototype, "code", void 0);
60
+ __decorate([
61
+ IsString(),
62
+ IsOptional(),
63
+ __metadata("design:type", String)
64
+ ], IItemDTO.prototype, "description", void 0);
65
+ __decorate([
66
+ IsNumber(),
67
+ IsOptional(),
68
+ __metadata("design:type", Number)
69
+ ], IItemDTO.prototype, "amount", void 0);
70
+ export class IMessageOptsDTO extends TakaroDTO {
71
+ }
72
+ __decorate([
73
+ Type(() => IPlayerReferenceDTO),
74
+ ValidateNested()
75
+ /** When specified, will send a DM to this player instead of a global message */
76
+ ,
77
+ __metadata("design:type", IPlayerReferenceDTO)
78
+ ], IMessageOptsDTO.prototype, "recipient", void 0);
79
+ export class BanDTO extends TakaroDTO {
80
+ }
81
+ __decorate([
82
+ Type(() => IPlayerReferenceDTO),
83
+ ValidateNested(),
84
+ __metadata("design:type", IPlayerReferenceDTO)
85
+ ], BanDTO.prototype, "player", void 0);
86
+ __decorate([
87
+ IsString(),
88
+ __metadata("design:type", String)
89
+ ], BanDTO.prototype, "reason", void 0);
90
+ __decorate([
91
+ IsISO8601(),
92
+ IsOptional(),
93
+ __metadata("design:type", Object)
94
+ ], BanDTO.prototype, "expiresAt", void 0);
95
+ //# sourceMappingURL=GameServer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GameServer.js","sourceRoot":"","sources":["../../src/interfaces/GameServer.ts"],"names":[],"mappings":";;;;;;;;;AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACvG,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAEzC,MAAM,OAAO,aAAc,SAAQ,SAAwB;CAQ1D;AANC;IADC,QAAQ,EAAE;;gDACO;AAElB;IADC,SAAS,EAAE;;8CACK;AAGjB;IAFC,UAAU,EAAE;IACZ,QAAQ,EAAE;;mDACW;AAExB,MAAM,OAAO,yBAA0B,SAAQ,SAAoC;CAOlF;AALC;IADC,SAAS,EAAE;;8DACS;AAIrB;IAFC,QAAQ,EAAE;IACV,UAAU,EAAE;;yDACG;AAGlB;;;;GAIG;AACH,MAAM,OAAO,mBAAoB,SAAQ,SAA8B;CAGtE;AADC;IADC,QAAQ,EAAE;;mDACI;AAGjB,MAAM,OAAO,QAAS,SAAQ,SAAmB;CAWhD;AATC;IADC,QAAQ,EAAE;;sCACE;AAEb;IADC,QAAQ,EAAE;;sCACE;AAGb;IAFC,QAAQ,EAAE;IACV,UAAU,EAAE;;6CACO;AAGpB;IAFC,QAAQ,EAAE;IACV,UAAU,EAAE;;wCACG;AAGlB,MAAM,OAAO,eAAgB,SAAQ,SAA0B;CAK9D;AADC;IAHC,IAAI,CAAC,GAAG,EAAE,CAAC,mBAAmB,CAAC;IAC/B,cAAc,EAAE;IACjB,gFAAgF;;8BACpE,mBAAmB;kDAAC;AAGlC,MAAM,OAAO,MAAO,SAAQ,SAAiB;CAW5C;AARC;IAFC,IAAI,CAAC,GAAG,EAAE,CAAC,mBAAmB,CAAC;IAC/B,cAAc,EAAE;8BACT,mBAAmB;sCAAC;AAG5B;IADC,QAAQ,EAAE;;sCACI;AAIf;IAFC,SAAS,EAAE;IACX,UAAU,EAAE;;yCACY"}
package/dist/main.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ export * from './interfaces/GameServer.js';
2
+ export { TakaroEmitter } from './TakaroEmitter.js';
3
+ export { Mock } from './gameservers/mock/index.js';
4
+ export { MockConnectionInfo, mockJsonSchema } from './gameservers/mock/connectionInfo.js';
5
+ export { SevenDaysToDie } from './gameservers/7d2d/index.js';
6
+ export { SdtdConnectionInfo, sdtdJsonSchema } from './gameservers/7d2d/connectionInfo.js';
7
+ export { Rust } from './gameservers/rust/index.js';
8
+ export { RustConnectionInfo, rustJsonSchema } from './gameservers/rust/connectionInfo.js';
9
+ export { getGame, GAME_SERVER_TYPE } from './getGame.js';
package/dist/main.js ADDED
@@ -0,0 +1,10 @@
1
+ export * from './interfaces/GameServer.js';
2
+ export { TakaroEmitter } from './TakaroEmitter.js';
3
+ export { Mock } from './gameservers/mock/index.js';
4
+ export { MockConnectionInfo, mockJsonSchema } from './gameservers/mock/connectionInfo.js';
5
+ export { SevenDaysToDie } from './gameservers/7d2d/index.js';
6
+ export { SdtdConnectionInfo, sdtdJsonSchema } from './gameservers/7d2d/connectionInfo.js';
7
+ export { Rust } from './gameservers/rust/index.js';
8
+ export { RustConnectionInfo, rustJsonSchema } from './gameservers/rust/connectionInfo.js';
9
+ export { getGame, GAME_SERVER_TYPE } from './getGame.js';
10
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAC;AAE3C,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,OAAO,EAAE,IAAI,EAAE,MAAM,6BAA6B,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,sCAAsC,CAAC;AAE1F,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,sCAAsC,CAAC;AAE1F,OAAO,EAAE,IAAI,EAAE,MAAM,6BAA6B,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,sCAAsC,CAAC;AAE1F,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC"}
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@takaro/gameserver",
3
+ "version": "0.0.1",
4
+ "description": "Handles abstraction between Takaro and game servers",
5
+ "main": "dist/main.js",
6
+ "types": "dist/main.d.ts",
7
+ "type": "module",
8
+ "scripts": {
9
+ "start:dev": "tsc --watch --preserveWatchOutput -p ./tsconfig.build.json",
10
+ "build": "tsc -p ./tsconfig.build.json",
11
+ "test": "npm run test:unit --if-present && npm run test:integration --if-present",
12
+ "test:unit": "mocha --config ../../.mocharc.js src/**/*.unit.test.ts",
13
+ "test:integration": "echo 'No tests (yet :))'"
14
+ },
15
+ "keywords": [],
16
+ "author": "",
17
+ "license": "ISC",
18
+ "dependencies": {
19
+ "@takaro/modules": "0.0.1",
20
+ "eventsource": "^2.0.2"
21
+ },
22
+ "devDependencies": {
23
+ "@takaro/test": "0.0.1",
24
+ "@types/eventsource": "^1.1.9"
25
+ }
26
+ }
@@ -0,0 +1,138 @@
1
+ import {
2
+ BaseEvent,
3
+ EventChatMessage,
4
+ EventEntityKilled,
5
+ EventLogLine,
6
+ EventPlayerConnected,
7
+ EventPlayerDeath,
8
+ EventPlayerDisconnected,
9
+ GameEvents,
10
+ } from '@takaro/modules';
11
+ import { errors, isTakaroDTO, logger } from '@takaro/util';
12
+ import { isPromise } from 'util/types';
13
+
14
+ const log = logger('TakaroEmitter');
15
+
16
+ /**
17
+ * Maps event types to their listener function signatures
18
+ * This allows our EventEmitter to be strongly typed
19
+ */
20
+ export interface IEventMap {
21
+ [GameEvents.LOG_LINE]: (log: EventLogLine) => Promise<void>;
22
+ [GameEvents.PLAYER_CONNECTED]: (player: EventPlayerConnected) => Promise<void>;
23
+ [GameEvents.PLAYER_DISCONNECTED]: (player: EventPlayerDisconnected) => Promise<void>;
24
+ [GameEvents.CHAT_MESSAGE]: (chatMessage: EventChatMessage) => Promise<void>;
25
+ [GameEvents.PLAYER_DEATH]: (playerDeath: EventPlayerDeath) => Promise<void>;
26
+ [GameEvents.ENTITY_KILLED]: (entityKilled: EventEntityKilled) => Promise<void>;
27
+ error: (error: errors.TakaroError | Error) => Promise<void> | void;
28
+ }
29
+
30
+ export abstract class TakaroEmitter {
31
+ private listenerMap: Map<keyof IEventMap, IEventMap[keyof IEventMap][]> = new Map();
32
+
33
+ abstract stop(): Promise<void>;
34
+ abstract start(): Promise<void>;
35
+
36
+ constructor() {
37
+ return getErrorProxyHandler(this);
38
+ }
39
+
40
+ async emit<E extends keyof IEventMap>(event: E, data: BaseEvent<unknown> | Error) {
41
+ try {
42
+ // No listeners are attached, return early
43
+ if (!this.listenerMap.has(event)) return;
44
+
45
+ // Validate the data, it is user-input after all :)
46
+ if (isTakaroDTO(data)) {
47
+ if (!data.timestamp) data.timestamp = new Date().toISOString();
48
+ // await data.validate();
49
+ }
50
+
51
+ const listeners = this.listenerMap.get(event);
52
+
53
+ if (listeners) {
54
+ for (const listener of listeners) {
55
+ // We know this is okay because our listener map always corresponds to the right event
56
+ // This is implicit in our implementation and checked in the tests
57
+ // @ts-expect-error Can't get the types quite right :(
58
+ await listener(data);
59
+ }
60
+ }
61
+ } catch (error) {
62
+ this.emit('error', error as Error);
63
+ }
64
+ }
65
+
66
+ on<E extends keyof IEventMap>(event: E, listener: IEventMap[E]): this {
67
+ this.listenerMap.set(event, [listener]);
68
+ return this;
69
+ }
70
+
71
+ off<E extends keyof IEventMap>(event: E, listener: IEventMap[E]): this {
72
+ const listeners = this.listenerMap.get(event);
73
+
74
+ if (listeners) {
75
+ this.listenerMap.set(
76
+ event,
77
+ listeners.filter((l) => l !== listener)
78
+ );
79
+ }
80
+
81
+ return this;
82
+ }
83
+
84
+ hasErrorListener() {
85
+ return this.listenerMap.has('error');
86
+ }
87
+ }
88
+
89
+ export function getErrorProxyHandler<T extends TakaroEmitter>(emitter: T) {
90
+ const errorProxyHandler: ProxyHandler<T> = {
91
+ construct(target: any, argArray) {
92
+ return Reflect.construct(target, argArray, TakaroEmitter);
93
+ },
94
+
95
+ set: function (obj, prop: keyof TakaroEmitter, value) {
96
+ obj[prop] = value;
97
+ return true;
98
+ },
99
+
100
+ get(target, prop: keyof TakaroEmitter) {
101
+ return async (...[one, two]: any[]) => {
102
+ try {
103
+ // Check if callable function
104
+ if (typeof target[prop] === 'function') {
105
+ return await target[prop](one, two);
106
+ // Or if its a Promise, await it
107
+ } else if (isPromise(target[prop])) {
108
+ return await target[prop];
109
+ } else {
110
+ // Otherwise, return the value
111
+ return target[prop];
112
+ }
113
+ } catch (error) {
114
+ if (!target.hasErrorListener()) {
115
+ log.error('Unhandled error', error);
116
+ const err = new Error(
117
+ 'Unhandled error in TakaroEmitter, attach a listener to the "error" event to handle this'
118
+ );
119
+ Error.captureStackTrace(err);
120
+ throw err;
121
+ }
122
+
123
+ if (error instanceof errors.TakaroError) {
124
+ await target.emit('error', error);
125
+ } else if (error instanceof Error) {
126
+ await target.emit('error', error);
127
+ }
128
+
129
+ if (!TakaroEmitter.prototype.hasOwnProperty(prop)) {
130
+ return Promise.reject(error);
131
+ }
132
+ }
133
+ };
134
+ },
135
+ };
136
+
137
+ return new Proxy(emitter, errorProxyHandler);
138
+ }
@@ -0,0 +1,125 @@
1
+ import { TakaroEmitter } from './TakaroEmitter.js';
2
+ import { expect, sandbox } from '@takaro/test';
3
+ import { EventLogLine, GameEvents } from '@takaro/modules';
4
+
5
+ class ExtendedTakaroEmitter extends TakaroEmitter {
6
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
7
+ async start() {}
8
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
9
+ async stop() {}
10
+
11
+ async foo() {
12
+ throw new Error('testing error');
13
+ }
14
+ }
15
+
16
+ describe('TakaroEmitter', () => {
17
+ it('Can listen for events', async () => {
18
+ const emitter = new ExtendedTakaroEmitter();
19
+ const spy = sandbox.spy();
20
+
21
+ emitter.on(GameEvents.LOG_LINE, spy);
22
+
23
+ await emitter.emit(
24
+ GameEvents.LOG_LINE,
25
+ new EventLogLine({
26
+ msg: 'test',
27
+ })
28
+ );
29
+
30
+ expect(spy).to.have.been.calledOnce;
31
+
32
+ await emitter.emit(
33
+ GameEvents.LOG_LINE,
34
+ new EventLogLine({
35
+ msg: 'test',
36
+ })
37
+ );
38
+
39
+ expect(spy).to.have.been.calledTwice;
40
+ });
41
+
42
+ it('Can remove a listener, which causes further events to not be received', async () => {
43
+ const emitter = new ExtendedTakaroEmitter();
44
+ const spy = sandbox.spy();
45
+
46
+ emitter.on(GameEvents.LOG_LINE, spy);
47
+
48
+ await emitter.emit(
49
+ GameEvents.LOG_LINE,
50
+ new EventLogLine({
51
+ msg: 'test',
52
+ })
53
+ );
54
+
55
+ expect(spy).to.have.been.calledOnce;
56
+
57
+ emitter.off(GameEvents.LOG_LINE, spy);
58
+
59
+ await emitter.emit(
60
+ GameEvents.LOG_LINE,
61
+ new EventLogLine({
62
+ msg: 'test',
63
+ })
64
+ );
65
+
66
+ expect(spy).to.have.been.calledOnce;
67
+ });
68
+
69
+ it('Errors happening inside extended class do not interrupt flow of events', async () => {
70
+ const emitter = new ExtendedTakaroEmitter();
71
+ const spy = sandbox.spy();
72
+ const errorSpy = sandbox.spy();
73
+
74
+ emitter.on(GameEvents.LOG_LINE, spy);
75
+ emitter.on('error', errorSpy);
76
+
77
+ await expect(emitter.foo()).to.eventually.be.rejectedWith('testing error');
78
+
79
+ expect(errorSpy).to.have.been.calledOnce;
80
+
81
+ await emitter.emit(
82
+ GameEvents.LOG_LINE,
83
+ new EventLogLine({
84
+ msg: 'test',
85
+ })
86
+ );
87
+
88
+ expect(spy).to.have.been.calledOnce;
89
+ });
90
+
91
+ xit('Validates data on emitting', async () => {
92
+ const emitter = new ExtendedTakaroEmitter();
93
+ const spy = sandbox.spy();
94
+ const errorSpy = sandbox.spy();
95
+
96
+ emitter.on(GameEvents.LOG_LINE, spy);
97
+ emitter.on('error', errorSpy);
98
+
99
+ await emitter.emit(
100
+ GameEvents.LOG_LINE,
101
+ new EventLogLine({
102
+ msg: 'test',
103
+ // @ts-expect-error testing validation, our types accurately detect this is invalid
104
+ unknownProperty: 'this should trip validation',
105
+ })
106
+ );
107
+
108
+ expect(errorSpy).to.have.been.calledOnce;
109
+ expect(errorSpy.getCall(0).args[0].message).to.match(
110
+ /property unknownProperty has failed the following constraints: whitelistValidation/
111
+ );
112
+ });
113
+
114
+ it('Throws when an error occurs and no listeners are attached to the "error" event', async () => {
115
+ const emitter = new ExtendedTakaroEmitter();
116
+ const errorSpy = sandbox.spy();
117
+
118
+ await expect(emitter.foo()).to.eventually.be.rejectedWith('Unhandled error in TakaroEmitter');
119
+
120
+ emitter.on('error', errorSpy);
121
+
122
+ await expect(emitter.foo()).to.eventually.be.rejectedWith('testing error');
123
+ expect(errorSpy).to.have.been.calledOnce;
124
+ });
125
+ });
@@ -0,0 +1,36 @@
1
+ import { expect, sandbox } from '@takaro/test';
2
+ import { MockConnectionInfo } from '../gameservers/mock/connectionInfo.js';
3
+ import { Mock } from '../gameservers/mock/index.js';
4
+ import { GameEvents } from '@takaro/modules';
5
+
6
+ describe('GameEventEmitter', () => {
7
+ /**
8
+ * This test doesn't really do much interesting runtime validation
9
+ * It exists to ensure that the event emitter is safely typed
10
+ * We use @ts-expect-error so that if the compiler fails to mark these as errors, we'll know instantly
11
+ */
12
+ it('Has a typed event emitter', async () => {
13
+ const gameServer = new Mock(new MockConnectionInfo({}));
14
+ const emitter = await gameServer.getEventEmitter();
15
+
16
+ const listenerSpy = sandbox.spy();
17
+
18
+ emitter.on(GameEvents.PLAYER_CONNECTED, async (e) => {
19
+ expect(() => e.player.name).to.throw(
20
+ // Eslint and prettier disagree on how to format this
21
+ // And I cba fixing it for this specific instance :)
22
+ // eslint-disable-next-line quotes
23
+ "Cannot read properties of undefined (reading 'name')"
24
+ );
25
+ });
26
+ emitter.on(GameEvents.PLAYER_CONNECTED, listenerSpy);
27
+
28
+ // @ts-expect-error Should use the enum here
29
+ emitter.on('non-existent-event', listenerSpy);
30
+
31
+ // But the raw string will also work in runtime when ignoring the compilation error
32
+ emitter.on('player-connected', listenerSpy);
33
+
34
+ expect(listenerSpy).to.have.been.calledTwice;
35
+ });
36
+ });
@@ -0,0 +1,91 @@
1
+ import { expect, sandbox } from '@takaro/test';
2
+ import { SevenDaysToDie } from '../index.js';
3
+ import { CommandOutput } from '../../../interfaces/GameServer.js';
4
+ import { SdtdConnectionInfo } from '../connectionInfo.js';
5
+
6
+ const testData = {
7
+ oneBan:
8
+ 'Ban list entries:\n Banned until - UserID (name) - Reason\n 2023-06-29 19:39:56 - EOS_00028a9b73bb45b2b74e8f22cda7d225 (-unknown-) - Totally valid testing reason :)\n',
9
+ twoBans:
10
+ 'Ban list entries:\n Banned until - UserID (name) - Reason\n 2028-06-29 17:49:45 - EOS_0002e0daea3b493fa146ce6d06e79a57 (-unknown-) - test\n 2028-06-29 19:19:40 - EOS_00028a9b73bb45b2b74e8f22cda7d225 (-unknown-) - test\n',
11
+ noBans: 'Ban list entries:\n Banned until - UserID (name) - Reason\n',
12
+ oneBanWithDisplayName:
13
+ 'Ban list entries:\n Banned until - UserID (name) - Reason\n 2023-06-29 19:40:59 - EOS_00028a9b73bb45b2b74e8f22cda7d225 (testing display name) - Totally valid testing reason :)\n',
14
+ };
15
+
16
+ const mockSdtdConnectionInfo = new SdtdConnectionInfo({
17
+ adminToken: 'aaa',
18
+ adminUser: 'aaa',
19
+ useTls: false,
20
+ host: 'localhost',
21
+ });
22
+
23
+ describe('7d2d Actions', () => {
24
+ describe('listBans', () => {
25
+ it('Can parse ban list with a single ban', async () => {
26
+ sandbox.stub(SevenDaysToDie.prototype, 'executeConsoleCommand').resolves(
27
+ new CommandOutput({
28
+ rawResult: testData.oneBan,
29
+ success: true,
30
+ })
31
+ );
32
+
33
+ const result = await new SevenDaysToDie(await mockSdtdConnectionInfo).listBans();
34
+
35
+ expect(result).to.be.an('array');
36
+ expect(result).to.have.lengthOf(1);
37
+ expect(result[0].player.gameId).to.equal('EOS_00028a9b73bb45b2b74e8f22cda7d225');
38
+ expect(result[0].expiresAt).to.equal('2023-06-29T19:39:56.000Z');
39
+ });
40
+
41
+ it('Can parse ban list with two bans', async () => {
42
+ sandbox.stub(SevenDaysToDie.prototype, 'executeConsoleCommand').resolves(
43
+ new CommandOutput({
44
+ rawResult: testData.twoBans,
45
+ success: true,
46
+ })
47
+ );
48
+
49
+ const result = await new SevenDaysToDie(await mockSdtdConnectionInfo).listBans();
50
+
51
+ expect(result).to.be.an('array');
52
+ expect(result).to.have.lengthOf(2);
53
+
54
+ expect(result[0].player.gameId).to.equal('EOS_0002e0daea3b493fa146ce6d06e79a57');
55
+ expect(result[0].expiresAt).to.equal('2028-06-29T17:49:45.000Z');
56
+
57
+ expect(result[1].player.gameId).to.equal('EOS_00028a9b73bb45b2b74e8f22cda7d225');
58
+ expect(result[1].expiresAt).to.equal('2028-06-29T19:19:40.000Z');
59
+ });
60
+
61
+ it('Can parse ban list with no bans', async () => {
62
+ sandbox.stub(SevenDaysToDie.prototype, 'executeConsoleCommand').resolves(
63
+ new CommandOutput({
64
+ rawResult: testData.noBans,
65
+ success: true,
66
+ })
67
+ );
68
+
69
+ const result = await new SevenDaysToDie(await mockSdtdConnectionInfo).listBans();
70
+
71
+ expect(result).to.be.an('array');
72
+ expect(result).to.have.lengthOf(0);
73
+ });
74
+
75
+ it('Can parse ban list with a single ban with a display name', async () => {
76
+ sandbox.stub(SevenDaysToDie.prototype, 'executeConsoleCommand').resolves(
77
+ new CommandOutput({
78
+ rawResult: testData.oneBanWithDisplayName,
79
+ success: true,
80
+ })
81
+ );
82
+
83
+ const result = await new SevenDaysToDie(await mockSdtdConnectionInfo).listBans();
84
+
85
+ expect(result).to.be.an('array');
86
+ expect(result).to.have.lengthOf(1);
87
+ expect(result[0].player.gameId).to.equal('EOS_00028a9b73bb45b2b74e8f22cda7d225');
88
+ expect(result[0].expiresAt).to.equal('2023-06-29T19:40:59.000Z');
89
+ });
90
+ });
91
+ });