@dimzxzzx07/mc-headless 2.2.1 → 2.2.3

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 (35) hide show
  1. package/README.md +658 -765
  2. package/dist/core/JavaChecker.d.ts +8 -2
  3. package/dist/core/JavaChecker.d.ts.map +1 -1
  4. package/dist/core/JavaChecker.js +219 -104
  5. package/dist/core/JavaChecker.js.map +1 -1
  6. package/dist/core/MinecraftServer.d.ts +17 -32
  7. package/dist/core/MinecraftServer.d.ts.map +1 -1
  8. package/dist/core/MinecraftServer.js +449 -186
  9. package/dist/core/MinecraftServer.js.map +1 -1
  10. package/dist/index.d.ts +19 -18
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +40 -18
  13. package/dist/index.js.map +1 -1
  14. package/dist/platforms/GeyserBridge.d.ts +21 -3
  15. package/dist/platforms/GeyserBridge.d.ts.map +1 -1
  16. package/dist/platforms/GeyserBridge.js +160 -60
  17. package/dist/platforms/GeyserBridge.js.map +1 -1
  18. package/dist/platforms/PluginManager.d.ts +16 -0
  19. package/dist/platforms/PluginManager.d.ts.map +1 -0
  20. package/dist/platforms/PluginManager.js +208 -0
  21. package/dist/platforms/PluginManager.js.map +1 -0
  22. package/dist/platforms/ViaVersion.d.ts +1 -7
  23. package/dist/platforms/ViaVersion.d.ts.map +1 -1
  24. package/dist/platforms/ViaVersion.js +23 -87
  25. package/dist/platforms/ViaVersion.js.map +1 -1
  26. package/dist/types/index.d.ts +189 -0
  27. package/dist/types/index.d.ts.map +1 -1
  28. package/package.json +4 -2
  29. package/src/core/JavaChecker.ts +224 -108
  30. package/src/core/MinecraftServer.ts +540 -254
  31. package/src/index.ts +19 -19
  32. package/src/platforms/GeyserBridge.ts +196 -80
  33. package/src/platforms/PluginManager.ts +206 -0
  34. package/src/platforms/ViaVersion.ts +26 -107
  35. package/src/types/index.ts +206 -0
@@ -37,6 +37,8 @@ exports.MinecraftServer = void 0;
37
37
  const events_1 = require("events");
38
38
  const child_process_1 = require("child_process");
39
39
  const cron = __importStar(require("node-cron"));
40
+ const path = __importStar(require("path"));
41
+ const fs = __importStar(require("fs-extra"));
40
42
  const ConfigHandler_1 = require("./ConfigHandler");
41
43
  const JavaChecker_1 = require("./JavaChecker");
42
44
  const FileUtils_1 = require("../utils/FileUtils");
@@ -49,8 +51,7 @@ const FabricEngine_1 = require("../engines/FabricEngine");
49
51
  const GeyserBridge_1 = require("../platforms/GeyserBridge");
50
52
  const ViaVersion_1 = require("../platforms/ViaVersion");
51
53
  const SkinRestorer_1 = require("../platforms/SkinRestorer");
52
- const path = __importStar(require("path"));
53
- const fs = __importStar(require("fs-extra"));
54
+ const PluginManager_1 = require("../platforms/PluginManager");
54
55
  class MinecraftServer extends events_1.EventEmitter {
55
56
  config;
56
57
  options;
@@ -59,10 +60,13 @@ class MinecraftServer extends events_1.EventEmitter {
59
60
  geyser;
60
61
  viaVersion;
61
62
  skinRestorer;
63
+ pluginManager;
62
64
  process = null;
63
65
  serverInfo;
64
66
  players = new Map();
65
67
  backupCron = null;
68
+ cleanLogsCron = null;
69
+ ramdiskBackupCron = null;
66
70
  startTime = null;
67
71
  memoryMonitorInterval = null;
68
72
  statsInterval = null;
@@ -78,46 +82,90 @@ class MinecraftServer extends events_1.EventEmitter {
78
82
  cpuUsage = 0;
79
83
  cgroupMemory = 0;
80
84
  cgroupCpu = 0;
85
+ isPterodactyl = false;
86
+ ramdiskPaths = new Map();
87
+ serverReady = false;
81
88
  constructor(userConfig = {}) {
82
89
  super();
83
90
  this.logger = Logger_1.Logger.getInstance();
84
91
  this.logger.banner();
92
+ this.detectEnvironment();
93
+ const defaultMemoryMonitor = {
94
+ enabled: true,
95
+ threshold: 90,
96
+ interval: 30000,
97
+ action: 'warn',
98
+ maxMemoryLeakRestarts: 3,
99
+ gcOnWarning: true
100
+ };
101
+ const defaultNetworkOptimization = {
102
+ tcpFastOpen: true,
103
+ tcpNoDelay: true,
104
+ preferIPv4: true,
105
+ compressionThreshold: 64,
106
+ bungeeMode: false,
107
+ proxyProtocol: false,
108
+ velocity: {
109
+ enabled: false,
110
+ secret: '',
111
+ forwardingMode: 'modern'
112
+ }
113
+ };
114
+ const defaultOwnerCommands = {
115
+ prefix: '!',
116
+ enabled: true,
117
+ usernames: [],
118
+ permissions: []
119
+ };
120
+ const defaultRamdisk = {
121
+ enabled: false,
122
+ world: false,
123
+ plugins: false,
124
+ addons: false,
125
+ backupInterval: 300000,
126
+ masterStorage: '/home/minecraft-master',
127
+ size: '1G',
128
+ mountPoint: '/dev/shm/minecraft'
129
+ };
85
130
  this.options = {
86
131
  javaVersion: 'auto',
87
- usePortableJava: true,
88
- memoryMonitor: {
89
- enabled: true,
90
- threshold: 90,
91
- interval: 30000,
92
- action: 'warn'
93
- },
132
+ usePortableJava: !this.isPterodactyl,
133
+ memoryMonitor: defaultMemoryMonitor,
94
134
  autoInstallJava: true,
95
- networkOptimization: {
96
- tcpFastOpen: true,
97
- bungeeMode: false,
98
- proxyProtocol: false
99
- },
135
+ networkOptimization: defaultNetworkOptimization,
100
136
  owners: [],
101
- ownerCommands: {
102
- prefix: '!',
103
- enabled: true
104
- },
137
+ ownerCommands: defaultOwnerCommands,
105
138
  silentMode: true,
106
139
  statsInterval: 30000,
140
+ cleanLogsInterval: 3 * 60 * 60 * 1000,
141
+ optimizeForPterodactyl: true,
142
+ ramdisk: defaultRamdisk,
143
+ symlinkMaster: '/home/minecraft-master',
144
+ enableProtocolLib: false,
145
+ enableTCPShield: false,
146
+ enableSpigotOptimizers: false,
147
+ enableSpark: false,
148
+ enableViewDistanceTweaks: false,
149
+ enableFarmControl: false,
150
+ enableEntityDetection: false,
107
151
  ...userConfig
108
152
  };
109
153
  if (this.options.owners) {
110
- this.options.owners.forEach(owner => this.owners.add(owner.toLowerCase()));
154
+ this.options.owners.forEach((owner) => this.owners.add(owner.toLowerCase()));
111
155
  }
112
156
  if (this.options.ownerCommands?.prefix) {
113
157
  this.ownerCommandPrefix = this.options.ownerCommands.prefix;
114
158
  }
115
159
  const handler = new ConfigHandler_1.ConfigHandler(userConfig);
116
160
  this.config = handler.getConfig();
161
+ if (this.isPterodactyl && this.options.optimizeForPterodactyl) {
162
+ this.optimizeForPterodactylPanel();
163
+ }
117
164
  this.engine = this.createEngine();
118
165
  this.geyser = new GeyserBridge_1.GeyserBridge();
119
166
  this.viaVersion = new ViaVersion_1.ViaVersionManager();
120
167
  this.skinRestorer = new SkinRestorer_1.SkinRestorerManager();
168
+ this.pluginManager = new PluginManager_1.PluginManager();
121
169
  this.serverInfo = {
122
170
  pid: 0,
123
171
  ip: this.config.network.ip,
@@ -135,6 +183,28 @@ class MinecraftServer extends events_1.EventEmitter {
135
183
  };
136
184
  this.detectCgroupLimits();
137
185
  }
186
+ detectEnvironment() {
187
+ this.isPterodactyl = !!(process.env.PTERODACTYL ||
188
+ process.env.SERVER_PORT ||
189
+ fs.existsSync('/home/container'));
190
+ if (this.isPterodactyl) {
191
+ this.logger.info('Pterodactyl environment detected');
192
+ const serverPort = process.env.SERVER_PORT;
193
+ if (serverPort) {
194
+ this.config.network.port = parseInt(serverPort);
195
+ this.logger.info(`Using Pterodactyl Java port: ${serverPort}`);
196
+ }
197
+ const bedrockPort = process.env.BEDROCK_PORT;
198
+ if (bedrockPort) {
199
+ this.config.network.bedrockPort = parseInt(bedrockPort);
200
+ this.logger.info(`Using Pterodactyl Bedrock port: ${bedrockPort}`);
201
+ }
202
+ else {
203
+ this.logger.warning('No BEDROCK_PORT environment variable found. Geyser will share Java port.');
204
+ this.logger.warning('Make sure UDP is enabled for this port in Pterodactyl panel');
205
+ }
206
+ }
207
+ }
138
208
  detectCgroupLimits() {
139
209
  try {
140
210
  if (fs.existsSync('/sys/fs/cgroup/memory/memory.limit_in_bytes')) {
@@ -155,6 +225,25 @@ class MinecraftServer extends events_1.EventEmitter {
155
225
  this.logger.debug('Not running in cgroup environment');
156
226
  }
157
227
  }
228
+ optimizeForPterodactylPanel() {
229
+ const pterodactylPort = process.env.SERVER_PORT;
230
+ if (pterodactylPort) {
231
+ this.config.network.port = parseInt(pterodactylPort);
232
+ }
233
+ const pterodactylMemory = process.env.SERVER_MEMORY_MAX;
234
+ if (pterodactylMemory) {
235
+ const memValue = parseInt(pterodactylMemory);
236
+ const safeMemory = Math.floor(memValue * 0.9);
237
+ this.config.memory.max = `${safeMemory}M`;
238
+ this.logger.info(`Pterodactyl memory limit detected, using 90% safe value: ${safeMemory}MB`);
239
+ }
240
+ const pterodactylInitMemory = process.env.SERVER_MEMORY_INIT;
241
+ if (pterodactylInitMemory) {
242
+ this.config.memory.init = pterodactylInitMemory;
243
+ }
244
+ this.config.world.viewDistance = 6;
245
+ this.config.world.simulationDistance = 4;
246
+ }
158
247
  createEngine() {
159
248
  switch (this.config.type) {
160
249
  case 'paper':
@@ -171,18 +260,194 @@ class MinecraftServer extends events_1.EventEmitter {
171
260
  throw new Error(`Unsupported server type: ${this.config.type}`);
172
261
  }
173
262
  }
263
+ async setupSymlinkMaster() {
264
+ if (!this.options.symlinkMaster)
265
+ return;
266
+ const masterDir = this.options.symlinkMaster;
267
+ await fs.ensureDir(masterDir);
268
+ const subDirs = ['worlds', 'plugins', 'addons', 'mods'];
269
+ for (const dir of subDirs) {
270
+ const masterSubDir = path.join(masterDir, dir);
271
+ await fs.ensureDir(masterSubDir);
272
+ }
273
+ this.logger.info(`Symlink master directory created at ${masterDir}`);
274
+ }
275
+ async restoreFromMaster() {
276
+ if (!this.options.ramdisk?.enabled)
277
+ return;
278
+ const ramdiskBase = '/dev/shm/minecraft';
279
+ const items = [
280
+ { name: 'world', target: this.config.folders.world },
281
+ { name: 'plugins', target: this.config.folders.plugins },
282
+ { name: 'addons', target: this.config.folders.addons }
283
+ ];
284
+ for (const item of items) {
285
+ const ramdiskPath = path.join(ramdiskBase, item.name);
286
+ const masterPath = path.join(this.options.ramdisk.masterStorage, item.name);
287
+ if (fs.existsSync(ramdiskPath)) {
288
+ const ramdiskFiles = fs.readdirSync(ramdiskPath);
289
+ if (ramdiskFiles.length > 0) {
290
+ this.logger.debug(`RAM disk ${item.name} already has data, skipping restore`);
291
+ continue;
292
+ }
293
+ }
294
+ if (fs.existsSync(masterPath)) {
295
+ const masterFiles = fs.readdirSync(masterPath);
296
+ if (masterFiles.length > 0) {
297
+ this.logger.info(`Restoring ${item.name} from master storage...`);
298
+ await fs.ensureDir(ramdiskPath);
299
+ await fs.copy(masterPath, ramdiskPath, { overwrite: true });
300
+ if (!fs.existsSync(item.target)) {
301
+ await fs.symlink(ramdiskPath, item.target, 'dir');
302
+ }
303
+ }
304
+ }
305
+ }
306
+ }
307
+ async setupRamdisk() {
308
+ if (!this.options.ramdisk?.enabled)
309
+ return;
310
+ const ramdiskBase = '/dev/shm/minecraft';
311
+ await fs.ensureDir(ramdiskBase);
312
+ await this.restoreFromMaster();
313
+ const items = [
314
+ { name: 'world', enabled: this.options.ramdisk.world, target: this.config.folders.world },
315
+ { name: 'plugins', enabled: this.options.ramdisk.plugins, target: this.config.folders.plugins },
316
+ { name: 'addons', enabled: this.options.ramdisk.addons, target: this.config.folders.addons }
317
+ ];
318
+ for (const item of items) {
319
+ if (!item.enabled)
320
+ continue;
321
+ const ramdiskPath = path.join(ramdiskBase, item.name);
322
+ const masterPath = path.join(this.options.ramdisk.masterStorage, item.name);
323
+ await fs.ensureDir(masterPath);
324
+ if (fs.existsSync(item.target)) {
325
+ const stat = await fs.lstat(item.target);
326
+ if (stat.isSymbolicLink()) {
327
+ continue;
328
+ }
329
+ await fs.copy(item.target, masterPath, { overwrite: true });
330
+ await fs.remove(item.target);
331
+ }
332
+ await fs.ensureDir(ramdiskPath);
333
+ await fs.symlink(ramdiskPath, item.target, 'dir');
334
+ this.ramdiskPaths.set(item.name, ramdiskPath);
335
+ this.logger.info(`RAM disk created for ${item.name} at ${ramdiskPath}`);
336
+ }
337
+ if (this.options.ramdisk.backupInterval > 0) {
338
+ const minutes = Math.floor(this.options.ramdisk.backupInterval / 60000);
339
+ this.ramdiskBackupCron = cron.schedule(`*/${minutes} * * * *`, () => {
340
+ this.backupRamdiskToMaster();
341
+ });
342
+ this.logger.info(`RAM disk backup scheduled every ${minutes} minutes`);
343
+ }
344
+ }
345
+ async backupRamdiskToMaster() {
346
+ if (!this.options.ramdisk?.enabled)
347
+ return;
348
+ for (const [name, ramdiskPath] of this.ramdiskPaths) {
349
+ const masterPath = path.join(this.options.ramdisk.masterStorage, name);
350
+ try {
351
+ await fs.copy(ramdiskPath, masterPath, { overwrite: true });
352
+ this.logger.debug(`RAM disk ${name} backed up to master storage`);
353
+ }
354
+ catch (error) {
355
+ const errorMessage = error instanceof Error ? error.message : String(error);
356
+ this.logger.error(`Failed to backup ${name} RAM disk: ${errorMessage}`);
357
+ }
358
+ }
359
+ }
360
+ cleanOldLogs() {
361
+ const logsDir = path.join(process.cwd(), 'logs');
362
+ if (!fs.existsSync(logsDir))
363
+ return;
364
+ try {
365
+ const files = fs.readdirSync(logsDir);
366
+ const now = Date.now();
367
+ const threeHours = 3 * 60 * 60 * 1000;
368
+ files.forEach(file => {
369
+ const filePath = path.join(logsDir, file);
370
+ const stats = fs.statSync(filePath);
371
+ if (now - stats.mtimeMs > threeHours) {
372
+ fs.unlinkSync(filePath);
373
+ this.logger.debug(`Cleaned old log: ${file}`);
374
+ }
375
+ });
376
+ this.logger.info('Old logs cleaned');
377
+ }
378
+ catch (err) {
379
+ const errorMessage = err instanceof Error ? err.message : String(err);
380
+ this.logger.warning(`Failed to clean logs: ${errorMessage}`);
381
+ }
382
+ }
383
+ async calculateWorldSize() {
384
+ try {
385
+ const worldPath = path.join(process.cwd(), this.config.folders.world);
386
+ if (!await fs.pathExists(worldPath))
387
+ return 0;
388
+ const getSize = async (dir) => {
389
+ let total = 0;
390
+ const files = await fs.readdir(dir);
391
+ for (const file of files) {
392
+ const filePath = path.join(dir, file);
393
+ const stat = await fs.stat(filePath);
394
+ if (stat.isDirectory()) {
395
+ total += await getSize(filePath);
396
+ }
397
+ else {
398
+ total += stat.size;
399
+ }
400
+ }
401
+ return total;
402
+ };
403
+ return await getSize(worldPath);
404
+ }
405
+ catch {
406
+ return 0;
407
+ }
408
+ }
174
409
  buildJavaArgs() {
175
410
  if (this.options.customJavaArgs && this.options.customJavaArgs.length > 0) {
176
411
  return this.options.customJavaArgs;
177
412
  }
178
- const memMax = this.parseMemory(this.config.memory.max);
413
+ let memMax = this.parseMemory(this.config.memory.max);
414
+ if (this.isPterodactyl && this.cgroupMemory > 0) {
415
+ const safeMem = Math.floor(this.cgroupMemory * 0.9);
416
+ memMax = Math.min(memMax, safeMem);
417
+ this.logger.info(`Adjusted memory to ${memMax}MB (90% of cgroup limit)`);
418
+ }
179
419
  const javaVersion = this.options.javaVersion || 'auto';
420
+ const baseArgs = [
421
+ `-Xms${this.config.memory.init}`,
422
+ `-Xmx${memMax}M`,
423
+ '-XX:+UseG1GC',
424
+ '-XX:MaxGCPauseMillis=50',
425
+ '-XX:+UseStringDeduplication',
426
+ '-XX:G1MixedGCLiveThresholdPercent=90',
427
+ '-Dio.netty.recycler.maxCapacity.default=0',
428
+ '-Dio.netty.leakDetectionLevel=disabled',
429
+ '-Dterminal.jline=false',
430
+ '-Dterminal.ansi=true'
431
+ ];
432
+ if (this.options.networkOptimization?.preferIPv4) {
433
+ baseArgs.unshift('-Djava.net.preferIPv4Stack=true');
434
+ }
435
+ if (this.options.networkOptimization?.tcpFastOpen) {
436
+ baseArgs.unshift('-Djava.net.tcpFastOpen=true');
437
+ }
438
+ if (this.options.networkOptimization?.tcpNoDelay) {
439
+ baseArgs.unshift('-Djava.net.tcp.nodelay=true');
440
+ }
441
+ if (this.options.networkOptimization?.compressionThreshold) {
442
+ baseArgs.push(`-Dnetwork-compression-threshold=${this.options.networkOptimization.compressionThreshold}`);
443
+ }
444
+ if (this.options.networkOptimization?.velocity?.enabled) {
445
+ baseArgs.push('-Dvelocity.secret=' + this.options.networkOptimization.velocity.secret);
446
+ }
180
447
  let gcArgs = [];
181
448
  if (memMax >= 16384) {
182
449
  gcArgs = [
183
- '-XX:+UseG1GC',
184
450
  '-XX:+ParallelRefProcEnabled',
185
- '-XX:MaxGCPauseMillis=100',
186
451
  '-XX:+UnlockExperimentalVMOptions',
187
452
  '-XX:+DisableExplicitGC',
188
453
  '-XX:+AlwaysPreTouch',
@@ -193,7 +458,6 @@ class MinecraftServer extends events_1.EventEmitter {
193
458
  '-XX:G1HeapWastePercent=5',
194
459
  '-XX:G1MixedGCCountTarget=4',
195
460
  '-XX:InitiatingHeapOccupancyPercent=20',
196
- '-XX:G1MixedGCLiveThresholdPercent=90',
197
461
  '-XX:G1RSetUpdatingPauseTimePercent=5',
198
462
  '-XX:SurvivorRatio=32',
199
463
  '-XX:+PerfDisableSharedMem',
@@ -202,9 +466,7 @@ class MinecraftServer extends events_1.EventEmitter {
202
466
  }
203
467
  else if (memMax >= 8192) {
204
468
  gcArgs = [
205
- '-XX:+UseG1GC',
206
469
  '-XX:+ParallelRefProcEnabled',
207
- '-XX:MaxGCPauseMillis=150',
208
470
  '-XX:+UnlockExperimentalVMOptions',
209
471
  '-XX:+DisableExplicitGC',
210
472
  '-XX:+AlwaysPreTouch',
@@ -215,7 +477,6 @@ class MinecraftServer extends events_1.EventEmitter {
215
477
  '-XX:G1HeapWastePercent=5',
216
478
  '-XX:G1MixedGCCountTarget=4',
217
479
  '-XX:InitiatingHeapOccupancyPercent=15',
218
- '-XX:G1MixedGCLiveThresholdPercent=90',
219
480
  '-XX:G1RSetUpdatingPauseTimePercent=5',
220
481
  '-XX:SurvivorRatio=32',
221
482
  '-XX:+PerfDisableSharedMem',
@@ -223,32 +484,27 @@ class MinecraftServer extends events_1.EventEmitter {
223
484
  ];
224
485
  }
225
486
  else {
226
- gcArgs = this.config.memory.useAikarsFlags ? [
227
- '-XX:+UseG1GC',
487
+ gcArgs = [
228
488
  '-XX:+ParallelRefProcEnabled',
229
- '-XX:MaxGCPauseMillis=200',
230
489
  '-XX:+UnlockExperimentalVMOptions',
231
490
  '-XX:+DisableExplicitGC',
232
491
  '-XX:+AlwaysPreTouch',
233
492
  '-XX:G1HeapWastePercent=5',
234
493
  '-XX:G1MixedGCCountTarget=4',
235
494
  '-XX:InitiatingHeapOccupancyPercent=15',
236
- '-XX:G1MixedGCLiveThresholdPercent=90',
237
495
  '-XX:G1RSetUpdatingPauseTimePercent=5',
238
496
  '-XX:SurvivorRatio=32',
239
497
  '-XX:+PerfDisableSharedMem',
240
- '-XX:MaxTenuringThreshold=1',
241
- '-Dusing.aikars.flags=https://mcflags.emc.gs',
242
- '-Daikars.new.flags=true'
243
- ] : [];
498
+ '-XX:MaxTenuringThreshold=1'
499
+ ];
500
+ if (this.config.memory.useAikarsFlags) {
501
+ gcArgs.push('-Dusing.aikars.flags=https://mcflags.emc.gs');
502
+ gcArgs.push('-Daikars.new.flags=true');
503
+ }
244
504
  }
245
505
  if (javaVersion === '21') {
246
506
  gcArgs.push('--enable-preview');
247
507
  }
248
- const baseArgs = [
249
- `-Xms${this.config.memory.init}`,
250
- `-Xmx${this.config.memory.max}`
251
- ];
252
508
  return [...baseArgs, ...gcArgs];
253
509
  }
254
510
  buildEnvironment() {
@@ -268,6 +524,67 @@ class MinecraftServer extends events_1.EventEmitter {
268
524
  }
269
525
  return env;
270
526
  }
527
+ setupLogHandlers() {
528
+ this.process.stdout.on('data', (data) => {
529
+ const output = data.toString();
530
+ process.stdout.write(output);
531
+ if (!this.serverReady) {
532
+ if (output.includes('Done') || output.includes('For help, type "help"')) {
533
+ this.serverReady = true;
534
+ this.serverInfo.status = 'running';
535
+ this.logger.success('Server started successfully!');
536
+ if (this.options.enableViaVersion !== false) {
537
+ this.logger.info('ViaVersion is active - players from older versions can connect');
538
+ }
539
+ if (this.options.enableSkinRestorer !== false) {
540
+ this.logger.info('SkinRestorer is active - player skins will be restored');
541
+ }
542
+ if (this.worldSize > 0) {
543
+ this.logger.info(`World size: ${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB`);
544
+ }
545
+ if (this.owners.size > 0) {
546
+ this.logger.info(`Owner commands enabled with prefix: ${this.ownerCommandPrefix}`);
547
+ }
548
+ if (this.options.ramdisk?.enabled) {
549
+ this.logger.info('RAM disk active - worlds/plugins running in memory');
550
+ }
551
+ this.emit('ready', this.serverInfo);
552
+ this.startMemoryMonitor();
553
+ }
554
+ }
555
+ if (output.includes('joined the game')) {
556
+ const match = output.match(/(\w+) joined the game/);
557
+ if (match) {
558
+ this.playerCount++;
559
+ this.handlePlayerJoin(match[1]);
560
+ if (this.owners.has(match[1].toLowerCase())) {
561
+ this.sendCommand(`tellraw ${match[1]} {"text":"Welcome Owner! Use ${this.ownerCommandPrefix}help for commands","color":"gold"}`);
562
+ }
563
+ }
564
+ }
565
+ if (output.includes('left the game')) {
566
+ const match = output.match(/(\w+) left the game/);
567
+ if (match) {
568
+ this.playerCount--;
569
+ this.handlePlayerLeave(match[1]);
570
+ }
571
+ }
572
+ if (output.includes('<') && output.includes('>')) {
573
+ const chatMatch = output.match(/<(\w+)>\s+(.+)/);
574
+ if (chatMatch) {
575
+ const player = chatMatch[1];
576
+ const message = chatMatch[2];
577
+ if (message.startsWith(this.ownerCommandPrefix)) {
578
+ const command = message.substring(this.ownerCommandPrefix.length);
579
+ this.processOwnerCommand(player, command);
580
+ }
581
+ }
582
+ }
583
+ });
584
+ this.process.stderr.on('data', (data) => {
585
+ process.stderr.write(data.toString());
586
+ });
587
+ }
271
588
  processOwnerCommand(player, command) {
272
589
  if (!this.options.ownerCommands?.enabled)
273
590
  return;
@@ -533,11 +850,79 @@ class MinecraftServer extends events_1.EventEmitter {
533
850
  this.emit('resource', this.serverInfo);
534
851
  }
535
852
  catch (error) {
536
- this.logger.error('Stats update error:', error);
853
+ const errorMessage = error instanceof Error ? error.message : String(error);
854
+ this.logger.error(`Stats update error: ${errorMessage}`);
537
855
  }
538
856
  }
857
+ startMemoryMonitor() {
858
+ if (!this.options.memoryMonitor?.enabled)
859
+ return;
860
+ const threshold = this.options.memoryMonitor.threshold || 90;
861
+ const interval = this.options.memoryMonitor.interval || 30000;
862
+ const action = this.options.memoryMonitor.action || 'warn';
863
+ this.memoryMonitorInterval = setInterval(async () => {
864
+ if (this.serverInfo.status !== 'running' || !this.process)
865
+ return;
866
+ try {
867
+ await this.updateStats();
868
+ const memPercent = (this.serverInfo.memory.used / this.serverInfo.memory.max) * 100;
869
+ this.memoryUsageHistory.push(memPercent);
870
+ if (this.memoryUsageHistory.length > 10) {
871
+ this.memoryUsageHistory.shift();
872
+ }
873
+ if (memPercent > threshold) {
874
+ this.logger.warning(`High memory usage: ${memPercent.toFixed(1)}%`);
875
+ const isIncreasing = this.memoryUsageHistory.length > 5 &&
876
+ this.memoryUsageHistory[this.memoryUsageHistory.length - 1] >
877
+ this.memoryUsageHistory[0] * 1.2;
878
+ if (isIncreasing) {
879
+ this.logger.warning('Memory leak detected!');
880
+ if (action === 'restart' && this.players.size === 0) {
881
+ this.logger.info('No players online, restarting server due to memory leak...');
882
+ await this.gracefulRestart();
883
+ }
884
+ else if (action === 'stop' && this.players.size === 0) {
885
+ this.logger.info('No players online, stopping server due to memory leak...');
886
+ await this.stop();
887
+ }
888
+ else if (action === 'warn') {
889
+ this.logger.warning('Please restart server to free memory');
890
+ }
891
+ else {
892
+ this.logger.warning('Cannot restart: players online');
893
+ }
894
+ }
895
+ }
896
+ }
897
+ catch (error) {
898
+ const errorMessage = error instanceof Error ? error.message : String(error);
899
+ this.logger.error(`Memory monitor error: ${errorMessage}`);
900
+ }
901
+ }, interval);
902
+ }
903
+ async gracefulRestart() {
904
+ this.logger.info('Initiating graceful restart...');
905
+ this.sendCommand('say Server restarting in 30 seconds');
906
+ this.sendCommand('save-all');
907
+ await new Promise(resolve => setTimeout(resolve, 10000));
908
+ this.sendCommand('say Server restarting in 20 seconds');
909
+ await new Promise(resolve => setTimeout(resolve, 10000));
910
+ this.sendCommand('say Server restarting in 10 seconds');
911
+ this.sendCommand('save-all');
912
+ await new Promise(resolve => setTimeout(resolve, 5000));
913
+ this.sendCommand('say Server restarting in 5 seconds');
914
+ await new Promise(resolve => setTimeout(resolve, 5000));
915
+ await this.stop();
916
+ await this.start();
917
+ }
539
918
  async start() {
540
919
  this.logger.info(`Starting ${this.config.type} server v${this.config.version}...`);
920
+ await this.setupSymlinkMaster();
921
+ await this.setupRamdisk();
922
+ if (this.options.cleanLogsInterval && this.options.cleanLogsInterval > 0) {
923
+ this.cleanLogsCron = cron.schedule('0 */3 * * *', () => this.cleanOldLogs());
924
+ this.logger.info('Log cleanup scheduled every 3 hours');
925
+ }
541
926
  if (this.owners.size > 0) {
542
927
  this.logger.info(`Owners configured: ${Array.from(this.owners).join(', ')}`);
543
928
  }
@@ -561,7 +946,21 @@ class MinecraftServer extends events_1.EventEmitter {
561
946
  }
562
947
  if (this.config.platform === 'all') {
563
948
  await FileUtils_1.FileUtils.ensureDir(this.config.folders.plugins);
564
- await this.geyser.setup(this.config);
949
+ await this.geyser.setup(this.config, this.isPterodactyl);
950
+ }
951
+ const pluginsToInstall = [
952
+ { name: 'ProtocolLib', enabled: this.options.enableProtocolLib, url: 'https://github.com/dmulloy2/ProtocolLib/releases/latest/download/ProtocolLib.jar' },
953
+ { name: 'TCPShield', enabled: this.options.enableTCPShield, url: 'https://tcpshield.com/api/download/plugin' },
954
+ { name: 'SpigotOptimizers', enabled: this.options.enableSpigotOptimizers, url: 'https://github.com/SpigotOptimizers/SpigotOptimizers/releases/latest/download/SpigotOptimizers.jar' },
955
+ { name: 'Spark', enabled: this.options.enableSpark, url: 'https://spark.lucko.me/download' },
956
+ { name: 'ViewDistanceTweaks', enabled: this.options.enableViewDistanceTweaks, url: 'https://github.com/ViewDistanceTweaks/ViewDistanceTweaks/releases/latest/download/ViewDistanceTweaks.jar' },
957
+ { name: 'FarmControl', enabled: this.options.enableFarmControl, url: 'https://github.com/FarmControl/FarmControl/releases/latest/download/FarmControl.jar' },
958
+ { name: 'EntityDetection', enabled: this.options.enableEntityDetection, url: 'https://github.com/EntityDetection/EntityDetection/releases/latest/download/EntityDetection.jar' }
959
+ ];
960
+ for (const plugin of pluginsToInstall) {
961
+ if (plugin.enabled) {
962
+ await this.pluginManager.installPlugin(plugin.name, plugin.url, this.config.folders.plugins);
963
+ }
565
964
  }
566
965
  if (this.options.enableViaVersion !== false) {
567
966
  this.logger.info('Enabling ViaVersion for client version compatibility...');
@@ -589,12 +988,6 @@ class MinecraftServer extends events_1.EventEmitter {
589
988
  serverJar,
590
989
  ...serverArgs
591
990
  ];
592
- if (this.options.networkOptimization?.tcpFastOpen) {
593
- fullArgs.unshift('-Djava.net.preferIPv4Stack=true');
594
- }
595
- if (this.options.networkOptimization?.bungeeMode) {
596
- fullArgs.unshift('-Dnet.kyori.adventure.text.serializer.legacy.AMPMSupport=true');
597
- }
598
991
  const env = this.buildEnvironment();
599
992
  this.logger.info(`Launching: ${this.javaCommand} ${fullArgs.join(' ')}`);
600
993
  this.process = (0, child_process_1.spawn)(this.javaCommand, fullArgs, {
@@ -610,41 +1003,7 @@ class MinecraftServer extends events_1.EventEmitter {
610
1003
  this.process.stderr.pipe(process.stderr);
611
1004
  }
612
1005
  else {
613
- this.process.stdout.on('data', (data) => {
614
- const output = data.toString();
615
- process.stdout.write(output);
616
- if (output.includes('joined the game')) {
617
- const match = output.match(/(\w+) joined the game/);
618
- if (match) {
619
- this.playerCount++;
620
- this.handlePlayerJoin(match[1]);
621
- if (this.owners.has(match[1].toLowerCase())) {
622
- this.sendCommand(`tellraw ${match[1]} {"text":"Welcome Owner! Use ${this.ownerCommandPrefix}help for commands","color":"gold"}`);
623
- }
624
- }
625
- }
626
- if (output.includes('left the game')) {
627
- const match = output.match(/(\w+) left the game/);
628
- if (match) {
629
- this.playerCount--;
630
- this.handlePlayerLeave(match[1]);
631
- }
632
- }
633
- if (output.includes('<') && output.includes('>')) {
634
- const chatMatch = output.match(/<(\w+)>\s+(.+)/);
635
- if (chatMatch) {
636
- const player = chatMatch[1];
637
- const message = chatMatch[2];
638
- if (message.startsWith(this.ownerCommandPrefix)) {
639
- const command = message.substring(this.ownerCommandPrefix.length);
640
- this.processOwnerCommand(player, command);
641
- }
642
- }
643
- }
644
- });
645
- this.process.stderr.on('data', (data) => {
646
- process.stderr.write(data.toString());
647
- });
1006
+ this.setupLogHandlers();
648
1007
  }
649
1008
  this.process.on('exit', (code) => {
650
1009
  this.serverInfo.status = 'stopped';
@@ -662,87 +1021,8 @@ class MinecraftServer extends events_1.EventEmitter {
662
1021
  if (this.config.backup.enabled) {
663
1022
  this.setupBackups();
664
1023
  }
665
- setTimeout(() => {
666
- if (this.serverInfo.status === 'starting') {
667
- this.serverInfo.status = 'running';
668
- this.logger.success('Server started successfully!');
669
- if (this.options.enableViaVersion !== false) {
670
- this.logger.info('ViaVersion is active - players from older versions can connect');
671
- }
672
- if (this.options.enableSkinRestorer !== false) {
673
- this.logger.info('SkinRestorer is active - player skins will be restored');
674
- }
675
- if (this.worldSize > 0) {
676
- this.logger.info(`World size: ${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB`);
677
- }
678
- if (this.owners.size > 0) {
679
- this.logger.info(`Owner commands enabled with prefix: ${this.ownerCommandPrefix}`);
680
- }
681
- this.emit('ready', this.serverInfo);
682
- this.startMemoryMonitor();
683
- }
684
- }, 10000);
685
1024
  return this.serverInfo;
686
1025
  }
687
- startMemoryMonitor() {
688
- if (!this.options.memoryMonitor?.enabled)
689
- return;
690
- const threshold = this.options.memoryMonitor.threshold || 90;
691
- const interval = this.options.memoryMonitor.interval || 30000;
692
- const action = this.options.memoryMonitor.action || 'warn';
693
- this.memoryMonitorInterval = setInterval(async () => {
694
- if (this.serverInfo.status !== 'running' || !this.process)
695
- return;
696
- try {
697
- await this.updateStats();
698
- const memPercent = (this.serverInfo.memory.used / this.serverInfo.memory.max) * 100;
699
- this.memoryUsageHistory.push(memPercent);
700
- if (this.memoryUsageHistory.length > 10) {
701
- this.memoryUsageHistory.shift();
702
- }
703
- if (memPercent > threshold) {
704
- this.logger.warning(`High memory usage: ${memPercent.toFixed(1)}%`);
705
- const isIncreasing = this.memoryUsageHistory.length > 5 &&
706
- this.memoryUsageHistory[this.memoryUsageHistory.length - 1] >
707
- this.memoryUsageHistory[0] * 1.2;
708
- if (isIncreasing) {
709
- this.logger.warning('Memory leak detected!');
710
- switch (action) {
711
- case 'restart':
712
- this.logger.info('Restarting server due to memory leak...');
713
- await this.gracefulRestart();
714
- break;
715
- case 'stop':
716
- this.logger.info('Stopping server due to memory leak...');
717
- await this.stop();
718
- break;
719
- case 'warn':
720
- default:
721
- this.logger.warning('Please restart server to free memory');
722
- }
723
- }
724
- }
725
- }
726
- catch (error) {
727
- this.logger.error('Memory monitor error:', error);
728
- }
729
- }, interval);
730
- }
731
- async gracefulRestart() {
732
- this.logger.info('Initiating graceful restart...');
733
- this.sendCommand('say Server restarting in 30 seconds');
734
- this.sendCommand('save-all');
735
- await new Promise(resolve => setTimeout(resolve, 10000));
736
- this.sendCommand('say Server restarting in 20 seconds');
737
- await new Promise(resolve => setTimeout(resolve, 10000));
738
- this.sendCommand('say Server restarting in 10 seconds');
739
- this.sendCommand('save-all');
740
- await new Promise(resolve => setTimeout(resolve, 5000));
741
- this.sendCommand('say Server restarting in 5 seconds');
742
- await new Promise(resolve => setTimeout(resolve, 5000));
743
- await this.stop();
744
- await this.start();
745
- }
746
1026
  async stop() {
747
1027
  if (!this.process) {
748
1028
  this.logger.warning('Server not running');
@@ -760,6 +1040,12 @@ class MinecraftServer extends events_1.EventEmitter {
760
1040
  if (this.backupCron) {
761
1041
  this.backupCron.stop();
762
1042
  }
1043
+ if (this.cleanLogsCron) {
1044
+ this.cleanLogsCron.stop();
1045
+ }
1046
+ if (this.ramdiskBackupCron) {
1047
+ this.ramdiskBackupCron.stop();
1048
+ }
763
1049
  if (this.memoryMonitorInterval) {
764
1050
  clearInterval(this.memoryMonitorInterval);
765
1051
  }
@@ -769,6 +1055,9 @@ class MinecraftServer extends events_1.EventEmitter {
769
1055
  if (this.config.platform === 'all') {
770
1056
  this.geyser.stop();
771
1057
  }
1058
+ if (this.options.ramdisk?.enabled) {
1059
+ await this.backupRamdiskToMaster();
1060
+ }
772
1061
  this.logger.success('Server stopped');
773
1062
  }
774
1063
  sendCommand(command) {
@@ -864,32 +1153,6 @@ class MinecraftServer extends events_1.EventEmitter {
864
1153
  }
865
1154
  return value;
866
1155
  }
867
- async calculateWorldSize() {
868
- try {
869
- const worldPath = path.join(process.cwd(), this.config.folders.world);
870
- if (!await fs.pathExists(worldPath))
871
- return 0;
872
- const getSize = async (dir) => {
873
- let total = 0;
874
- const files = await fs.readdir(dir);
875
- for (const file of files) {
876
- const filePath = path.join(dir, file);
877
- const stat = await fs.stat(filePath);
878
- if (stat.isDirectory()) {
879
- total += await getSize(filePath);
880
- }
881
- else {
882
- total += stat.size;
883
- }
884
- }
885
- return total;
886
- };
887
- return await getSize(worldPath);
888
- }
889
- catch {
890
- return 0;
891
- }
892
- }
893
1156
  }
894
1157
  exports.MinecraftServer = MinecraftServer;
895
1158
  //# sourceMappingURL=MinecraftServer.js.map