@pilaf/backends 1.0.2 → 1.2.0

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.
@@ -0,0 +1,512 @@
1
+ /**
2
+ * MinecraftLogParser - Parser for Minecraft server logs
3
+ *
4
+ * Parses raw Minecraft server log lines into structured events.
5
+ * Supports Minecraft versions 1.19, 1.20, 1.21+.
6
+ *
7
+ * Responsibilities (MECE):
8
+ * - ONLY: Parse raw log lines into structured events
9
+ * - NOT: Collect logs (LogCollector's responsibility)
10
+ * - NOT: Correlate events (CorrelationStrategy's responsibility)
11
+ *
12
+ * Event Categories:
13
+ * - Entity: Player join/leave/death/spawn
14
+ * - Movement: Teleport events
15
+ * - Command: Server command execution
16
+ * - World: Time/weather/save changes
17
+ * - Status: Server lifecycle events
18
+ * - Plugin: Custom plugin events (extensible)
19
+ *
20
+ * Usage Example:
21
+ * const parser = new MinecraftLogParser();
22
+ * const event = parser.parse('[12:34:56] [Server thread/INFO]: TestPlayer joined');
23
+ * // Returns event object with type, data, raw, timestamp, thread, level
24
+ */
25
+
26
+ const { LogParser } = require('../core/LogParser.js');
27
+ const { PatternRegistry } = require('./PatternRegistry.js');
28
+ const { UnknownPatternError } = require('../errors/index.js');
29
+
30
+ class MinecraftLogParser extends LogParser {
31
+ /**
32
+ * Create a MinecraftLogParser
33
+ * @param {Object} options - Parser options
34
+ * @param {boolean} [options.includeMetadata=true] - Include timestamp/thread/level in output
35
+ * @param {boolean} [options.strictMode=false] - Throw on unknown patterns (vs return null)
36
+ */
37
+ constructor(options = {}) {
38
+ super();
39
+
40
+ /**
41
+ * Parser options
42
+ * @private
43
+ * @type {Object}
44
+ */
45
+ this._options = {
46
+ includeMetadata: options?.includeMetadata !== false,
47
+ strictMode: options?.strictMode || false
48
+ };
49
+
50
+ /**
51
+ * Pattern registry for all log patterns
52
+ * @private
53
+ * @type {PatternRegistry}
54
+ */
55
+ this._registry = new PatternRegistry();
56
+
57
+ /**
58
+ * Initialize all pattern categories
59
+ * @private
60
+ */
61
+ this._initializePatterns();
62
+ }
63
+
64
+ /**
65
+ * Parse a log line into a structured event
66
+ *
67
+ * @param {string} line - Raw log line
68
+ * @returns {Object|null} - Parsed event or null if no pattern matches
69
+ * @property {string} type - Event type (e.g., 'entity.join', 'movement.teleport')
70
+ * @property {Object} data - Event data (extracted from pattern)
71
+ * @property {string} raw - Original log line
72
+ * @property {string} [timestamp] - Time from log line (if includeMetadata)
73
+ * @property {string} [thread] - Thread name (if includeMetadata)
74
+ * @property {string} [level] - Log level (if includeMetadata)
75
+ * @throws {UnknownPatternError} If strictMode is true and pattern doesn't match
76
+ */
77
+ parse(line) {
78
+ if (!line || typeof line !== 'string') {
79
+ return null;
80
+ }
81
+
82
+ // Trim the line
83
+ const trimmed = line.trim();
84
+ if (!trimmed) {
85
+ return null;
86
+ }
87
+
88
+ // Try to match against all patterns in priority order
89
+ const match = this._registry.match(trimmed);
90
+
91
+ if (!match) {
92
+ if (this._options.strictMode) {
93
+ throw new UnknownPatternError(trimmed);
94
+ }
95
+ // Even when no pattern matches, return metadata if requested
96
+ if (this._options.includeMetadata) {
97
+ const metadata = this._extractMetadata(trimmed);
98
+ return {
99
+ type: null,
100
+ data: null,
101
+ raw: trimmed,
102
+ ...metadata
103
+ };
104
+ }
105
+ return null;
106
+ }
107
+
108
+ // Build result object
109
+ const result = {
110
+ type: match.name,
111
+ data: match.data,
112
+ raw: trimmed
113
+ };
114
+
115
+ // Add metadata if requested
116
+ if (this._options.includeMetadata) {
117
+ const metadata = this._extractMetadata(trimmed);
118
+ Object.assign(result, metadata);
119
+ }
120
+
121
+ return result;
122
+ }
123
+
124
+ /**
125
+ * Extract metadata (timestamp, thread, level) from log line
126
+ *
127
+ * @private
128
+ * @param {string} line - Log line
129
+ * @returns {Object} - Extracted metadata
130
+ */
131
+ _extractMetadata(line) {
132
+ // Minecraft log format: [HH:MM:SS] [Thread/LEVEL]: Message
133
+ // Or: [HH:MM:SS] [Thread/INFO]: [uuid] Message (for some versions)
134
+
135
+ const metadataRegex = /^\[(\d{2}:\d{2}:\d{2})\]\s+\[([^\]]+)\/(INFO|WARN|ERROR|DEBUG)\]:/;
136
+ const match = line.match(metadataRegex);
137
+
138
+ if (match) {
139
+ return {
140
+ timestamp: match[1],
141
+ thread: match[2],
142
+ level: match[3]
143
+ };
144
+ }
145
+
146
+ // Fallback: try to extract just timestamp
147
+ const timestampRegex = /^\[(\d{2}:\d{2}:\d{2})\]/;
148
+ const timestampMatch = line.match(timestampRegex);
149
+
150
+ if (timestampMatch) {
151
+ return {
152
+ timestamp: timestampMatch[1],
153
+ thread: null,
154
+ level: null
155
+ };
156
+ }
157
+
158
+ return {
159
+ timestamp: null,
160
+ thread: null,
161
+ level: null
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Initialize all Minecraft log patterns
167
+ *
168
+ * Patterns are registered in priority order (higher priority = tested first).
169
+ * Priority ranges:
170
+ * - 10+: Most specific patterns (teleport with coordinates)
171
+ * - 8-9: Specific sub-categories (death types)
172
+ * - 5-7: Player actions and commands
173
+ * - 3-4: World state changes
174
+ * - 1-2: Status messages
175
+ * - 0: Plugin/custom patterns (fallback)
176
+ *
177
+ * @private
178
+ */
179
+ _initializePatterns() {
180
+ // =========================================================================
181
+ // PRIORITY 10: Movement Events (Most Specific)
182
+ // =========================================================================
183
+
184
+ this._registry.addPattern(
185
+ 'movement.teleport',
186
+ /Teleported\s+(\w+)\s+from\s+([\d.-]+),\s*([\d.-]+),\s*([\d.-]+)\s+to\s+([\d.-]+),\s*([\d.-]+),\s*([\d.-]+)/,
187
+ (match) => ({
188
+ player: match[1],
189
+ from: { x: parseFloat(match[2]), y: parseFloat(match[3]), z: parseFloat(match[4]) },
190
+ to: { x: parseFloat(match[5]), y: parseFloat(match[6]), z: parseFloat(match[7]) }
191
+ }),
192
+ 10
193
+ );
194
+
195
+ // =========================================================================
196
+ // PRIORITY 8: Entity Death Events
197
+ // =========================================================================
198
+
199
+ // Death by entity
200
+ this._registry.addPattern(
201
+ 'entity.death.slain',
202
+ /(\w+)\s+was\s+slain\s+by\s+(.+)/,
203
+ (match) => ({
204
+ player: match[1],
205
+ killer: match[2],
206
+ cause: 'entity_attack'
207
+ }),
208
+ 8
209
+ );
210
+
211
+ // Death by fall
212
+ this._registry.addPattern(
213
+ 'entity.death.fall',
214
+ /(\w+)\s+fell\s+from\s+a\s+high\s+place/,
215
+ (match) => ({
216
+ player: match[1],
217
+ cause: 'fall'
218
+ }),
219
+ 8
220
+ );
221
+
222
+ // Death by fire
223
+ this._registry.addPattern(
224
+ 'entity.death.fire',
225
+ /(\w+)\s+(?:burned\s+to\s+death|was\s+burnt\s+to\s+a\s+crisp)/,
226
+ (match) => ({
227
+ player: match[1],
228
+ cause: 'fire'
229
+ }),
230
+ 8
231
+ );
232
+
233
+ // Death by lava
234
+ this._registry.addPattern(
235
+ 'entity.death.lava',
236
+ /(\w+)\s+(?:tried\s+to\s+swim\s+in\s+lava|was\s+killed\s+by\s+(?:Magma|Lava)(?:\s+Block)?)/,
237
+ (match) => ({
238
+ player: match[1],
239
+ cause: 'lava'
240
+ }),
241
+ 8
242
+ );
243
+
244
+ // Death by drowning
245
+ this._registry.addPattern(
246
+ 'entity.death.drown',
247
+ /(\w+)\s+drowned/,
248
+ (match) => ({
249
+ player: match[1],
250
+ cause: 'drown'
251
+ }),
252
+ 8
253
+ );
254
+
255
+ // Death by sprinting into wall
256
+ this._registry.addPattern(
257
+ 'entity.death.sprint',
258
+ /(\w+)\s+splatted\s+against\s+a\s+wall/,
259
+ (match) => ({
260
+ player: match[1],
261
+ cause: 'sprint_into_wall'
262
+ }),
263
+ 8
264
+ );
265
+
266
+ // Generic death
267
+ this._registry.addPattern(
268
+ 'entity.death.generic',
269
+ /(\w+)\s+died/,
270
+ (match) => ({
271
+ player: match[1],
272
+ cause: 'unknown'
273
+ }),
274
+ 8
275
+ );
276
+
277
+ // =========================================================================
278
+ // PRIORITY 6: Player Actions (join, leave, command)
279
+ // =========================================================================
280
+
281
+ // Player joined
282
+ this._registry.addPattern(
283
+ 'entity.join',
284
+ /([^\s]+)\s+joined\s+the\s+game/,
285
+ (match) => ({
286
+ player: match[1]
287
+ }),
288
+ 6
289
+ );
290
+
291
+ // Player left
292
+ this._registry.addPattern(
293
+ 'entity.leave',
294
+ /(\w+)\s+(?:lost\s+connection:\s*(.+)|left\s+the\s+game)/,
295
+ (match) => ({
296
+ player: match[1],
297
+ reason: match[2]?.trim() || 'Left the game'
298
+ }),
299
+ 6
300
+ );
301
+
302
+ // Player issued command
303
+ this._registry.addPattern(
304
+ 'command.issued',
305
+ /(\w+)\s+issued\s+server\s+command:\s*(.+)/,
306
+ (match) => ({
307
+ player: match[1],
308
+ command: match[2]?.trim()
309
+ }),
310
+ 6
311
+ );
312
+
313
+ // Player UUID/spawn (modern versions)
314
+ this._registry.addPattern(
315
+ 'entity.spawn',
316
+ /UUID\s+of\s+player\s+(\w+)\s+is\s+([a-f0-9-]{36})/,
317
+ (match) => ({
318
+ player: match[1],
319
+ uuid: match[2]
320
+ }),
321
+ 6
322
+ );
323
+
324
+ // =========================================================================
325
+ // PRIORITY 4: World Events (time, weather, save)
326
+ // =========================================================================
327
+
328
+ // Time change
329
+ this._registry.addPattern(
330
+ 'world.time',
331
+ /Changing\s+the\s+time\s+to\s+(\d+)/,
332
+ (match) => ({
333
+ time: parseInt(match[1], 10)
334
+ }),
335
+ 4
336
+ );
337
+
338
+ // Weather change
339
+ this._registry.addPattern(
340
+ 'world.weather',
341
+ /Changing\s+the\s+weather\s+to\s+(\w+)/,
342
+ (match) => ({
343
+ weather: match[1]
344
+ }),
345
+ 4
346
+ );
347
+
348
+ // Difficulty change
349
+ this._registry.addPattern(
350
+ 'world.difficulty',
351
+ /Changing\s+the\s+difficulty\s+to\s+(\w+)/,
352
+ (match) => ({
353
+ difficulty: match[1]
354
+ }),
355
+ 4
356
+ );
357
+
358
+ // Game mode change
359
+ this._registry.addPattern(
360
+ 'world.gamemode',
361
+ /(?:The\s+game\s+mode|Gamemode)\s+has\s+been\s+updated\s+to\s+(\w+)/,
362
+ (match) => ({
363
+ gamemode: match[1]
364
+ }),
365
+ 4
366
+ );
367
+
368
+ // Save start
369
+ this._registry.addPattern(
370
+ 'world.save.start',
371
+ /Saving\s+(?:the\s+game|chunks\s+for\s+level)/,
372
+ (match) => ({}),
373
+ 4
374
+ );
375
+
376
+ // Save complete
377
+ this._registry.addPattern(
378
+ 'world.save.complete',
379
+ /Saved\s+the\s+game/,
380
+ (match) => ({}),
381
+ 4
382
+ );
383
+
384
+ // =========================================================================
385
+ // PRIORITY 2: Status Events (server lifecycle)
386
+ // =========================================================================
387
+
388
+ // Server starting (version)
389
+ this._registry.addPattern(
390
+ 'status.start',
391
+ /Starting\s+minecraft\s+server\s+version\s+(.+)/,
392
+ (match) => ({
393
+ version: match[1]?.trim()
394
+ }),
395
+ 2
396
+ );
397
+
398
+ // Server starting (generic)
399
+ this._registry.addPattern(
400
+ 'status.starting',
401
+ /Starting\s+(?:minecraft\s+server|server)/,
402
+ (match) => ({}),
403
+ 2
404
+ );
405
+
406
+ // Loading properties
407
+ this._registry.addPattern(
408
+ 'status.loading',
409
+ /Loading\s+(?:properties|chunks|world)/,
410
+ (match) => ({}),
411
+ 2
412
+ );
413
+
414
+ // Default game type
415
+ this._registry.addPattern(
416
+ 'status.gametype',
417
+ /Default\s+game\s+type:\s+(\w+)/,
418
+ (match) => ({
419
+ gameType: match[1]
420
+ }),
421
+ 2
422
+ );
423
+
424
+ // Generating keypair
425
+ this._registry.addPattern(
426
+ 'status.keypair',
427
+ /Generating\s+keypair/,
428
+ (match) => ({}),
429
+ 2
430
+ );
431
+
432
+ // Preparing level
433
+ this._registry.addPattern(
434
+ 'status.preparing',
435
+ /Preparing\s+(?:level\s+"([^"]+)"|start\s+region)/,
436
+ (match) => match[1] ? { level: match[1] } : {},
437
+ 2
438
+ );
439
+
440
+ // Done loading
441
+ this._registry.addPattern(
442
+ 'status.done',
443
+ /Done\s+\([^)]+\)!\s+For\s+help,\s+type\s+"help"/,
444
+ (match) => ({}),
445
+ 2
446
+ );
447
+
448
+ // Time elapsed
449
+ this._registry.addPattern(
450
+ 'status.elapsed',
451
+ /Time\s+elapsed:\s+(\d+)\s+ms/,
452
+ (match) => ({
453
+ elapsed: parseInt(match[1], 10)
454
+ }),
455
+ 2
456
+ );
457
+
458
+ // =========================================================================
459
+ // PRIORITY 1: Plugin Events (extensible fallback)
460
+ // =========================================================================
461
+
462
+ // Placeholder for plugin-defined patterns
463
+ // Users can add custom patterns via addPattern() method
464
+ }
465
+
466
+ /**
467
+ * Add a custom pattern to the parser
468
+ *
469
+ * Allows users to extend the parser with plugin-specific patterns.
470
+ *
471
+ * @param {string} name - Pattern name (e.g., 'plugin.myevent')
472
+ * @param {string|RegExp} pattern - Regex pattern to match
473
+ * @param {Function} handler - Function to extract data from regex match
474
+ * @param {number} [priority] - Priority (0-10, default 1 for plugin patterns)
475
+ * @returns {void}
476
+ */
477
+ addPattern(name, pattern, handler, priority) {
478
+ this._registry.addPattern(name, pattern, handler, priority ?? 1);
479
+ }
480
+
481
+ /**
482
+ * Remove a pattern from the parser
483
+ *
484
+ * @param {string} name - Pattern name to remove
485
+ * @returns {boolean} - True if pattern was removed
486
+ */
487
+ removePattern(name) {
488
+ return this._registry.removePattern(name);
489
+ }
490
+
491
+ /**
492
+ * Get all registered patterns
493
+ *
494
+ * @returns {Array} - Array of pattern definitions
495
+ */
496
+ getPatterns() {
497
+ return this._registry.getPatterns();
498
+ }
499
+
500
+ /**
501
+ * Clone the parser with all its patterns
502
+ *
503
+ * @returns {MinecraftLogParser} - A new parser instance with cloned patterns
504
+ */
505
+ clone() {
506
+ const cloned = new MinecraftLogParser(this._options);
507
+ cloned._registry = this._registry.clone();
508
+ return cloned;
509
+ }
510
+ }
511
+
512
+ module.exports = { MinecraftLogParser };