@pilaf/backends 1.0.2 → 1.2.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.
@@ -8,6 +8,9 @@ const { PilafBackend } = require('./backend.js');
8
8
  const { BotPool } = require('./BotPool.js');
9
9
  const { BotLifecycleManager } = require('./BotLifecycleManager.js');
10
10
  const { ServerHealthChecker } = require('./ServerHealthChecker.js');
11
+ const { RconBackend } = require('./rcon-backend.js');
12
+ const { QueryHelper } = require('./helpers/QueryHelper.js');
13
+ const { EventObserver } = require('./helpers/EventObserver.js');
11
14
 
12
15
  class MineflayerBackend extends PilafBackend {
13
16
  /**
@@ -22,6 +25,9 @@ class MineflayerBackend extends PilafBackend {
22
25
  this._connectConfig = {};
23
26
  this._botPool = new BotPool();
24
27
  this._healthChecker = null;
28
+ this._rconBackend = null; // Optional RconBackend for QueryHelper
29
+ this._queryHelper = null;
30
+ this._eventObserver = null;
25
31
  }
26
32
 
27
33
  /**
@@ -82,16 +88,36 @@ class MineflayerBackend extends PilafBackend {
82
88
  rconPassword: config?.rconPassword || 'cavarest'
83
89
  });
84
90
 
91
+ // Create RconBackend if RCON config is provided (for QueryHelper)
92
+ if (config?.rconHost || config?.rconPort || config?.rconPassword) {
93
+ this._rconBackend = new RconBackend();
94
+ await this._rconBackend.connect({
95
+ host: config?.rconHost || this._host,
96
+ port: config?.rconPort || 25575,
97
+ password: config?.rconPassword || 'cavarest'
98
+ });
99
+ // Initialize query helper with the RCON backend
100
+ this._queryHelper = new QueryHelper(this._rconBackend);
101
+ } else {
102
+ this._queryHelper = null;
103
+ }
104
+
85
105
  return this;
86
106
  }
87
107
 
88
108
  /**
89
- * Disconnect all bots
109
+ * Disconnect all bots and RCON
90
110
  * @returns {Promise<void>}
91
111
  */
92
112
  async disconnect() {
93
113
  const results = await this._botPool.quitAll({ disconnectTimeout: 10000 });
94
114
  this._botPool.clear();
115
+
116
+ // Disconnect RCON backend if it was created
117
+ if (this._rconBackend) {
118
+ await this._rconBackend.disconnect();
119
+ this._rconBackend = null;
120
+ }
95
121
  }
96
122
 
97
123
  /**
@@ -201,6 +227,245 @@ class MineflayerBackend extends PilafBackend {
201
227
  return this._botPool;
202
228
  }
203
229
 
230
+ /**
231
+ * Get query helper instance
232
+ * @returns {QueryHelper}
233
+ */
234
+ getQueryHelper() {
235
+ return this._queryHelper;
236
+ }
237
+
238
+ /**
239
+ * Get event observer instance
240
+ * @returns {EventObserver}
241
+ */
242
+ getEventObserver() {
243
+ return this._eventObserver;
244
+ }
245
+
246
+ // ==========================================================================
247
+ // QUERY HELPER METHODS
248
+ // ==========================================================================
249
+
250
+ /**
251
+ * Get player information via RCON
252
+ * @param {string} username - Player username
253
+ * @returns {Promise<Object>} Player information
254
+ */
255
+ async getPlayerInfo(username) {
256
+ if (!this._queryHelper) {
257
+ throw new Error('Backend not connected. Call connect() first.');
258
+ }
259
+ return await this._queryHelper.getPlayerInfo(username);
260
+ }
261
+
262
+ /**
263
+ * List all online players
264
+ * @returns {Promise<Object>} List of players
265
+ */
266
+ async listPlayers() {
267
+ if (!this._queryHelper) {
268
+ throw new Error('Backend not connected. Call connect() first.');
269
+ }
270
+ return await this._queryHelper.listPlayers();
271
+ }
272
+
273
+ /**
274
+ * Get current world time
275
+ * @returns {Promise<Object>} World time information
276
+ */
277
+ async getWorldTime() {
278
+ if (!this._queryHelper) {
279
+ throw new Error('Backend not connected. Call connect() first.');
280
+ }
281
+ return await this._queryHelper.getWorldTime();
282
+ }
283
+
284
+ /**
285
+ * Get current weather
286
+ * @returns {Promise<Object>} Weather information
287
+ */
288
+ async getWeather() {
289
+ if (!this._queryHelper) {
290
+ throw new Error('Backend not connected. Call connect() first.');
291
+ }
292
+ return await this._queryHelper.getWeather();
293
+ }
294
+
295
+ /**
296
+ * Get difficulty level
297
+ * @returns {Promise<Object>} Difficulty information
298
+ */
299
+ async getDifficulty() {
300
+ if (!this._queryHelper) {
301
+ throw new Error('Backend not connected. Call connect() first.');
302
+ }
303
+ return await this._queryHelper.getDifficulty();
304
+ }
305
+
306
+ /**
307
+ * Get game mode
308
+ * @param {string} [player='@s'] - Target player selector
309
+ * @returns {Promise<Object>} Game mode information
310
+ */
311
+ async getGameMode(player = '@s') {
312
+ if (!this._queryHelper) {
313
+ throw new Error('Backend not connected. Call connect() first.');
314
+ }
315
+ return await this._queryHelper.getGameMode(player);
316
+ }
317
+
318
+ /**
319
+ * Get server TPS
320
+ * @returns {Promise<Object>} TPS information
321
+ */
322
+ async getTPS() {
323
+ if (!this._queryHelper) {
324
+ throw new Error('Backend not connected. Call connect() first.');
325
+ }
326
+ return await this._queryHelper.getTPS();
327
+ }
328
+
329
+ /**
330
+ * Get world seed
331
+ * @returns {Promise<Object>} Seed information
332
+ */
333
+ async getSeed() {
334
+ if (!this._queryHelper) {
335
+ throw new Error('Backend not connected. Call connect() first.');
336
+ }
337
+ return await this._queryHelper.getSeed();
338
+ }
339
+
340
+ // ==========================================================================
341
+ // EVENT OBSERVATION METHODS
342
+ // ==========================================================================
343
+
344
+ /**
345
+ * Subscribe to server events
346
+ * @param {string} pattern - Event pattern (supports wildcards)
347
+ * @param {Function} callback - Event callback
348
+ * @returns {Function} Unsubscribe function
349
+ */
350
+ onEvent(pattern, callback) {
351
+ this._ensureEventObserver();
352
+ return this._eventObserver.onEvent(pattern, callback);
353
+ }
354
+
355
+ /**
356
+ * Subscribe to player join events
357
+ * @param {Function} callback - Event callback
358
+ * @returns {Function} Unsubscribe function
359
+ */
360
+ onPlayerJoin(callback) {
361
+ this._ensureEventObserver();
362
+ return this._eventObserver.onPlayerJoin(callback);
363
+ }
364
+
365
+ /**
366
+ * Subscribe to player leave events
367
+ * @param {Function} callback - Event callback
368
+ * @returns {Function} Unsubscribe function
369
+ */
370
+ onPlayerLeave(callback) {
371
+ this._ensureEventObserver();
372
+ return this._eventObserver.onPlayerLeave(callback);
373
+ }
374
+
375
+ /**
376
+ * Subscribe to player death events
377
+ * @param {Function} callback - Event callback
378
+ * @returns {Function} Unsubscribe function
379
+ */
380
+ onPlayerDeath(callback) {
381
+ this._ensureEventObserver();
382
+ return this._eventObserver.onPlayerDeath(callback);
383
+ }
384
+
385
+ /**
386
+ * Subscribe to command events
387
+ * @param {Function} callback - Event callback
388
+ * @returns {Function} Unsubscribe function
389
+ */
390
+ onCommand(callback) {
391
+ this._ensureEventObserver();
392
+ return this._eventObserver.onCommand(callback);
393
+ }
394
+
395
+ /**
396
+ * Subscribe to world events
397
+ * @param {Function} callback - Event callback
398
+ * @returns {Function} Unsubscribe function
399
+ */
400
+ onWorldEvent(callback) {
401
+ this._ensureEventObserver();
402
+ return this._eventObserver.onWorldEvent(callback);
403
+ }
404
+
405
+ /**
406
+ * Start observing server events
407
+ * @returns {Promise<void>}
408
+ */
409
+ async observe() {
410
+ this._ensureEventObserver();
411
+ await this._eventObserver.start();
412
+ }
413
+
414
+ /**
415
+ * Stop observing server events
416
+ * @returns {void}
417
+ */
418
+ unobserve() {
419
+ if (this._eventObserver) {
420
+ this._eventObserver.stop();
421
+ }
422
+ }
423
+
424
+ /**
425
+ * Check if currently observing events
426
+ * @returns {boolean}
427
+ */
428
+ isObserving() {
429
+ return this._eventObserver && this._eventObserver.isObserving;
430
+ }
431
+
432
+ /**
433
+ * Ensure event observer is initialized
434
+ * @private
435
+ */
436
+ _ensureEventObserver() {
437
+ if (!this._eventObserver) {
438
+ // Lazy initialization - requires LogMonitor and parser
439
+ const { LogMonitor } = require('./monitoring/index.js');
440
+ const { MinecraftLogParser } = require('./parsers/index.js');
441
+ const { DockerLogCollector } = require('./collectors/index.js');
442
+ const { UsernameCorrelationStrategy } = require('./monitoring/correlations/index.js');
443
+
444
+ // Create log collector for this server
445
+ const collector = new DockerLogCollector({
446
+ container: this._host, // Use server host as container name
447
+ follow: true
448
+ });
449
+
450
+ // Create parser
451
+ const parser = new MinecraftLogParser();
452
+
453
+ // Create log monitor
454
+ const logMonitor = new LogMonitor({
455
+ collector,
456
+ parser,
457
+ correlation: new UsernameCorrelationStrategy(),
458
+ bufferSize: 1000
459
+ });
460
+
461
+ // Create event observer
462
+ this._eventObserver = new EventObserver({
463
+ logMonitor,
464
+ parser
465
+ });
466
+ }
467
+ }
468
+
204
469
  /**
205
470
  * Get entities from all bots
206
471
  * @param {string} selector - Entity selector (optional, unused in Mineflayer)
@@ -0,0 +1,202 @@
1
+ /**
2
+ * CircularBuffer - Fixed-size circular buffer implementation
3
+ *
4
+ * A circular buffer is a fixed-size data structure that uses a single,
5
+ * contiguous block of memory. When the buffer is full, new data overwrites
6
+ * the oldest data.
7
+ *
8
+ * This provides O(1) time complexity for push and pop operations,
9
+ * making it ideal for streaming data and log monitoring scenarios.
10
+ *
11
+ * Usage Example:
12
+ * const buffer = new CircularBuffer(3);
13
+ * buffer.push(1); // [1]
14
+ * buffer.push(2); // [1, 2]
15
+ * buffer.push(3); // [1, 2, 3]
16
+ * buffer.push(4); // [2, 3, 4] - 1 was overwritten
17
+ * buffer.pop(); // Returns 2, buffer is [3, 4]
18
+ */
19
+
20
+ /**
21
+ * CircularBuffer class for efficient fixed-size buffer operations
22
+ */
23
+ class CircularBuffer {
24
+ /**
25
+ * Create a CircularBuffer
26
+ *
27
+ * @param {number} capacity - Maximum number of elements (must be > 0)
28
+ * @throws {Error} If capacity is not a positive integer
29
+ */
30
+ constructor(capacity) {
31
+ if (!Number.isInteger(capacity) || capacity <= 0) {
32
+ throw new Error(`Capacity must be a positive integer, got: ${capacity}`);
33
+ }
34
+
35
+ /**
36
+ * Maximum capacity of the buffer
37
+ * @private
38
+ * @type {number}
39
+ */
40
+ this._capacity = capacity;
41
+
42
+ /**
43
+ * Internal array to store elements
44
+ * @private
45
+ * @type {Array}
46
+ */
47
+ this._buffer = new Array(capacity);
48
+
49
+ /**
50
+ * Index of the oldest element (front of queue)
51
+ * @private
52
+ * @type {number}
53
+ */
54
+ this._head = 0;
55
+
56
+ /**
57
+ * Index where the next element will be inserted (back of queue)
58
+ * @private
59
+ * @type {number}
60
+ */
61
+ this._tail = 0;
62
+
63
+ /**
64
+ * Current number of elements in the buffer
65
+ * @private
66
+ * @type {number}
67
+ */
68
+ this._size = 0;
69
+ }
70
+
71
+ /**
72
+ * Add a value to the buffer
73
+ *
74
+ * If the buffer is full, the oldest value is removed and returned.
75
+ *
76
+ * @param {*} value - Value to add
77
+ * @returns {*} - The displaced value if buffer was full, undefined otherwise
78
+ */
79
+ push(value) {
80
+ const displaced = this.isFull ? this._buffer[this._head] : undefined;
81
+
82
+ this._buffer[this._tail] = value;
83
+ this._tail = (this._tail + 1) % this._capacity;
84
+
85
+ if (this.isFull) {
86
+ this._head = (this._head + 1) % this._capacity;
87
+ } else {
88
+ this._size++;
89
+ }
90
+
91
+ return displaced;
92
+ }
93
+
94
+ /**
95
+ * Remove and return the oldest value
96
+ *
97
+ * @returns {*} - The oldest value, or undefined if buffer is empty
98
+ */
99
+ pop() {
100
+ if (this.isEmpty) {
101
+ return undefined;
102
+ }
103
+
104
+ const value = this._buffer[this._head];
105
+ this._buffer[this._head] = undefined; // Clear reference
106
+ this._head = (this._head + 1) % this._capacity;
107
+ this._size--;
108
+
109
+ return value;
110
+ }
111
+
112
+ /**
113
+ * Get value at logical index (0 = oldest, size-1 = newest)
114
+ *
115
+ * @param {number} index - Logical index
116
+ * @returns {*} - Value at index, or undefined if index is out of bounds
117
+ */
118
+ get(index) {
119
+ if (index < 0 || index >= this._size) {
120
+ return undefined;
121
+ }
122
+
123
+ const physicalIndex = (this._head + index) % this._capacity;
124
+ return this._buffer[physicalIndex];
125
+ }
126
+
127
+ /**
128
+ * Convert buffer to a regular array (ordered from oldest to newest)
129
+ *
130
+ * @returns {Array} - Array containing all elements
131
+ */
132
+ toArray() {
133
+ const result = [];
134
+ for (let i = 0; i < this._size; i++) {
135
+ result.push(this.get(i));
136
+ }
137
+ return result;
138
+ }
139
+
140
+ /**
141
+ * Extract a slice of the buffer
142
+ *
143
+ * @param {number} start - Start index (default: 0)
144
+ * @param {number} end - End index (default: size)
145
+ * @returns {Array} - Array containing elements from start to end-1
146
+ */
147
+ slice(start = 0, end = this._size) {
148
+ const result = [];
149
+ for (let i = start; i < Math.min(end, this._size); i++) {
150
+ result.push(this.get(i));
151
+ }
152
+ return result;
153
+ }
154
+
155
+ /**
156
+ * Clear all elements from the buffer
157
+ */
158
+ clear() {
159
+ this._buffer = new Array(this._capacity);
160
+ this._head = 0;
161
+ this._tail = 0;
162
+ this._size = 0;
163
+ }
164
+
165
+ /**
166
+ * Get the current number of elements
167
+ *
168
+ * @type {number}
169
+ */
170
+ get size() {
171
+ return this._size;
172
+ }
173
+
174
+ /**
175
+ * Get the maximum capacity
176
+ *
177
+ * @type {number}
178
+ */
179
+ get capacity() {
180
+ return this._capacity;
181
+ }
182
+
183
+ /**
184
+ * Check if buffer is empty
185
+ *
186
+ * @type {boolean}
187
+ */
188
+ get isEmpty() {
189
+ return this._size === 0;
190
+ }
191
+
192
+ /**
193
+ * Check if buffer is full
194
+ *
195
+ * @type {boolean}
196
+ */
197
+ get isFull() {
198
+ return this._size === this._capacity;
199
+ }
200
+ }
201
+
202
+ module.exports = { CircularBuffer };