@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.
- package/README.md +1170 -10
- package/lib/collectors/DockerLogCollector.js +334 -0
- package/lib/collectors/index.js +9 -0
- package/lib/core/CommandRouter.js +154 -0
- package/lib/core/CorrelationStrategy.js +172 -0
- package/lib/core/LogCollector.js +194 -0
- package/lib/core/LogParser.js +125 -0
- package/lib/core/index.js +26 -0
- package/lib/errors/index.js +363 -0
- package/lib/helpers/EventObserver.js +267 -0
- package/lib/helpers/QueryHelper.js +279 -0
- package/lib/helpers/index.js +13 -0
- package/lib/index.js +72 -1
- package/lib/mineflayer-backend.js +266 -1
- package/lib/monitoring/CircularBuffer.js +202 -0
- package/lib/monitoring/LogMonitor.js +303 -0
- package/lib/monitoring/correlations/TagCorrelationStrategy.js +214 -0
- package/lib/monitoring/correlations/UsernameCorrelationStrategy.js +233 -0
- package/lib/monitoring/correlations/index.js +13 -0
- package/lib/monitoring/index.js +16 -0
- package/lib/parsers/MinecraftLogParser.js +512 -0
- package/lib/parsers/PatternRegistry.js +366 -0
- package/lib/parsers/fixtures/minecraft-logs.js +188 -0
- package/lib/parsers/index.js +13 -0
- package/lib/rcon-backend.js +42 -26
- package/package.json +3 -2
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventObserver - Observe and subscribe to Minecraft server events
|
|
3
|
+
*
|
|
4
|
+
* Wraps LogMonitor and MinecraftLogParser to provide a clean API for
|
|
5
|
+
* subscribing to Minecraft server events without manually parsing logs.
|
|
6
|
+
*
|
|
7
|
+
* Responsibilities (MECE):
|
|
8
|
+
* - ONLY: Provide clean API for event subscriptions
|
|
9
|
+
* - NOT: Collect logs (LogCollector's responsibility)
|
|
10
|
+
* - NOT: Parse logs (LogParser's responsibility)
|
|
11
|
+
* - NOT: Monitor logs (LogMonitor's responsibility)
|
|
12
|
+
*
|
|
13
|
+
* Usage Example:
|
|
14
|
+
* const observer = new EventObserver(logMonitor, parser);
|
|
15
|
+
* observer.onPlayerJoin((event) => {
|
|
16
|
+
* console.log('Player joined:', event.data.player);
|
|
17
|
+
* });
|
|
18
|
+
* await observer.start();
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const { EventEmitter } = require('events');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* EventObserver class for event subscriptions
|
|
25
|
+
* @extends EventEmitter
|
|
26
|
+
*/
|
|
27
|
+
class EventObserver extends EventEmitter {
|
|
28
|
+
/**
|
|
29
|
+
* Create an EventObserver
|
|
30
|
+
*
|
|
31
|
+
* @param {Object} options - Observer options
|
|
32
|
+
* @param {Object} options.logMonitor - LogMonitor instance
|
|
33
|
+
* @param {Object} options.parser - LogParser instance
|
|
34
|
+
* @throws {Error} If required dependencies are not provided
|
|
35
|
+
*/
|
|
36
|
+
constructor(options = {}) {
|
|
37
|
+
super();
|
|
38
|
+
|
|
39
|
+
if (!options.logMonitor) {
|
|
40
|
+
throw new Error('logMonitor is required');
|
|
41
|
+
}
|
|
42
|
+
if (!options.parser) {
|
|
43
|
+
throw new Error('parser is required');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* LogMonitor instance for log collection
|
|
48
|
+
* @private
|
|
49
|
+
* @type {Object}
|
|
50
|
+
*/
|
|
51
|
+
this._logMonitor = options.logMonitor;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* LogParser instance for parsing logs
|
|
55
|
+
* @private
|
|
56
|
+
* @type {Object}
|
|
57
|
+
*/
|
|
58
|
+
this._parser = options.parser;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Event subscriptions: pattern -> Set<callback>
|
|
62
|
+
* @private
|
|
63
|
+
* @type {Map<string, Set<Function>>}
|
|
64
|
+
*/
|
|
65
|
+
this._subscriptions = new Map();
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Whether observer is currently running
|
|
69
|
+
* @private
|
|
70
|
+
* @type {boolean}
|
|
71
|
+
*/
|
|
72
|
+
this._isObserving = false;
|
|
73
|
+
|
|
74
|
+
// Set up event listener for log monitor
|
|
75
|
+
this._logMonitor.on('event', (event) => {
|
|
76
|
+
this._handleEvent(event);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Subscribe to events matching a pattern
|
|
82
|
+
*
|
|
83
|
+
* @param {string} pattern - Event pattern (supports wildcards: "entity.*")
|
|
84
|
+
* @param {Function} callback - Callback function(event)
|
|
85
|
+
* @returns {Function} - Unsubscribe function
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* const unsubscribe = observer.onEvent('entity.*', (event) => {
|
|
89
|
+
* console.log('Entity event:', event.type);
|
|
90
|
+
* });
|
|
91
|
+
*/
|
|
92
|
+
onEvent(pattern, callback) {
|
|
93
|
+
if (!this._subscriptions.has(pattern)) {
|
|
94
|
+
this._subscriptions.set(pattern, new Set());
|
|
95
|
+
}
|
|
96
|
+
this._subscriptions.get(pattern).add(callback);
|
|
97
|
+
|
|
98
|
+
// Return unsubscribe function
|
|
99
|
+
return () => {
|
|
100
|
+
this._subscriptions.get(pattern)?.delete(callback);
|
|
101
|
+
if (this._subscriptions.get(pattern)?.size === 0) {
|
|
102
|
+
this._subscriptions.delete(pattern);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Subscribe to player join events
|
|
109
|
+
*
|
|
110
|
+
* @param {Function} callback - Callback function(event)
|
|
111
|
+
* @returns {Function} - Unsubscribe function
|
|
112
|
+
*/
|
|
113
|
+
onPlayerJoin(callback) {
|
|
114
|
+
return this.onEvent('entity.join', callback);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Subscribe to player leave events
|
|
119
|
+
*
|
|
120
|
+
* @param {Function} callback - Callback function(event)
|
|
121
|
+
* @returns {Function} - Unsubscribe function
|
|
122
|
+
*/
|
|
123
|
+
onPlayerLeave(callback) {
|
|
124
|
+
return this.onEvent('entity.leave', callback);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Subscribe to player death events
|
|
129
|
+
*
|
|
130
|
+
* @param {Function} callback - Callback function(event)
|
|
131
|
+
* @returns {Function} - Unsubscribe function
|
|
132
|
+
*/
|
|
133
|
+
onPlayerDeath(callback) {
|
|
134
|
+
return this.onEvent('entity.death.*', callback);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Subscribe to command events
|
|
139
|
+
*
|
|
140
|
+
* @param {Function} callback - Callback function(event)
|
|
141
|
+
* @returns {Function} - Unsubscribe function
|
|
142
|
+
*/
|
|
143
|
+
onCommand(callback) {
|
|
144
|
+
return this.onEvent('command.*', callback);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Subscribe to world events (time, weather, save)
|
|
149
|
+
*
|
|
150
|
+
* @param {Function} callback - Callback function(event)
|
|
151
|
+
* @returns {Function} - Unsubscribe function
|
|
152
|
+
*/
|
|
153
|
+
onWorldEvent(callback) {
|
|
154
|
+
return this.onEvent('world.*', callback);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Start observing events
|
|
159
|
+
*
|
|
160
|
+
* @returns {Promise<void>}
|
|
161
|
+
* @throws {Error} If already observing
|
|
162
|
+
*/
|
|
163
|
+
async start() {
|
|
164
|
+
if (this._isObserving) {
|
|
165
|
+
throw new Error('EventObserver is already observing');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
this._isObserving = true;
|
|
169
|
+
await this._logMonitor.start();
|
|
170
|
+
this.emit('start');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Stop observing events
|
|
175
|
+
*
|
|
176
|
+
* @returns {void}
|
|
177
|
+
*/
|
|
178
|
+
stop() {
|
|
179
|
+
if (!this._isObserving) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
this._isObserving = false;
|
|
184
|
+
this._logMonitor.stop();
|
|
185
|
+
this.emit('stop');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Check if observer is currently running
|
|
190
|
+
*
|
|
191
|
+
* @type {boolean}
|
|
192
|
+
*/
|
|
193
|
+
get isObserving() {
|
|
194
|
+
return this._isObserving;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Get all active subscriptions
|
|
199
|
+
*
|
|
200
|
+
* @returns {Array<Object>} - Array of {pattern, callbackCount}
|
|
201
|
+
*/
|
|
202
|
+
getSubscriptions() {
|
|
203
|
+
return Array.from(this._subscriptions.entries()).map(([pattern, callbacks]) => ({
|
|
204
|
+
pattern,
|
|
205
|
+
callbackCount: callbacks.size
|
|
206
|
+
}));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Clear all event subscriptions
|
|
211
|
+
*
|
|
212
|
+
* @returns {void}
|
|
213
|
+
*/
|
|
214
|
+
clearSubscriptions() {
|
|
215
|
+
this._subscriptions.clear();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Handle incoming event from log monitor
|
|
220
|
+
*
|
|
221
|
+
* @private
|
|
222
|
+
* @param {Object} event - Parsed event
|
|
223
|
+
* @returns {void}
|
|
224
|
+
*/
|
|
225
|
+
_handleEvent(event) {
|
|
226
|
+
// Find matching subscriptions and notify
|
|
227
|
+
for (const [pattern, callbacks] of this._subscriptions) {
|
|
228
|
+
if (this._matchesPattern(event.type, pattern)) {
|
|
229
|
+
callbacks.forEach(cb => {
|
|
230
|
+
try {
|
|
231
|
+
cb(event);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
// Emit error event but don't crash
|
|
234
|
+
this.emit('error', { error, event });
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Check if event type matches pattern
|
|
243
|
+
*
|
|
244
|
+
* @private
|
|
245
|
+
* @param {string} eventType - Event type (e.g., "entity.join")
|
|
246
|
+
* @param {string} pattern - Pattern (e.g., "entity.*", "command.issued")
|
|
247
|
+
* @returns {boolean} - True if pattern matches
|
|
248
|
+
*/
|
|
249
|
+
_matchesPattern(eventType, pattern) {
|
|
250
|
+
// Wildcard match
|
|
251
|
+
if (pattern === '*') {
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Glob-style pattern matching
|
|
256
|
+
if (pattern.includes('*')) {
|
|
257
|
+
const regexPattern = '^' + pattern.replace(/\*/g, '.*') + '$';
|
|
258
|
+
const regex = new RegExp(regexPattern);
|
|
259
|
+
return regex.test(eventType);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Exact match
|
|
263
|
+
return eventType === pattern;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
module.exports = { EventObserver };
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QueryHelper - Convenience methods for common Minecraft server queries
|
|
3
|
+
*
|
|
4
|
+
* Wraps RconBackend to provide structured data from common RCON commands.
|
|
5
|
+
* Parses raw RCON responses into useful JavaScript objects.
|
|
6
|
+
*
|
|
7
|
+
* Responsibilities (MECE):
|
|
8
|
+
* - ONLY: Provide convenience methods for RCON queries
|
|
9
|
+
* - NOT: Connect to RCON (RconBackend's responsibility)
|
|
10
|
+
* - NOT: Handle errors (RconBackend's responsibility)
|
|
11
|
+
* - NOT: Cache results (caller's responsibility)
|
|
12
|
+
*
|
|
13
|
+
* Usage Example:
|
|
14
|
+
* const helper = new QueryHelper(rconBackend);
|
|
15
|
+
* const players = await helper.listPlayers();
|
|
16
|
+
* // Returns: { online: 2, players: ['Steve', 'Alex'] }
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* QueryHelper class for structured RCON queries
|
|
21
|
+
*/
|
|
22
|
+
class QueryHelper {
|
|
23
|
+
/**
|
|
24
|
+
* Create a QueryHelper
|
|
25
|
+
*
|
|
26
|
+
* @param {Object} rconBackend - RconBackend instance
|
|
27
|
+
* @throws {Error} If rconBackend is not provided
|
|
28
|
+
*/
|
|
29
|
+
constructor(rconBackend) {
|
|
30
|
+
if (!rconBackend) {
|
|
31
|
+
throw new Error('RconBackend is required');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* RconBackend instance
|
|
36
|
+
* @private
|
|
37
|
+
* @type {Object}
|
|
38
|
+
*/
|
|
39
|
+
this._rcon = rconBackend;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get information about a specific player
|
|
44
|
+
*
|
|
45
|
+
* @param {string} username - Player username
|
|
46
|
+
* @returns {Promise<Object>} - Player information
|
|
47
|
+
* @property {string} username - Player name
|
|
48
|
+
* @property {Object} position - Player position {x, y, z}
|
|
49
|
+
* @property {number} health - Player health
|
|
50
|
+
* @property {number} food - Player food level
|
|
51
|
+
* @property {number} exp - Player experience
|
|
52
|
+
* @property {number} level - Player level
|
|
53
|
+
*/
|
|
54
|
+
async getPlayerInfo(username) {
|
|
55
|
+
const response = await this._rcon.sendCommand(`data get entity ${username}`);
|
|
56
|
+
|
|
57
|
+
// If RCON returned parsed NBT data, use it
|
|
58
|
+
if (response.parsed) {
|
|
59
|
+
return this._parsePlayerInfo(response.parsed, username);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Otherwise parse from raw text or return minimal info
|
|
63
|
+
return {
|
|
64
|
+
username,
|
|
65
|
+
position: null,
|
|
66
|
+
health: null,
|
|
67
|
+
food: null,
|
|
68
|
+
exp: null,
|
|
69
|
+
level: null,
|
|
70
|
+
raw: response.raw
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* List all online players
|
|
76
|
+
*
|
|
77
|
+
* @returns {Promise<Object>} - List of online players
|
|
78
|
+
* @property {number} online - Number of online players
|
|
79
|
+
* @property {Array<string>} players - Array of player names
|
|
80
|
+
*/
|
|
81
|
+
async listPlayers() {
|
|
82
|
+
const response = await this._rcon.sendCommand('/list');
|
|
83
|
+
|
|
84
|
+
// Parse response format: "There are 2 players online: Steve, Alex"
|
|
85
|
+
const onlineMatch = response.raw.match(/There are (\d+) players online:? (.*)/);
|
|
86
|
+
if (onlineMatch) {
|
|
87
|
+
const online = parseInt(onlineMatch[1], 10);
|
|
88
|
+
const playersStr = onlineMatch[2].trim();
|
|
89
|
+
const players = playersStr === '' ? [] : playersStr.split(', ').map(p => p.trim());
|
|
90
|
+
return { online, players };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Alternative format: "Steve, Alex"
|
|
94
|
+
const playersMatch = response.raw.match(/^([a-zA-Z0-9_]+(?:, [a-zA-Z0-9_]+)*)$/);
|
|
95
|
+
if (playersMatch) {
|
|
96
|
+
const players = response.raw.split(', ').map(p => p.trim());
|
|
97
|
+
return { online: players.length, players };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Fallback
|
|
101
|
+
return { online: 0, players: [], raw: response.raw };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get current world time
|
|
106
|
+
*
|
|
107
|
+
* @returns {Promise<Object>} - World time information
|
|
108
|
+
* @property {number} time - Game time (0-24000)
|
|
109
|
+
* @property {boolean} daytime - Whether it's daytime
|
|
110
|
+
*/
|
|
111
|
+
async getWorldTime() {
|
|
112
|
+
const response = await this._rcon.sendCommand('/time query daytime');
|
|
113
|
+
|
|
114
|
+
// Parse: "The time is 1500"
|
|
115
|
+
const timeMatch = response.raw.match(/The time is (\d+)/);
|
|
116
|
+
if (timeMatch) {
|
|
117
|
+
const time = parseInt(timeMatch[1], 10);
|
|
118
|
+
return {
|
|
119
|
+
time,
|
|
120
|
+
daytime: time >= 0 && time < 13000
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Parse: "Time: 1500" (alternative format)
|
|
125
|
+
const altMatch = response.raw.match(/Time: (\d+)/);
|
|
126
|
+
if (altMatch) {
|
|
127
|
+
const time = parseInt(altMatch[1], 10);
|
|
128
|
+
return {
|
|
129
|
+
time,
|
|
130
|
+
daytime: time >= 0 && time < 13000
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { time: null, daytime: null, raw: response.raw };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get current weather state
|
|
139
|
+
*
|
|
140
|
+
* @returns {Promise<Object>} - Weather information
|
|
141
|
+
* @property {string} weather - Weather type (clear, rain, thunder)
|
|
142
|
+
* @property {number} duration - Remaining duration (if available)
|
|
143
|
+
*/
|
|
144
|
+
async getWeather() {
|
|
145
|
+
const response = await this._rcon.sendCommand('/weather query');
|
|
146
|
+
|
|
147
|
+
// Parse: "The weather is clear"
|
|
148
|
+
const weatherMatch = response.raw.match(/The weather is (\w+)/);
|
|
149
|
+
if (weatherMatch) {
|
|
150
|
+
return { weather: weatherMatch[1].toLowerCase() };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Parse: "Weather: clear" (alternative format)
|
|
154
|
+
const altMatch = response.raw.match(/Weather: (\w+)/);
|
|
155
|
+
if (altMatch) {
|
|
156
|
+
return { weather: altMatch[1].toLowerCase() };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { weather: null, raw: response.raw };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get difficulty level
|
|
164
|
+
*
|
|
165
|
+
* @returns {Promise<Object>} - Difficulty information
|
|
166
|
+
* @property {string} difficulty - Difficulty (peaceful, easy, normal, hard)
|
|
167
|
+
*/
|
|
168
|
+
async getDifficulty() {
|
|
169
|
+
const response = await this._rcon.sendCommand('/difficulty');
|
|
170
|
+
|
|
171
|
+
// Parse: "The difficulty is set to: Normal"
|
|
172
|
+
const match = response.raw.match(/The difficulty is set to: (\w+)/);
|
|
173
|
+
if (match) {
|
|
174
|
+
return { difficulty: match[1].toLowerCase() };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Parse: "Difficulty: Normal" (alternative format)
|
|
178
|
+
const altMatch = response.raw.match(/Difficulty: (\w+)/);
|
|
179
|
+
if (altMatch) {
|
|
180
|
+
return { difficulty: altMatch[1].toLowerCase() };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { difficulty: null, raw: response.raw };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get game mode
|
|
188
|
+
*
|
|
189
|
+
* @param {string} [player='@s'] - Target player selector
|
|
190
|
+
* @returns {Promise<Object>} - Game mode information
|
|
191
|
+
* @property {string} gameMode - Game mode (survival, creative, adventure, spectator)
|
|
192
|
+
*/
|
|
193
|
+
async getGameMode(player = '@s') {
|
|
194
|
+
const response = await this._rcon.sendCommand(`/gamemode query ${player}`);
|
|
195
|
+
|
|
196
|
+
// Parse: "Game mode: Creative (Player: Steve)"
|
|
197
|
+
const match = response.raw.match(/Game mode: (\w+)/);
|
|
198
|
+
if (match) {
|
|
199
|
+
return { gameMode: match[1].toLowerCase() };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { gameMode: null, raw: response.raw };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get server TPS (ticks per second)
|
|
207
|
+
*
|
|
208
|
+
* @returns {Promise<Object>} - TPS information
|
|
209
|
+
* @property {number} tps - Current TPS
|
|
210
|
+
*/
|
|
211
|
+
async getTPS() {
|
|
212
|
+
const response = await this._rcon.sendCommand('/tps');
|
|
213
|
+
|
|
214
|
+
// Parse: "TPS from last 1m, 1m, 5m, 15m: 20.0, 20.0, 20.0, 20.0"
|
|
215
|
+
const match = response.raw.match(/TPS from last [\w, ]+: ([\d., ]+)/);
|
|
216
|
+
if (match) {
|
|
217
|
+
const tpsValues = match[1].split(', ').map(s => parseFloat(s.trim()));
|
|
218
|
+
return {
|
|
219
|
+
tps: tpsValues[0] || null,
|
|
220
|
+
allTPS: tpsValues
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Parse: "Current TPS: 20.0"
|
|
225
|
+
const simpleMatch = response.raw.match(/Current TPS: ([\d.]+)/);
|
|
226
|
+
if (simpleMatch) {
|
|
227
|
+
return { tps: parseFloat(simpleMatch[1]) };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return { tps: null, raw: response.raw };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get world seed
|
|
235
|
+
*
|
|
236
|
+
* @returns {Promise<Object>} - Seed information
|
|
237
|
+
* @property {string} seed - World seed
|
|
238
|
+
*/
|
|
239
|
+
async getSeed() {
|
|
240
|
+
const response = await this._rcon.sendCommand('/seed');
|
|
241
|
+
|
|
242
|
+
// Parse: "Seed: [123456789]"
|
|
243
|
+
const match = response.raw.match(/Seed: \[([-\d]+)\]/);
|
|
244
|
+
if (match) {
|
|
245
|
+
return { seed: match[1] };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return { seed: null, raw: response.raw };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Parse player info from NBT data
|
|
253
|
+
*
|
|
254
|
+
* @private
|
|
255
|
+
* @param {Object} nbtData - NBT data from RCON
|
|
256
|
+
* @param {string} username - Player username
|
|
257
|
+
* @returns {Object} - Parsed player info
|
|
258
|
+
*/
|
|
259
|
+
_parsePlayerInfo(nbtData, username) {
|
|
260
|
+
// NBT data structure varies by Minecraft version
|
|
261
|
+
// Common structure: { Pos: [I; x, y, z], Health, FoodLevel, XpLevel, ... }
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
username,
|
|
265
|
+
position: nbtData.Pos ? {
|
|
266
|
+
x: nbtData.Pos[0],
|
|
267
|
+
y: nbtData.Pos[1],
|
|
268
|
+
z: nbtData.Pos[2]
|
|
269
|
+
} : null,
|
|
270
|
+
health: nbtData.Health || null,
|
|
271
|
+
food: nbtData.FoodLevel || null,
|
|
272
|
+
exp: nbtData.XpTotal || nbtData.XpLevel ? (nbtData.XpTotal || 0) : null,
|
|
273
|
+
level: nbtData.XpLevel || null,
|
|
274
|
+
raw: nbtData
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
module.exports = { QueryHelper };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper Classes for Pilaf Backends
|
|
3
|
+
*
|
|
4
|
+
* Utility classes that enhance backend functionality.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { QueryHelper } = require('./QueryHelper.js');
|
|
8
|
+
const { EventObserver } = require('./EventObserver.js');
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
QueryHelper,
|
|
12
|
+
EventObserver
|
|
13
|
+
};
|
package/lib/index.js
CHANGED
|
@@ -11,6 +11,42 @@ const { BotLifecycleManager } = require('./BotLifecycleManager.js');
|
|
|
11
11
|
const { ServerHealthChecker } = require('./ServerHealthChecker.js');
|
|
12
12
|
const { BotPool } = require('./BotPool.js');
|
|
13
13
|
|
|
14
|
+
// Core abstractions
|
|
15
|
+
const { LogCollector, LogParser, CommandRouter, CorrelationStrategy } = require('./core/index.js');
|
|
16
|
+
|
|
17
|
+
// Collectors
|
|
18
|
+
const { DockerLogCollector } = require('./collectors/index.js');
|
|
19
|
+
|
|
20
|
+
// Parsers
|
|
21
|
+
const { MinecraftLogParser } = require('./parsers/index.js');
|
|
22
|
+
|
|
23
|
+
// Monitoring
|
|
24
|
+
const { CircularBuffer, LogMonitor, TagCorrelationStrategy, UsernameCorrelationStrategy } = require('./monitoring/index.js');
|
|
25
|
+
|
|
26
|
+
// Helpers
|
|
27
|
+
const { QueryHelper, EventObserver } = require('./helpers/index.js');
|
|
28
|
+
|
|
29
|
+
// Errors
|
|
30
|
+
const {
|
|
31
|
+
PilafError,
|
|
32
|
+
ConnectionError,
|
|
33
|
+
RconConnectionError,
|
|
34
|
+
DockerConnectionError,
|
|
35
|
+
FileAccessError,
|
|
36
|
+
CommandExecutionError,
|
|
37
|
+
CommandTimeoutError,
|
|
38
|
+
CommandRejectedError,
|
|
39
|
+
ParseError,
|
|
40
|
+
MalformedLogError,
|
|
41
|
+
UnknownPatternError,
|
|
42
|
+
CorrelationError,
|
|
43
|
+
ResponseTimeoutError,
|
|
44
|
+
AmbiguousMatchError,
|
|
45
|
+
ResourceError,
|
|
46
|
+
BufferOverflowError,
|
|
47
|
+
HandleExhaustedError
|
|
48
|
+
} = require('./errors/index.js');
|
|
49
|
+
|
|
14
50
|
module.exports = {
|
|
15
51
|
PilafBackend,
|
|
16
52
|
RconBackend,
|
|
@@ -19,5 +55,40 @@ module.exports = {
|
|
|
19
55
|
ConnectionState,
|
|
20
56
|
BotLifecycleManager,
|
|
21
57
|
ServerHealthChecker,
|
|
22
|
-
BotPool
|
|
58
|
+
BotPool,
|
|
59
|
+
// Core abstractions
|
|
60
|
+
LogCollector,
|
|
61
|
+
LogParser,
|
|
62
|
+
CommandRouter,
|
|
63
|
+
CorrelationStrategy,
|
|
64
|
+
// Collectors
|
|
65
|
+
DockerLogCollector,
|
|
66
|
+
// Parsers
|
|
67
|
+
MinecraftLogParser,
|
|
68
|
+
// Monitoring
|
|
69
|
+
CircularBuffer,
|
|
70
|
+
LogMonitor,
|
|
71
|
+
TagCorrelationStrategy,
|
|
72
|
+
UsernameCorrelationStrategy,
|
|
73
|
+
// Helpers
|
|
74
|
+
QueryHelper,
|
|
75
|
+
EventObserver,
|
|
76
|
+
// Errors
|
|
77
|
+
PilafError,
|
|
78
|
+
ConnectionError,
|
|
79
|
+
RconConnectionError,
|
|
80
|
+
DockerConnectionError,
|
|
81
|
+
FileAccessError,
|
|
82
|
+
CommandExecutionError,
|
|
83
|
+
CommandTimeoutError,
|
|
84
|
+
CommandRejectedError,
|
|
85
|
+
ParseError,
|
|
86
|
+
MalformedLogError,
|
|
87
|
+
UnknownPatternError,
|
|
88
|
+
CorrelationError,
|
|
89
|
+
ResponseTimeoutError,
|
|
90
|
+
AmbiguousMatchError,
|
|
91
|
+
ResourceError,
|
|
92
|
+
BufferOverflowError,
|
|
93
|
+
HandleExhaustedError
|
|
23
94
|
};
|