minercon 3.0.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +439 -0
  2. package/LICENSE +22 -0
  3. package/README.md +401 -0
  4. package/images/icon.png +0 -0
  5. package/out/ansi.js +123 -0
  6. package/out/argumentHint.js +43 -0
  7. package/out/bukkitHelpParsing.js +62 -0
  8. package/out/cli.js +253 -0
  9. package/out/cliConfig.js +141 -0
  10. package/out/commandLine.js +28 -0
  11. package/out/commandSuggestions.js +202 -0
  12. package/out/commandTree.js +46 -0
  13. package/out/commandTreeCache.js +171 -0
  14. package/out/commandTreeCrawler.js +583 -0
  15. package/out/commandTreeParsingBrigadier.js +426 -0
  16. package/out/commandTreeParsingBukkit.js +116 -0
  17. package/out/commandTreeSuggestions.js +142 -0
  18. package/out/completionBackend.js +94 -0
  19. package/out/completionEngine.js +376 -0
  20. package/out/completionQueries.js +86 -0
  21. package/out/completionsBackend.js +97 -0
  22. package/out/connectionManager.js +209 -0
  23. package/out/displayArgumentHint.js +43 -0
  24. package/out/displayCommandTree.js +115 -0
  25. package/out/displaySuggestion.js +282 -0
  26. package/out/extension.js +190 -0
  27. package/out/helpTextParsing.js +445 -0
  28. package/out/historySearch.js +46 -0
  29. package/out/historyStore.js +126 -0
  30. package/out/lineEditor.js +525 -0
  31. package/out/localCommandTree.js +541 -0
  32. package/out/logger.js +14 -0
  33. package/out/minercon +253 -0
  34. package/out/pager.js +168 -0
  35. package/out/pagination.js +142 -0
  36. package/out/rconClient.js +97 -0
  37. package/out/rconConnectionManager.js +238 -0
  38. package/out/rconProtocol.js +421 -0
  39. package/out/rconSession.js +920 -0
  40. package/out/rconTerminal.js +80 -0
  41. package/out/suggestionDisplay.js +286 -0
  42. package/out/terminalOutput.js +110 -0
  43. package/out/unpaginate.js +30 -0
  44. package/package.json +138 -0
@@ -0,0 +1,238 @@
1
+ "use strict";
2
+ // src/rconConnectionManager.ts
3
+ //
4
+ // Owns the RCON connection lifecycle: the live `RconController` (recreated on
5
+ // each reconnect attempt), connection/reconnection status, and the
6
+ // exponential-backoff retry loop. Pulled out of RconTerminal as part of the
7
+ // mega-module split — see lineEditor.ts / displaySuggestion.ts for the sibling
8
+ // extractions and their rationale.
9
+ //
10
+ // `RconSession` keeps `detectAndInitialize`/`initializeCommands` (they're
11
+ // about the autocomplete command-tree and `pluginMode`, a different concern
12
+ // that happens to run at similar times) but reads connection status and reaches
13
+ // the live controller through this class, and is notified via `onReconnected`
14
+ // when it should reload the command tree.
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.RconConnectionManager = void 0;
50
+ const rconClient_1 = require("./rconClient");
51
+ const logger_1 = require("./logger");
52
+ const ansi = __importStar(require("./ansi"));
53
+ const defaultControllerFactory = (host, port, password, logger) => new rconClient_1.RconController(host, port, password, logger);
54
+ // Auto-reconnect timing. After a dropped connection we wait a short grace
55
+ // period, then retry with exponential backoff (doubling each attempt) capped
56
+ // at the max delay.
57
+ const CONNECTION_LOST_GRACE_MS = 1000;
58
+ const INITIAL_RECONNECT_DELAY_MS = 2000;
59
+ const MAX_RECONNECT_DELAY_MS = 32000;
60
+ const MAX_RECONNECT_ATTEMPTS = 5;
61
+ class RconConnectionManager {
62
+ serverHost;
63
+ serverPort;
64
+ password;
65
+ logger;
66
+ host;
67
+ controllerFactory;
68
+ _controller;
69
+ // Intent-level connection state: whether the session currently considers
70
+ // itself connected, driven by the lifecycle transitions here (initial
71
+ // connect, `disconnect`, `reportConnectionLost`, successful reconnect). This
72
+ // is what the UI/prompt reads, and is deliberately distinct from the live
73
+ // socket truth in `RconController.isConnected()` — the two can briefly differ
74
+ // mid-reconnect (e.g. this is `false` while a new socket is being dialed).
75
+ _isConnected = true;
76
+ _isReconnecting = false;
77
+ reconnectAttempts = 0;
78
+ reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
79
+ reconnectTimeout = null;
80
+ constructor(serverHost, serverPort, password, logger, controller, host, controllerFactory = defaultControllerFactory) {
81
+ this.serverHost = serverHost;
82
+ this.serverPort = serverPort;
83
+ this.password = password;
84
+ this.logger = logger;
85
+ this.host = host;
86
+ this.controllerFactory = controllerFactory;
87
+ this._controller = controller;
88
+ this.wireCloseHandler(this._controller);
89
+ }
90
+ get controller() {
91
+ return this._controller;
92
+ }
93
+ get isConnected() {
94
+ return this._isConnected;
95
+ }
96
+ get isReconnecting() {
97
+ return this._isReconnecting;
98
+ }
99
+ /**
100
+ * Subscribes to unexpected-close notifications from the given controller.
101
+ * The handler fires when the socket drops without being triggered by an
102
+ * in-flight command — i.e. a silent socket timeout or server-side close.
103
+ * The guard conditions prevent double-triggering when the close is the
104
+ * result of a deliberate disconnect() or an in-progress reconnect attempt.
105
+ */
106
+ wireCloseHandler(controller) {
107
+ controller.setUnexpectedCloseHandler(() => {
108
+ if (this._isConnected && !this._isReconnecting) {
109
+ this.host.write(ansi.yellow('⚠ Connection lost. Auto-reconnecting...') + '\r\n');
110
+ this.reportConnectionLost();
111
+ }
112
+ });
113
+ }
114
+ /** Resets the reconnect-attempt counter, backoff delay, and any pending reconnect timer back to their initial state. */
115
+ resetReconnectState() {
116
+ this.reconnectAttempts = 0;
117
+ this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
118
+ if (this.reconnectTimeout) {
119
+ clearTimeout(this.reconnectTimeout);
120
+ this.reconnectTimeout = null;
121
+ }
122
+ }
123
+ /**
124
+ * Records that an in-flight command discovered the connection is gone, and
125
+ * schedules an auto-reconnect attempt shortly after. The "Connection
126
+ * lost..." message itself stays with the caller (`executeCommand` accounts
127
+ * for it in its output-line bookkeeping).
128
+ */
129
+ reportConnectionLost() {
130
+ this._isConnected = false;
131
+ this.resetReconnectState();
132
+ this.reconnectTimeout = setTimeout(() => {
133
+ this.reconnectTimeout = null;
134
+ this.attemptReconnect();
135
+ }, CONNECTION_LOST_GRACE_MS);
136
+ }
137
+ disconnect() {
138
+ // No key-chord echo here: this runs for the typed /disconnect built-in.
139
+ // Ctrl+D echoes its own ^D in RconSession.handleCtrlD before closing.
140
+ this.host.write('Disconnecting...\r\n');
141
+ // Clear any pending reconnect
142
+ if (this.reconnectTimeout) {
143
+ clearTimeout(this.reconnectTimeout);
144
+ this.reconnectTimeout = null;
145
+ }
146
+ try {
147
+ this._controller.disconnect();
148
+ }
149
+ catch (err) {
150
+ this.logger.error(`Error during disconnect: ${err}`);
151
+ }
152
+ this._isConnected = false;
153
+ this._isReconnecting = false;
154
+ this.host.write('Connection closed. Type ' + ansi.yellow('.reconnect') + ' to reconnect.\r\n\r\n');
155
+ this.host.showPrompt();
156
+ }
157
+ async manualReconnect() {
158
+ if (this._isReconnecting) {
159
+ this.host.write(ansi.yellow('Already reconnecting...') + '\r\n\r\n');
160
+ this.host.showPrompt();
161
+ return;
162
+ }
163
+ this.resetReconnectState();
164
+ await this.attemptReconnect();
165
+ }
166
+ async attemptReconnect() {
167
+ if (this._isReconnecting) {
168
+ return;
169
+ }
170
+ if (this._isConnected) {
171
+ this.host.write(ansi.green('Already connected.') + '\r\n\r\n');
172
+ this.host.showPrompt();
173
+ return;
174
+ }
175
+ this._isReconnecting = true;
176
+ this.reconnectAttempts++;
177
+ const attemptText = this.reconnectAttempts > 1 ? ` (attempt ${this.reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})` : '';
178
+ this.host.write(ansi.yellow('Reconnecting to ' + this.serverHost + ':' + this.serverPort + attemptText + '...') + '\r\n');
179
+ try {
180
+ // Disconnect existing controller
181
+ try {
182
+ await this._controller.disconnect();
183
+ }
184
+ catch (err) {
185
+ // Ignore disconnect errors during reconnect
186
+ }
187
+ // Create new controller
188
+ this._controller = this.controllerFactory(this.serverHost, this.serverPort, this.password, this.logger);
189
+ this.wireCloseHandler(this._controller);
190
+ await this._controller.connect();
191
+ this._isConnected = true;
192
+ this._isReconnecting = false;
193
+ this.resetReconnectState();
194
+ this.host.write(ansi.boldGreen('✓ Reconnected successfully!') + '\r\n\r\n');
195
+ // Reload commands after reconnection
196
+ this.host.onReconnected();
197
+ }
198
+ catch (err) {
199
+ this._isReconnecting = false;
200
+ if (this.reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
201
+ this.host.write(ansi.red('✗ Connection failed: ' + (0, logger_1.errorMessage)(err)) + '\r\n');
202
+ this.host.write(ansi.yellow('Retrying in ' + (this.reconnectDelay / 1000) + ' seconds...') + '\r\n');
203
+ // Clear any existing timeout
204
+ if (this.reconnectTimeout) {
205
+ clearTimeout(this.reconnectTimeout);
206
+ }
207
+ this.reconnectTimeout = setTimeout(() => {
208
+ this.reconnectTimeout = null;
209
+ this.attemptReconnect();
210
+ }, this.reconnectDelay);
211
+ // Exponential backoff with max delay
212
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_DELAY_MS);
213
+ }
214
+ else {
215
+ // Max attempts reached
216
+ this.host.write(ansi.boldRed('✗ Reconnection failed after ' + MAX_RECONNECT_ATTEMPTS + ' attempts.') + '\r\n');
217
+ this.host.write('Type ' + ansi.yellow('.reconnect') + ' to try again.\r\n\r\n');
218
+ this.resetReconnectState();
219
+ this.host.showPrompt();
220
+ }
221
+ }
222
+ }
223
+ /** Tears down any pending reconnect timer and disconnects the controller — used by `RconSession.close()`. */
224
+ dispose() {
225
+ if (this.reconnectTimeout) {
226
+ clearTimeout(this.reconnectTimeout);
227
+ this.reconnectTimeout = null;
228
+ }
229
+ try {
230
+ this._controller.disconnect();
231
+ }
232
+ catch (err) {
233
+ this.logger.error(`Error during close: ${err}`);
234
+ }
235
+ }
236
+ }
237
+ exports.RconConnectionManager = RconConnectionManager;
238
+ //# sourceMappingURL=rconConnectionManager.js.map
@@ -0,0 +1,421 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.RconProtocol = void 0;
37
+ // src/rconProtocol.ts
38
+ const net = __importStar(require("net"));
39
+ const events_1 = require("events");
40
+ // RCON packet type tags. The wire protocol reuses the value 2 for two
41
+ // different things depending on direction (SERVERDATA_EXECCOMMAND when we send,
42
+ // SERVERDATA_AUTH_RESPONSE when the server replies), so they live in separate
43
+ // enums: nothing in this file ever compares an outgoing tag to an incoming one,
44
+ // which keeps that shared value from being mistaken for a single meaning.
45
+ var OutgoingType;
46
+ (function (OutgoingType) {
47
+ OutgoingType[OutgoingType["AUTH"] = 3] = "AUTH";
48
+ OutgoingType[OutgoingType["COMMAND"] = 2] = "COMMAND";
49
+ })(OutgoingType || (OutgoingType = {}));
50
+ var IncomingType;
51
+ (function (IncomingType) {
52
+ IncomingType[IncomingType["RESPONSE"] = 0] = "RESPONSE";
53
+ IncomingType[IncomingType["AUTH_RESPONSE"] = 2] = "AUTH_RESPONSE";
54
+ })(IncomingType || (IncomingType = {}));
55
+ class RconProtocol extends events_1.EventEmitter {
56
+ socket = null;
57
+ host;
58
+ port;
59
+ password;
60
+ logger;
61
+ createSocket;
62
+ authenticated = false;
63
+ requestId = 0;
64
+ responseBuffer = Buffer.alloc(0);
65
+ // For tracking requests and responses
66
+ pendingRequests = new Map();
67
+ // Configuration
68
+ RESPONSE_TIMEOUT = 10000; // 10 seconds for command responses
69
+ AUTH_TIMEOUT = 5000; // 5 seconds for the login handshake
70
+ constructor(host, port, password, logger,
71
+ // Defaults to a real socket; tests substitute a `SocketLike` fake here
72
+ // (record/replay harness — see src/test/rconProtocol.test.ts) so the
73
+ // wire protocol can be exercised without a live server.
74
+ createSocket = () => new net.Socket()) {
75
+ super();
76
+ this.host = host;
77
+ this.port = port;
78
+ this.password = password;
79
+ this.logger = logger;
80
+ this.createSocket = createSocket;
81
+ }
82
+ /**
83
+ * Connect to the RCON server
84
+ */
85
+ async connect() {
86
+ return new Promise((resolve, reject) => {
87
+ this.socket = this.createSocket();
88
+ // Enable TCP keepalive to prevent idle connection drops
89
+ // This sends periodic probes to keep the connection alive through NAT/firewalls
90
+ this.socket.setKeepAlive(true, 60000); // Send keepalive probes every 60 seconds
91
+ // Don't set a socket timeout - let the connection stay open indefinitely
92
+ // The keepalive will handle detecting dead connections
93
+ // Handle connection
94
+ this.socket.once('connect', async () => {
95
+ this.logger.info(`Connected to ${this.host}:${this.port}`);
96
+ try {
97
+ await this.authenticate();
98
+ resolve();
99
+ }
100
+ catch (error) {
101
+ reject(error);
102
+ }
103
+ });
104
+ // Handle data
105
+ this.socket.on('data', (data) => {
106
+ this.handleData(data);
107
+ });
108
+ // Handle errors
109
+ this.socket.on('error', (error) => {
110
+ this.logger.error(`Socket error: ${error.message}`);
111
+ this.emit('error', error);
112
+ reject(error);
113
+ });
114
+ // Handle timeout (shouldn't happen now that we removed setTimeout)
115
+ this.socket.on('timeout', () => {
116
+ this.logger.warn('Socket timeout — calling disconnect()');
117
+ const error = new Error('Connection timeout');
118
+ this.emit('error', error);
119
+ this.disconnect();
120
+ });
121
+ // Handle close
122
+ this.socket.on('close', () => {
123
+ const pending = [...this.pendingRequests.values()]
124
+ .map(r => r.kind === 'command' ? r.command : r.kind)
125
+ .join(', ');
126
+ this.logger.info(`Connection closed (pending: ${pending || 'none'})`);
127
+ this.authenticated = false;
128
+ this.emit('close');
129
+ for (const request of this.pendingRequests.values()) {
130
+ if (request.kind !== 'fence') {
131
+ clearTimeout(request.timeout);
132
+ }
133
+ // Deliver any accumulated fragments if the server closed mid-response
134
+ // (e.g. the final fragment was exactly one full packet and we were
135
+ // still waiting for more).
136
+ if (request.kind === 'command' && request.fragments.length > 0) {
137
+ request.resolve(request.fragments.join(''));
138
+ }
139
+ else {
140
+ request.reject(new Error('Connection closed'));
141
+ }
142
+ }
143
+ this.pendingRequests.clear();
144
+ });
145
+ // Connect
146
+ this.socket.connect(this.port, this.host);
147
+ });
148
+ }
149
+ /**
150
+ * Authenticate with the RCON server
151
+ */
152
+ async authenticate() {
153
+ if (!this.socket) {
154
+ throw new Error('Not connected');
155
+ }
156
+ return new Promise((resolve, reject) => {
157
+ const authId = this.getNextRequestId();
158
+ // Set up auth response handler
159
+ const authTimeout = setTimeout(() => {
160
+ this.pendingRequests.delete(authId);
161
+ reject(new Error('Authentication timeout'));
162
+ }, this.AUTH_TIMEOUT);
163
+ this.pendingRequests.set(authId, {
164
+ kind: 'auth',
165
+ resolve: () => {
166
+ clearTimeout(authTimeout);
167
+ this.authenticated = true;
168
+ this.logger.info('Authentication successful');
169
+ resolve();
170
+ },
171
+ reject: (error) => {
172
+ clearTimeout(authTimeout);
173
+ reject(error);
174
+ },
175
+ timeout: authTimeout
176
+ });
177
+ // Send auth packet
178
+ const packet = this.createPacket(authId, OutgoingType.AUTH, this.password);
179
+ if (this.socket) {
180
+ this.socket.write(packet);
181
+ }
182
+ });
183
+ }
184
+ /**
185
+ * Send a command to the server
186
+ */
187
+ async send(command) {
188
+ if (!this.socket || !this.authenticated) {
189
+ throw new Error('Not connected or authenticated');
190
+ }
191
+ // Use the double-packet technique for detecting end of fragmented
192
+ // responses: send the real command, then — once the first response
193
+ // fragment arrives — send an empty "fence" command. RCON processes
194
+ // commands in order, so when the fence response arrives all real
195
+ // fragments have already been received. Sending the fence only after
196
+ // the first fragment (via sendFence below) ensures the server has
197
+ // started replying before it sees the fence, which prevents servers
198
+ // from treating two back-to-back packets with no intervening response
199
+ // as a protocol error and closing the connection.
200
+ return new Promise((resolve, reject) => {
201
+ const requestId = this.getNextRequestId();
202
+ const dummyId = this.getNextRequestId();
203
+ const timeout = setTimeout(() => {
204
+ this.pendingRequests.delete(requestId);
205
+ this.pendingRequests.delete(dummyId);
206
+ reject(new Error(`Command timeout: ${command}`));
207
+ }, this.RESPONSE_TIMEOUT);
208
+ this.pendingRequests.set(requestId, {
209
+ kind: 'command',
210
+ command: command,
211
+ resolve: (response) => {
212
+ clearTimeout(timeout);
213
+ this.pendingRequests.delete(requestId);
214
+ this.pendingRequests.delete(dummyId);
215
+ resolve(response);
216
+ },
217
+ reject: (error) => {
218
+ clearTimeout(timeout);
219
+ this.pendingRequests.delete(requestId);
220
+ this.pendingRequests.delete(dummyId);
221
+ reject(error);
222
+ },
223
+ timeout: timeout,
224
+ fragments: [],
225
+ // Sent on the first response fragment — ensures the fence arrives in
226
+ // its own TCP read() on the server, not batched with the command packet.
227
+ // RconClient.run() closes the connection if read() returns more bytes
228
+ // than the first packet's length field, so back-to-back writes that
229
+ // arrive in one read() cause an immediate disconnect.
230
+ sendFence: () => {
231
+ if (this.socket) {
232
+ const dummyPacket = this.createPacket(dummyId, OutgoingType.COMMAND, '');
233
+ this.socket.write(dummyPacket);
234
+ }
235
+ },
236
+ });
237
+ this.pendingRequests.set(dummyId, {
238
+ kind: 'fence',
239
+ resolve: () => {
240
+ const mainRequest = this.pendingRequests.get(requestId);
241
+ if (mainRequest && mainRequest.kind === 'command') {
242
+ mainRequest.resolve(mainRequest.fragments.join(''));
243
+ }
244
+ },
245
+ reject: () => { },
246
+ });
247
+ const commandPacket = this.createPacket(requestId, OutgoingType.COMMAND, command);
248
+ if (this.socket) {
249
+ this.socket.write(commandPacket);
250
+ }
251
+ });
252
+ }
253
+ /**
254
+ * Handle incoming data from the socket
255
+ */
256
+ handleData(data) {
257
+ // Append to buffer
258
+ this.responseBuffer = Buffer.concat([this.responseBuffer, data]);
259
+ // Process complete packets
260
+ while (this.responseBuffer.length >= 4) {
261
+ // Read packet size (first 4 bytes, little-endian)
262
+ const size = this.responseBuffer.readInt32LE(0);
263
+ // Check if we have the complete packet
264
+ if (this.responseBuffer.length < size + 4) {
265
+ // Wait for more data
266
+ break;
267
+ }
268
+ // Extract the packet
269
+ const packetBuffer = this.responseBuffer.subarray(0, size + 4);
270
+ this.responseBuffer = this.responseBuffer.subarray(size + 4);
271
+ // Parse the packet
272
+ try {
273
+ const packet = this.parsePacket(packetBuffer);
274
+ this.handlePacket(packet);
275
+ }
276
+ catch (error) {
277
+ this.logger.error(`Error parsing packet: ${error}`);
278
+ }
279
+ }
280
+ }
281
+ /** The single in-flight auth request, if any, with the id it's keyed under. */
282
+ findAuthRequest() {
283
+ for (const [id, request] of this.pendingRequests) {
284
+ if (request.kind === 'auth') {
285
+ return { id, request };
286
+ }
287
+ }
288
+ return undefined;
289
+ }
290
+ /**
291
+ * Handle a parsed packet
292
+ */
293
+ handlePacket(packet) {
294
+ // Failed auth: the server replies with id -1 rather than echoing the id we
295
+ // sent, so we can't look it up — find the pending auth request directly.
296
+ if (packet.id === -1) {
297
+ const auth = this.findAuthRequest();
298
+ if (auth) {
299
+ this.pendingRequests.delete(auth.id);
300
+ auth.request.reject(new Error('Authentication failed'));
301
+ }
302
+ return;
303
+ }
304
+ const request = this.pendingRequests.get(packet.id);
305
+ if (!request) {
306
+ // Some servers don't echo our auth id on the AUTH_RESPONSE packet — if an
307
+ // auth handshake is in flight, this unmatched response is it.
308
+ if (packet.type === IncomingType.AUTH_RESPONSE) {
309
+ const auth = this.findAuthRequest();
310
+ if (auth) {
311
+ this.pendingRequests.delete(auth.id);
312
+ auth.request.resolve();
313
+ return;
314
+ }
315
+ }
316
+ this.logger.warn(`Received packet with unknown request ID: ${packet.id}`);
317
+ return;
318
+ }
319
+ switch (request.kind) {
320
+ case 'auth':
321
+ // Auth gets two packets: an empty RESPONSE_VALUE we ignore, then the
322
+ // AUTH_RESPONSE we resolve on. Delete on resolve so the entry doesn't
323
+ // linger where the close handler could re-fire it.
324
+ if (packet.type === IncomingType.AUTH_RESPONSE) {
325
+ this.pendingRequests.delete(packet.id);
326
+ request.resolve();
327
+ }
328
+ return;
329
+ case 'command':
330
+ if (packet.type !== IncomingType.RESPONSE) {
331
+ return;
332
+ }
333
+ // First fragment in: send the fence so its reply can mark the end of
334
+ // this (possibly multi-packet) response.
335
+ if (request.sendFence) {
336
+ request.sendFence();
337
+ request.sendFence = undefined;
338
+ }
339
+ request.fragments.push(packet.body);
340
+ return;
341
+ case 'fence':
342
+ // The fence's reply means every fragment of the command it follows has
343
+ // arrived; its resolve settles that command (and clears both entries).
344
+ if (packet.type !== IncomingType.RESPONSE) {
345
+ return;
346
+ }
347
+ request.resolve();
348
+ return;
349
+ }
350
+ }
351
+ /**
352
+ * Create an RCON packet
353
+ */
354
+ createPacket(id, type, body) {
355
+ // Calculate size (4 bytes ID + 4 bytes type + body + 2 null terminators)
356
+ const bodyLength = Buffer.byteLength(body, 'utf8');
357
+ const size = 4 + 4 + bodyLength + 2;
358
+ // Create buffer (size field + packet content)
359
+ const buffer = Buffer.alloc(4 + size);
360
+ // Write size (little-endian)
361
+ buffer.writeInt32LE(size, 0);
362
+ // Write ID (little-endian)
363
+ buffer.writeInt32LE(id, 4);
364
+ // Write type (little-endian)
365
+ buffer.writeInt32LE(type, 8);
366
+ // Write body
367
+ buffer.write(body, 12, bodyLength, 'utf8');
368
+ // Null terminators are already 0 from Buffer.alloc
369
+ return buffer;
370
+ }
371
+ /**
372
+ * Parse a packet from a buffer
373
+ */
374
+ parsePacket(buffer) {
375
+ if (buffer.length < 14) {
376
+ throw new Error('Packet too small');
377
+ }
378
+ const size = buffer.readInt32LE(0);
379
+ const id = buffer.readInt32LE(4);
380
+ const type = buffer.readInt32LE(8);
381
+ // Read body (from byte 12 to size + 2, excluding null terminators)
382
+ const bodyEnd = Math.min(12 + size - 10, buffer.length - 2);
383
+ const body = buffer.toString('utf8', 12, bodyEnd);
384
+ return { size, id, type, body };
385
+ }
386
+ /**
387
+ * Get the next request ID
388
+ */
389
+ getNextRequestId() {
390
+ return ++this.requestId;
391
+ }
392
+ /**
393
+ * Disconnect from the server
394
+ */
395
+ async disconnect() {
396
+ if (this.socket) {
397
+ this.authenticated = false;
398
+ // Clear pending requests
399
+ for (const [, request] of this.pendingRequests) {
400
+ if (request.kind !== 'fence') {
401
+ clearTimeout(request.timeout);
402
+ }
403
+ request.reject(new Error('Disconnected'));
404
+ }
405
+ this.pendingRequests.clear();
406
+ // Close socket
407
+ this.socket.destroy();
408
+ this.socket = null;
409
+ }
410
+ }
411
+ /**
412
+ * The lowest-level connection truth: an open socket that has authenticated.
413
+ * `RconController.isConnected()` forwards to this; `RconConnectionManager`
414
+ * keeps a separate intent-level flag that can briefly diverge during reconnects.
415
+ */
416
+ isConnected() {
417
+ return this.socket !== null && this.authenticated;
418
+ }
419
+ }
420
+ exports.RconProtocol = RconProtocol;
421
+ //# sourceMappingURL=rconProtocol.js.map