@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,334 @@
|
|
|
1
|
+
import { logger } from '@takaro/util';
|
|
2
|
+
import EventSource from 'eventsource';
|
|
3
|
+
import { JsonObject } from 'type-fest';
|
|
4
|
+
import {
|
|
5
|
+
ChatChannel,
|
|
6
|
+
EventChatMessage,
|
|
7
|
+
EventEntityKilled,
|
|
8
|
+
EventLogLine,
|
|
9
|
+
EventPlayerConnected,
|
|
10
|
+
EventPlayerDeath,
|
|
11
|
+
EventPlayerDisconnected,
|
|
12
|
+
GameEvents,
|
|
13
|
+
IGamePlayer,
|
|
14
|
+
} from '@takaro/modules';
|
|
15
|
+
import { SdtdConnectionInfo } from './connectionInfo.js';
|
|
16
|
+
import { TakaroEmitter } from '../../TakaroEmitter.js';
|
|
17
|
+
import { SevenDaysToDie } from './index.js';
|
|
18
|
+
import ms from 'ms';
|
|
19
|
+
|
|
20
|
+
interface I7DaysToDieEvent extends JsonObject {
|
|
21
|
+
msg: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 7d2d servers can get really spammy with bugged vehicles, buggy mods, etc.
|
|
26
|
+
* This is a list of messages that we don't want to emit events for.
|
|
27
|
+
*/
|
|
28
|
+
const blackListedMessages = [
|
|
29
|
+
'NullReferenceException',
|
|
30
|
+
'Infinity or NaN floating point numbers appear when calculating the transform matrix for a Collider',
|
|
31
|
+
'IsMovementBlocked',
|
|
32
|
+
'Particle System is trying to spawn on a mesh with zero surface area',
|
|
33
|
+
'AddDecorationAt',
|
|
34
|
+
'EntityFactory CreateEntity: unknown type',
|
|
35
|
+
'DroneManager',
|
|
36
|
+
'VehicleManager',
|
|
37
|
+
'kinematic body',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const EventRegexMap = {
|
|
41
|
+
[GameEvents.PLAYER_CONNECTED]:
|
|
42
|
+
/PlayerSpawnedInWorld \(reason: (JoinMultiplayer|EnterMultiplayer), position: [-\d]+, [-\d]+, [-\d]+\): EntityID=(?<entityId>[-\d]+), PltfmId='(Steam|XBL)_[\w\d]+', CrossId='EOS_[\w\d]+', OwnerID='(Steam|XBL)_\d+', PlayerName='(?<name>.+)'/,
|
|
43
|
+
[GameEvents.PLAYER_DISCONNECTED]: /(Player disconnected: )/,
|
|
44
|
+
[GameEvents.CHAT_MESSAGE]:
|
|
45
|
+
/Chat \(from '(?<platformId>[\w\d-]+)', entity id '(?<entityId>[-\d]+)', to '(?<channel>\w+)'\): ('(?<playerName>.+)':)?(?<message>.+)/,
|
|
46
|
+
[GameEvents.PLAYER_DEATH]:
|
|
47
|
+
/GMSG: Player '(?<name1>.+)' died|\[(?:CSMM_Patrons|PrismaCore)\]playerDied: (?<name2>.+) \((?<steamOrXboxId>.+)\) died @ (?<xCoord>[-\d]+) (?<yCoord>[-\d]+) (?<zCoord>[-\d]+)/,
|
|
48
|
+
[GameEvents.ENTITY_KILLED]:
|
|
49
|
+
/\[(?:CSMM_Patrons|PrismaCore)\]entityKilled: (?<killerName>.+) \((?<steamOrXboxId>.+)\) killed (?<entityType>\w+) (?<entityName>[\w\s\u00C0-\u024F\[\]-]+) with (?<weapon>.+)|Entity (?<entityName2>[\w\s\u00C0-\u024F]+) \d+ killed by (?<killerName2>.+) \d+/,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export class SevenDaysToDieEmitter extends TakaroEmitter {
|
|
53
|
+
private SSERegex = /\d+-\d+-\d+T\d+:\d+:\d+ \d+\.\d+ INF (.+)/;
|
|
54
|
+
private eventSource!: EventSource;
|
|
55
|
+
private logger = logger('7D2D:SSE');
|
|
56
|
+
private sdtd: SevenDaysToDie;
|
|
57
|
+
|
|
58
|
+
private recentMessages: Set<string> = new Set(); // To track recent messages
|
|
59
|
+
private checkInterval: NodeJS.Timeout;
|
|
60
|
+
private lastMessageTimestamp = Date.now();
|
|
61
|
+
private keepAliveTimeout = ms('5minutes');
|
|
62
|
+
private boundListener = (data: MessageEvent) => this.listener(data);
|
|
63
|
+
|
|
64
|
+
constructor(private config: SdtdConnectionInfo) {
|
|
65
|
+
super();
|
|
66
|
+
this.sdtd = new SevenDaysToDie(config, {});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private isModdedFormat(msg: string): boolean {
|
|
70
|
+
return msg.includes('[CSMM_Patrons]') || msg.includes('[PrismaCore]');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get url() {
|
|
74
|
+
if (this.config.useLegacy) {
|
|
75
|
+
return `${this.config.useTls ? 'https' : 'http'}://${this.config.host}/sse/log`;
|
|
76
|
+
}
|
|
77
|
+
return `${this.config.useTls ? 'https' : 'http'}://${this.config.host}/sse/?events=log`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async start(): Promise<void> {
|
|
81
|
+
this.checkInterval = setInterval(() => {
|
|
82
|
+
if (Date.now() - this.lastMessageTimestamp >= this.keepAliveTimeout) {
|
|
83
|
+
this.logger.warn(`No messages received for ${ms(this.keepAliveTimeout, { long: true })}. Reconnecting...`);
|
|
84
|
+
this.lastMessageTimestamp = Date.now();
|
|
85
|
+
this.stop()
|
|
86
|
+
.then(() => this.start())
|
|
87
|
+
.catch((err) => this.logger.error('Error during reconnection', err));
|
|
88
|
+
}
|
|
89
|
+
}, 5000);
|
|
90
|
+
|
|
91
|
+
await Promise.race([
|
|
92
|
+
new Promise<void>((resolve, reject) => {
|
|
93
|
+
this.logger.debug(`Connecting to ${this.config.host}`);
|
|
94
|
+
this.eventSource = new EventSource(this.url, {
|
|
95
|
+
headers: {
|
|
96
|
+
'X-SDTD-API-TOKENNAME': this.config.adminUser,
|
|
97
|
+
'X-SDTD-API-SECRET': this.config.adminToken,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
this.eventSource.addEventListener('logLine', this.boundListener);
|
|
102
|
+
|
|
103
|
+
this.eventSource.onerror = (e) => {
|
|
104
|
+
this.logger.error('Event source error', e);
|
|
105
|
+
return reject(e);
|
|
106
|
+
};
|
|
107
|
+
this.eventSource.onopen = () => {
|
|
108
|
+
this.logger.debug('Opened a SSE channel for server');
|
|
109
|
+
return resolve();
|
|
110
|
+
};
|
|
111
|
+
}),
|
|
112
|
+
new Promise((_resolve, reject) => {
|
|
113
|
+
setTimeout(() => {
|
|
114
|
+
reject(new Error('Timed out'));
|
|
115
|
+
}, 30000);
|
|
116
|
+
}),
|
|
117
|
+
]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async stop(): Promise<void> {
|
|
121
|
+
if (this.checkInterval) {
|
|
122
|
+
clearInterval(this.checkInterval);
|
|
123
|
+
}
|
|
124
|
+
this.eventSource.removeEventListener('logLine', this.boundListener);
|
|
125
|
+
this.eventSource.close();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async parseMessage(logLine: I7DaysToDieEvent) {
|
|
129
|
+
this.logger.silly(`Received message from game server: ${logLine.msg}`);
|
|
130
|
+
if (!logLine.msg || typeof logLine.msg !== 'string') {
|
|
131
|
+
throw new Error('Invalid logLine');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (blackListedMessages.some((msg) => logLine.msg.includes(msg))) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (EventRegexMap[GameEvents.PLAYER_CONNECTED].test(logLine.msg)) {
|
|
139
|
+
const data = await this.handlePlayerConnected(logLine);
|
|
140
|
+
await this.emit(GameEvents.PLAYER_CONNECTED, data);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (EventRegexMap[GameEvents.PLAYER_DISCONNECTED].test(logLine.msg)) {
|
|
144
|
+
const data = await this.handlePlayerDisconnected(logLine);
|
|
145
|
+
await this.emit(GameEvents.PLAYER_DISCONNECTED, data);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (EventRegexMap[GameEvents.CHAT_MESSAGE].test(logLine.msg)) {
|
|
149
|
+
const data = await this.handleChatMessage(logLine);
|
|
150
|
+
if (data) await this.emit(GameEvents.CHAT_MESSAGE, data);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (EventRegexMap[GameEvents.PLAYER_DEATH].test(logLine.msg)) {
|
|
154
|
+
const data = await this.handlePlayerDeath(logLine);
|
|
155
|
+
if (data) await this.emit(GameEvents.PLAYER_DEATH, data);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (EventRegexMap[GameEvents.ENTITY_KILLED].test(logLine.msg)) {
|
|
159
|
+
const data = await this.handleEntityKilled(logLine);
|
|
160
|
+
if (data) await this.emit(GameEvents.ENTITY_KILLED, data);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
await this.emit(
|
|
164
|
+
GameEvents.LOG_LINE,
|
|
165
|
+
new EventLogLine({
|
|
166
|
+
msg: logLine.msg,
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private async handlePlayerConnected(logLine: I7DaysToDieEvent) {
|
|
172
|
+
const nameMatches = /PlayerName='([^']+)/.exec(logLine.msg);
|
|
173
|
+
const platformIdMatches = /PltfmId='(.+)', CrossId=/.exec(logLine.msg);
|
|
174
|
+
const crossIdMatches = /CrossId='(.+)', OwnerID/.exec(logLine.msg);
|
|
175
|
+
|
|
176
|
+
const name = nameMatches ? nameMatches[1] : 'Unknown name';
|
|
177
|
+
const platformId = platformIdMatches ? platformIdMatches[1] : null;
|
|
178
|
+
const epicOnlineServicesId = crossIdMatches ? crossIdMatches[1].replace('EOS_', '') : undefined;
|
|
179
|
+
const gameId = epicOnlineServicesId;
|
|
180
|
+
|
|
181
|
+
const steamId = platformId && platformId.startsWith('Steam_') ? platformId.replace('Steam_', '') : undefined;
|
|
182
|
+
const xboxLiveId = platformId && platformId.startsWith('XBL_') ? platformId.replace('XBL_', '') : undefined;
|
|
183
|
+
|
|
184
|
+
if (!gameId) throw new Error('Could not find gameId');
|
|
185
|
+
|
|
186
|
+
return new EventPlayerConnected({
|
|
187
|
+
msg: logLine.msg,
|
|
188
|
+
player: new IGamePlayer({
|
|
189
|
+
name,
|
|
190
|
+
gameId,
|
|
191
|
+
steamId,
|
|
192
|
+
xboxLiveId,
|
|
193
|
+
epicOnlineServicesId,
|
|
194
|
+
}),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
private async handlePlayerDisconnected(logLine: I7DaysToDieEvent) {
|
|
198
|
+
const nameMatch = /PlayerName='([^']+)/.exec(logLine.msg);
|
|
199
|
+
const platformIdMatches = /PltfmId='(.+)', CrossId=/.exec(logLine.msg);
|
|
200
|
+
const crossIdMatches = /CrossId='(.+)', OwnerID/.exec(logLine.msg);
|
|
201
|
+
|
|
202
|
+
const name = nameMatch ? nameMatch[1] : 'Unknown name';
|
|
203
|
+
const platformId = platformIdMatches ? platformIdMatches[1] : null;
|
|
204
|
+
|
|
205
|
+
const steamId = platformId && platformId.startsWith('Steam_') ? platformId.replace('Steam_', '') : undefined;
|
|
206
|
+
const xboxLiveId = platformId && platformId.startsWith('XBL_') ? platformId.replace('XBL_', '') : undefined;
|
|
207
|
+
const epicOnlineServicesId = crossIdMatches ? crossIdMatches[1].replace('EOS_', '') : undefined;
|
|
208
|
+
const gameId = epicOnlineServicesId;
|
|
209
|
+
|
|
210
|
+
if (!gameId) throw new Error('Could not find gameId');
|
|
211
|
+
|
|
212
|
+
return new EventPlayerDisconnected({
|
|
213
|
+
msg: logLine.msg,
|
|
214
|
+
player: new IGamePlayer({
|
|
215
|
+
name,
|
|
216
|
+
gameId,
|
|
217
|
+
steamId,
|
|
218
|
+
xboxLiveId,
|
|
219
|
+
}),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private async handleChatMessage(logLine: I7DaysToDieEvent) {
|
|
224
|
+
const match = EventRegexMap[GameEvents.CHAT_MESSAGE].exec(logLine.msg);
|
|
225
|
+
if (!match) throw new Error('Could not parse chat message');
|
|
226
|
+
|
|
227
|
+
const { groups } = match;
|
|
228
|
+
if (!groups) throw new Error('Could not parse chat message');
|
|
229
|
+
|
|
230
|
+
const { platformId, name, message, channel } = groups;
|
|
231
|
+
|
|
232
|
+
if (platformId === '-non-player-' && name !== 'Server') {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const trimmedMessage = message.trim();
|
|
237
|
+
if (this.recentMessages.has(trimmedMessage)) {
|
|
238
|
+
return; // Ignore if recently processed
|
|
239
|
+
}
|
|
240
|
+
this.recentMessages.add(trimmedMessage);
|
|
241
|
+
setTimeout(() => this.recentMessages.delete(trimmedMessage), 1000);
|
|
242
|
+
|
|
243
|
+
const xboxLiveId = platformId.startsWith('XBL_') ? platformId.replace('XBL_', '') : undefined;
|
|
244
|
+
const steamId = platformId.startsWith('Steam_') ? platformId.replace('Steam_', '') : undefined;
|
|
245
|
+
|
|
246
|
+
if (steamId || xboxLiveId) {
|
|
247
|
+
const id = steamId || xboxLiveId || '';
|
|
248
|
+
const player = await this.sdtd.steamIdOrXboxToGameId(id);
|
|
249
|
+
|
|
250
|
+
let detectedChannel: ChatChannel = ChatChannel.GLOBAL;
|
|
251
|
+
|
|
252
|
+
switch (channel) {
|
|
253
|
+
case 'Global':
|
|
254
|
+
detectedChannel = ChatChannel.GLOBAL;
|
|
255
|
+
break;
|
|
256
|
+
case 'Party':
|
|
257
|
+
detectedChannel = ChatChannel.TEAM;
|
|
258
|
+
break;
|
|
259
|
+
case 'Friends':
|
|
260
|
+
detectedChannel = ChatChannel.FRIENDS;
|
|
261
|
+
break;
|
|
262
|
+
default:
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (player) {
|
|
267
|
+
return new EventChatMessage({
|
|
268
|
+
player,
|
|
269
|
+
channel: detectedChannel,
|
|
270
|
+
msg: trimmedMessage.trim(),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private async handlePlayerDeath(logLine: I7DaysToDieEvent) {
|
|
277
|
+
if (this.isModdedFormat(logLine.msg) && !this.config.useCPM) return;
|
|
278
|
+
if (logLine.msg.includes('GMSG') && this.config.useCPM) return;
|
|
279
|
+
|
|
280
|
+
const match = EventRegexMap[GameEvents.PLAYER_DEATH].exec(logLine.msg);
|
|
281
|
+
if (!match) throw new Error('Could not parse player death message');
|
|
282
|
+
const { groups } = match;
|
|
283
|
+
if (!groups) throw new Error('Could not parse player death message');
|
|
284
|
+
|
|
285
|
+
const { xCoord, yCoord, zCoord, steamOrXboxId } = groups;
|
|
286
|
+
|
|
287
|
+
const player = await this.sdtd.steamIdOrXboxToGameId(steamOrXboxId);
|
|
288
|
+
|
|
289
|
+
return new EventPlayerDeath({
|
|
290
|
+
msg: logLine.msg,
|
|
291
|
+
player,
|
|
292
|
+
position: {
|
|
293
|
+
x: parseFloat(xCoord),
|
|
294
|
+
y: parseFloat(yCoord),
|
|
295
|
+
z: parseFloat(zCoord),
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private async handleEntityKilled(logLine: I7DaysToDieEvent) {
|
|
301
|
+
if (this.isModdedFormat(logLine.msg) && !this.config.useCPM) return;
|
|
302
|
+
if (logLine.msg.includes('killed by') && this.config.useCPM) return;
|
|
303
|
+
|
|
304
|
+
const match = EventRegexMap[GameEvents.ENTITY_KILLED].exec(logLine.msg);
|
|
305
|
+
if (!match) throw new Error('Could not parse entity killed message');
|
|
306
|
+
const { groups } = match;
|
|
307
|
+
if (!groups) throw new Error('Could not parse entity killed message');
|
|
308
|
+
|
|
309
|
+
// Extracting the relevant details from the named groups
|
|
310
|
+
const { entityName, entityName2, weapon, steamOrXboxId } = groups;
|
|
311
|
+
|
|
312
|
+
const player = await this.sdtd.steamIdOrXboxToGameId(steamOrXboxId);
|
|
313
|
+
|
|
314
|
+
// Constructing the EventEntityKilled object with the parsed data
|
|
315
|
+
return new EventEntityKilled({
|
|
316
|
+
msg: logLine.msg,
|
|
317
|
+
entity: entityName || entityName2,
|
|
318
|
+
player,
|
|
319
|
+
weapon: weapon || undefined, // Assuming that 'weapon' might not be present in some log lines
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async listener(data: MessageEvent) {
|
|
324
|
+
this.lastMessageTimestamp = Date.now();
|
|
325
|
+
|
|
326
|
+
const parsed = JSON.parse(data.data);
|
|
327
|
+
const messageMatch = this.SSERegex.exec(parsed.msg);
|
|
328
|
+
if (messageMatch && messageMatch[1]) {
|
|
329
|
+
parsed.msg = messageMatch[1];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
await this.parseMessage(parsed);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { SevenDaysToDieEmitter } from './emitter.js';
|
|
3
|
+
import { SdtdConnectionInfo } from './connectionInfo.js';
|
|
4
|
+
import { SevenDaysToDie } from './index.js';
|
|
5
|
+
import { expect, sandbox } from '@takaro/test';
|
|
6
|
+
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
7
|
+
import EventSource from 'eventsource';
|
|
8
|
+
|
|
9
|
+
describe('SevenDaysToDieEmitter', () => {
|
|
10
|
+
let emitter: SevenDaysToDieEmitter;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
// Stub the SevenDaysToDie constructor to prevent network calls
|
|
14
|
+
sandbox.stub(SevenDaysToDie.prototype, 'steamIdOrXboxToGameId').resolves(undefined);
|
|
15
|
+
|
|
16
|
+
emitter = new SevenDaysToDieEmitter(
|
|
17
|
+
new SdtdConnectionInfo({
|
|
18
|
+
host: 'localhost:8080',
|
|
19
|
+
adminUser: 'test',
|
|
20
|
+
adminToken: 'test',
|
|
21
|
+
useTls: false,
|
|
22
|
+
useLegacy: false,
|
|
23
|
+
useCPM: false,
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
// Add error handler to prevent unhandled errors
|
|
27
|
+
emitter.on('error', () => {
|
|
28
|
+
// Ignore errors in tests
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(async () => {
|
|
33
|
+
if (emitter) {
|
|
34
|
+
try {
|
|
35
|
+
await emitter.stop();
|
|
36
|
+
} catch {
|
|
37
|
+
// Ignore errors during cleanup
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
sandbox.restore();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it.skip('Does not accumulate listeners on start/stop/start cycle', async () => {
|
|
44
|
+
// Track listeners
|
|
45
|
+
const listeners: Array<(data: any) => void> = [];
|
|
46
|
+
|
|
47
|
+
// Create a mock EventSource
|
|
48
|
+
class MockEventSource {
|
|
49
|
+
onerror: any = null;
|
|
50
|
+
onopen: any = null;
|
|
51
|
+
|
|
52
|
+
constructor(..._args: any[]) {
|
|
53
|
+
//No-op constructor - onopen will be set by the emitter
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
addEventListener(event: string, listener: any) {
|
|
57
|
+
listeners.push(listener);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
removeEventListener(event: string, listener: any) {
|
|
61
|
+
const index = listeners.indexOf(listener);
|
|
62
|
+
if (index > -1) {
|
|
63
|
+
listeners.splice(index, 1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
close() {
|
|
68
|
+
// Mock close
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Stub EventSource constructor
|
|
73
|
+
const OriginalEventSource = EventSource;
|
|
74
|
+
const MockEventSourceClass = class extends MockEventSource {
|
|
75
|
+
constructor(...args: any[]) {
|
|
76
|
+
super(...args);
|
|
77
|
+
// Immediately trigger onopen to resolve the Promise.race in start()
|
|
78
|
+
const triggerOpen = () => {
|
|
79
|
+
if (this.onopen) {
|
|
80
|
+
this.onopen();
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
queueMicrotask(triggerOpen.bind(this));
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
(global as any).EventSource = MockEventSourceClass;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
// First start
|
|
90
|
+
await emitter.start();
|
|
91
|
+
expect(listeners.length).to.equal(1, 'Should have 1 listener after start');
|
|
92
|
+
|
|
93
|
+
// Stop
|
|
94
|
+
await emitter.stop();
|
|
95
|
+
expect(listeners.length).to.equal(0, 'Should have 0 listeners after stop');
|
|
96
|
+
|
|
97
|
+
// Second start (this is where the bug would cause accumulation)
|
|
98
|
+
await emitter.start();
|
|
99
|
+
expect(listeners.length).to.equal(1, 'Should have 1 listener after restart (not 2)');
|
|
100
|
+
|
|
101
|
+
// Third start/stop cycle to be extra sure
|
|
102
|
+
await emitter.stop();
|
|
103
|
+
expect(listeners.length).to.equal(0, 'Should have 0 listeners after second stop');
|
|
104
|
+
|
|
105
|
+
await emitter.start();
|
|
106
|
+
expect(listeners.length).to.equal(1, 'Should have 1 listener after second restart (not 2 or 3)');
|
|
107
|
+
} finally {
|
|
108
|
+
// Restore EventSource
|
|
109
|
+
(global as any).EventSource = OriginalEventSource;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('Uses the same function reference for addEventListener and removeEventListener', () => {
|
|
114
|
+
// Verify boundListener is defined as a class field
|
|
115
|
+
expect((emitter as any).boundListener).to.be.a('function');
|
|
116
|
+
});
|
|
117
|
+
});
|