@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
@@ -1,7 +1,18 @@
1
1
  import { EventEmitter } from 'events';
2
2
  import { spawn } from 'child_process';
3
3
  import * as cron from 'node-cron';
4
- import { MinecraftConfig, ServerInfo, Player } from '../types';
4
+ import * as path from 'path';
5
+ import * as fs from 'fs-extra';
6
+ import {
7
+ MinecraftConfig,
8
+ ServerInfo,
9
+ Player,
10
+ MinecraftServerOptions,
11
+ MemoryMonitorConfig,
12
+ NetworkOptimizationConfig,
13
+ OwnerConfig,
14
+ RamdiskConfig
15
+ } from '../types';
5
16
  import { ConfigHandler } from './ConfigHandler';
6
17
  import { JavaChecker, JavaInfo } from './JavaChecker';
7
18
  import { FileUtils } from '../utils/FileUtils';
@@ -15,38 +26,7 @@ import { ServerEngine } from '../engines/ServerEngine';
15
26
  import { GeyserBridge } from '../platforms/GeyserBridge';
16
27
  import { ViaVersionManager } from '../platforms/ViaVersion';
17
28
  import { SkinRestorerManager } from '../platforms/SkinRestorer';
18
- import * as path from 'path';
19
- import * as fs from 'fs-extra';
20
-
21
- export interface MinecraftServerOptions extends Partial<MinecraftConfig> {
22
- enableViaVersion?: boolean;
23
- enableViaBackwards?: boolean;
24
- enableViaRewind?: boolean;
25
- enableSkinRestorer?: boolean;
26
- enableProtocolSupport?: boolean;
27
- customJavaArgs?: string[];
28
- javaVersion?: '17' | '21' | 'auto';
29
- usePortableJava?: boolean;
30
- memoryMonitor?: {
31
- enabled: boolean;
32
- threshold: number;
33
- interval: number;
34
- action: 'restart' | 'warn' | 'stop';
35
- };
36
- autoInstallJava?: boolean;
37
- networkOptimization?: {
38
- tcpFastOpen: boolean;
39
- bungeeMode: boolean;
40
- proxyProtocol: boolean;
41
- };
42
- owners?: string[];
43
- ownerCommands?: {
44
- prefix: string;
45
- enabled: boolean;
46
- };
47
- silentMode?: boolean;
48
- statsInterval?: number;
49
- }
29
+ import { PluginManager } from '../platforms/PluginManager';
50
30
 
51
31
  export class MinecraftServer extends EventEmitter {
52
32
  private config: MinecraftConfig;
@@ -56,10 +36,13 @@ export class MinecraftServer extends EventEmitter {
56
36
  private geyser: GeyserBridge;
57
37
  private viaVersion: ViaVersionManager;
58
38
  private skinRestorer: SkinRestorerManager;
39
+ private pluginManager: PluginManager;
59
40
  private process: any = null;
60
41
  private serverInfo: ServerInfo;
61
42
  private players: Map<string, Player> = new Map();
62
43
  private backupCron: cron.ScheduledTask | null = null;
44
+ private cleanLogsCron: cron.ScheduledTask | null = null;
45
+ private ramdiskBackupCron: cron.ScheduledTask | null = null;
63
46
  private startTime: Date | null = null;
64
47
  private memoryMonitorInterval: NodeJS.Timeout | null = null;
65
48
  private statsInterval: NodeJS.Timeout | null = null;
@@ -75,39 +58,84 @@ export class MinecraftServer extends EventEmitter {
75
58
  private cpuUsage: number = 0;
76
59
  private cgroupMemory: number = 0;
77
60
  private cgroupCpu: number = 0;
61
+ private isPterodactyl: boolean = false;
62
+ private ramdiskPaths: Map<string, string> = new Map();
63
+ private serverReady: boolean = false;
78
64
 
79
65
  constructor(userConfig: MinecraftServerOptions = {}) {
80
66
  super();
81
67
  this.logger = Logger.getInstance();
82
68
  this.logger.banner();
83
69
 
70
+ this.detectEnvironment();
71
+
72
+ const defaultMemoryMonitor: MemoryMonitorConfig = {
73
+ enabled: true,
74
+ threshold: 90,
75
+ interval: 30000,
76
+ action: 'warn',
77
+ maxMemoryLeakRestarts: 3,
78
+ gcOnWarning: true
79
+ };
80
+
81
+ const defaultNetworkOptimization: NetworkOptimizationConfig = {
82
+ tcpFastOpen: true,
83
+ tcpNoDelay: true,
84
+ preferIPv4: true,
85
+ compressionThreshold: 64,
86
+ bungeeMode: false,
87
+ proxyProtocol: false,
88
+ velocity: {
89
+ enabled: false,
90
+ secret: '',
91
+ forwardingMode: 'modern'
92
+ }
93
+ };
94
+
95
+ const defaultOwnerCommands: OwnerConfig = {
96
+ prefix: '!',
97
+ enabled: true,
98
+ usernames: [],
99
+ permissions: []
100
+ };
101
+
102
+ const defaultRamdisk: RamdiskConfig = {
103
+ enabled: false,
104
+ world: false,
105
+ plugins: false,
106
+ addons: false,
107
+ backupInterval: 300000,
108
+ masterStorage: '/home/minecraft-master',
109
+ size: '1G',
110
+ mountPoint: '/dev/shm/minecraft'
111
+ };
112
+
84
113
  this.options = {
85
114
  javaVersion: 'auto',
86
- usePortableJava: true,
87
- memoryMonitor: {
88
- enabled: true,
89
- threshold: 90,
90
- interval: 30000,
91
- action: 'warn'
92
- },
115
+ usePortableJava: !this.isPterodactyl,
116
+ memoryMonitor: defaultMemoryMonitor,
93
117
  autoInstallJava: true,
94
- networkOptimization: {
95
- tcpFastOpen: true,
96
- bungeeMode: false,
97
- proxyProtocol: false
98
- },
118
+ networkOptimization: defaultNetworkOptimization,
99
119
  owners: [],
100
- ownerCommands: {
101
- prefix: '!',
102
- enabled: true
103
- },
120
+ ownerCommands: defaultOwnerCommands,
104
121
  silentMode: true,
105
122
  statsInterval: 30000,
123
+ cleanLogsInterval: 3 * 60 * 60 * 1000,
124
+ optimizeForPterodactyl: true,
125
+ ramdisk: defaultRamdisk,
126
+ symlinkMaster: '/home/minecraft-master',
127
+ enableProtocolLib: false,
128
+ enableTCPShield: false,
129
+ enableSpigotOptimizers: false,
130
+ enableSpark: false,
131
+ enableViewDistanceTweaks: false,
132
+ enableFarmControl: false,
133
+ enableEntityDetection: false,
106
134
  ...userConfig
107
135
  };
108
136
 
109
137
  if (this.options.owners) {
110
- this.options.owners.forEach(owner => this.owners.add(owner.toLowerCase()));
138
+ this.options.owners.forEach((owner: string) => this.owners.add(owner.toLowerCase()));
111
139
  }
112
140
 
113
141
  if (this.options.ownerCommands?.prefix) {
@@ -117,10 +145,15 @@ export class MinecraftServer extends EventEmitter {
117
145
  const handler = new ConfigHandler(userConfig);
118
146
  this.config = handler.getConfig();
119
147
 
148
+ if (this.isPterodactyl && this.options.optimizeForPterodactyl) {
149
+ this.optimizeForPterodactylPanel();
150
+ }
151
+
120
152
  this.engine = this.createEngine();
121
153
  this.geyser = new GeyserBridge();
122
154
  this.viaVersion = new ViaVersionManager();
123
155
  this.skinRestorer = new SkinRestorerManager();
156
+ this.pluginManager = new PluginManager();
124
157
 
125
158
  this.serverInfo = {
126
159
  pid: 0,
@@ -141,6 +174,33 @@ export class MinecraftServer extends EventEmitter {
141
174
  this.detectCgroupLimits();
142
175
  }
143
176
 
177
+ private detectEnvironment(): void {
178
+ this.isPterodactyl = !!(
179
+ process.env.PTERODACTYL ||
180
+ process.env.SERVER_PORT ||
181
+ fs.existsSync('/home/container')
182
+ );
183
+
184
+ if (this.isPterodactyl) {
185
+ this.logger.info('Pterodactyl environment detected');
186
+
187
+ const serverPort = process.env.SERVER_PORT;
188
+ if (serverPort) {
189
+ this.config.network.port = parseInt(serverPort);
190
+ this.logger.info(`Using Pterodactyl Java port: ${serverPort}`);
191
+ }
192
+
193
+ const bedrockPort = process.env.BEDROCK_PORT;
194
+ if (bedrockPort) {
195
+ this.config.network.bedrockPort = parseInt(bedrockPort);
196
+ this.logger.info(`Using Pterodactyl Bedrock port: ${bedrockPort}`);
197
+ } else {
198
+ this.logger.warning('No BEDROCK_PORT environment variable found. Geyser will share Java port.');
199
+ this.logger.warning('Make sure UDP is enabled for this port in Pterodactyl panel');
200
+ }
201
+ }
202
+ }
203
+
144
204
  private detectCgroupLimits(): void {
145
205
  try {
146
206
  if (fs.existsSync('/sys/fs/cgroup/memory/memory.limit_in_bytes')) {
@@ -162,6 +222,29 @@ export class MinecraftServer extends EventEmitter {
162
222
  }
163
223
  }
164
224
 
225
+ private optimizeForPterodactylPanel(): void {
226
+ const pterodactylPort = process.env.SERVER_PORT;
227
+ if (pterodactylPort) {
228
+ this.config.network.port = parseInt(pterodactylPort);
229
+ }
230
+
231
+ const pterodactylMemory = process.env.SERVER_MEMORY_MAX;
232
+ if (pterodactylMemory) {
233
+ const memValue = parseInt(pterodactylMemory);
234
+ const safeMemory = Math.floor(memValue * 0.9);
235
+ this.config.memory.max = `${safeMemory}M`;
236
+ this.logger.info(`Pterodactyl memory limit detected, using 90% safe value: ${safeMemory}MB`);
237
+ }
238
+
239
+ const pterodactylInitMemory = process.env.SERVER_MEMORY_INIT;
240
+ if (pterodactylInitMemory) {
241
+ this.config.memory.init = pterodactylInitMemory;
242
+ }
243
+
244
+ this.config.world.viewDistance = 6;
245
+ this.config.world.simulationDistance = 4;
246
+ }
247
+
165
248
  private createEngine(): ServerEngine {
166
249
  switch (this.config.type) {
167
250
  case 'paper':
@@ -179,21 +262,226 @@ export class MinecraftServer extends EventEmitter {
179
262
  }
180
263
  }
181
264
 
265
+ private async setupSymlinkMaster(): Promise<void> {
266
+ if (!this.options.symlinkMaster) return;
267
+
268
+ const masterDir = this.options.symlinkMaster;
269
+ await fs.ensureDir(masterDir);
270
+
271
+ const subDirs = ['worlds', 'plugins', 'addons', 'mods'];
272
+ for (const dir of subDirs) {
273
+ const masterSubDir = path.join(masterDir, dir);
274
+ await fs.ensureDir(masterSubDir);
275
+ }
276
+
277
+ this.logger.info(`Symlink master directory created at ${masterDir}`);
278
+ }
279
+
280
+ private async restoreFromMaster(): Promise<void> {
281
+ if (!this.options.ramdisk?.enabled) return;
282
+
283
+ const ramdiskBase = '/dev/shm/minecraft';
284
+ const items = [
285
+ { name: 'world', target: this.config.folders.world },
286
+ { name: 'plugins', target: this.config.folders.plugins },
287
+ { name: 'addons', target: this.config.folders.addons }
288
+ ];
289
+
290
+ for (const item of items) {
291
+ const ramdiskPath = path.join(ramdiskBase, item.name);
292
+ const masterPath = path.join(this.options.ramdisk.masterStorage, item.name);
293
+
294
+ if (fs.existsSync(ramdiskPath)) {
295
+ const ramdiskFiles = fs.readdirSync(ramdiskPath);
296
+ if (ramdiskFiles.length > 0) {
297
+ this.logger.debug(`RAM disk ${item.name} already has data, skipping restore`);
298
+ continue;
299
+ }
300
+ }
301
+
302
+ if (fs.existsSync(masterPath)) {
303
+ const masterFiles = fs.readdirSync(masterPath);
304
+ if (masterFiles.length > 0) {
305
+ this.logger.info(`Restoring ${item.name} from master storage...`);
306
+ await fs.ensureDir(ramdiskPath);
307
+ await fs.copy(masterPath, ramdiskPath, { overwrite: true });
308
+
309
+ if (!fs.existsSync(item.target)) {
310
+ await fs.symlink(ramdiskPath, item.target, 'dir');
311
+ }
312
+ }
313
+ }
314
+ }
315
+ }
316
+
317
+ private async setupRamdisk(): Promise<void> {
318
+ if (!this.options.ramdisk?.enabled) return;
319
+
320
+ const ramdiskBase = '/dev/shm/minecraft';
321
+ await fs.ensureDir(ramdiskBase);
322
+
323
+ await this.restoreFromMaster();
324
+
325
+ const items = [
326
+ { name: 'world', enabled: this.options.ramdisk.world, target: this.config.folders.world },
327
+ { name: 'plugins', enabled: this.options.ramdisk.plugins, target: this.config.folders.plugins },
328
+ { name: 'addons', enabled: this.options.ramdisk.addons, target: this.config.folders.addons }
329
+ ];
330
+
331
+ for (const item of items) {
332
+ if (!item.enabled) continue;
333
+
334
+ const ramdiskPath = path.join(ramdiskBase, item.name);
335
+ const masterPath = path.join(this.options.ramdisk.masterStorage, item.name);
336
+
337
+ await fs.ensureDir(masterPath);
338
+
339
+ if (fs.existsSync(item.target)) {
340
+ const stat = await fs.lstat(item.target);
341
+ if (stat.isSymbolicLink()) {
342
+ continue;
343
+ }
344
+ await fs.copy(item.target, masterPath, { overwrite: true });
345
+ await fs.remove(item.target);
346
+ }
347
+
348
+ await fs.ensureDir(ramdiskPath);
349
+ await fs.symlink(ramdiskPath, item.target, 'dir');
350
+ this.ramdiskPaths.set(item.name, ramdiskPath);
351
+
352
+ this.logger.info(`RAM disk created for ${item.name} at ${ramdiskPath}`);
353
+ }
354
+
355
+ if (this.options.ramdisk.backupInterval > 0) {
356
+ const minutes = Math.floor(this.options.ramdisk.backupInterval / 60000);
357
+ this.ramdiskBackupCron = cron.schedule(`*/${minutes} * * * *`, () => {
358
+ this.backupRamdiskToMaster();
359
+ });
360
+ this.logger.info(`RAM disk backup scheduled every ${minutes} minutes`);
361
+ }
362
+ }
363
+
364
+ private async backupRamdiskToMaster(): Promise<void> {
365
+ if (!this.options.ramdisk?.enabled) return;
366
+
367
+ for (const [name, ramdiskPath] of this.ramdiskPaths) {
368
+ const masterPath = path.join(this.options.ramdisk.masterStorage, name);
369
+ try {
370
+ await fs.copy(ramdiskPath, masterPath, { overwrite: true });
371
+ this.logger.debug(`RAM disk ${name} backed up to master storage`);
372
+ } catch (error) {
373
+ const errorMessage = error instanceof Error ? error.message : String(error);
374
+ this.logger.error(`Failed to backup ${name} RAM disk: ${errorMessage}`);
375
+ }
376
+ }
377
+ }
378
+
379
+ private cleanOldLogs(): void {
380
+ const logsDir = path.join(process.cwd(), 'logs');
381
+ if (!fs.existsSync(logsDir)) return;
382
+
383
+ try {
384
+ const files = fs.readdirSync(logsDir);
385
+ const now = Date.now();
386
+ const threeHours = 3 * 60 * 60 * 1000;
387
+
388
+ files.forEach(file => {
389
+ const filePath = path.join(logsDir, file);
390
+ const stats = fs.statSync(filePath);
391
+ if (now - stats.mtimeMs > threeHours) {
392
+ fs.unlinkSync(filePath);
393
+ this.logger.debug(`Cleaned old log: ${file}`);
394
+ }
395
+ });
396
+ this.logger.info('Old logs cleaned');
397
+ } catch (err) {
398
+ const errorMessage = err instanceof Error ? err.message : String(err);
399
+ this.logger.warning(`Failed to clean logs: ${errorMessage}`);
400
+ }
401
+ }
402
+
403
+ private async calculateWorldSize(): Promise<number> {
404
+ try {
405
+ const worldPath = path.join(process.cwd(), this.config.folders.world);
406
+ if (!await fs.pathExists(worldPath)) return 0;
407
+
408
+ const getSize = async (dir: string): Promise<number> => {
409
+ let total = 0;
410
+ const files = await fs.readdir(dir);
411
+
412
+ for (const file of files) {
413
+ const filePath = path.join(dir, file);
414
+ const stat = await fs.stat(filePath);
415
+
416
+ if (stat.isDirectory()) {
417
+ total += await getSize(filePath);
418
+ } else {
419
+ total += stat.size;
420
+ }
421
+ }
422
+
423
+ return total;
424
+ };
425
+
426
+ return await getSize(worldPath);
427
+ } catch {
428
+ return 0;
429
+ }
430
+ }
431
+
182
432
  private buildJavaArgs(): string[] {
183
433
  if (this.options.customJavaArgs && this.options.customJavaArgs.length > 0) {
184
434
  return this.options.customJavaArgs;
185
435
  }
186
436
 
187
- const memMax = this.parseMemory(this.config.memory.max);
437
+ let memMax = this.parseMemory(this.config.memory.max);
438
+
439
+ if (this.isPterodactyl && this.cgroupMemory > 0) {
440
+ const safeMem = Math.floor(this.cgroupMemory * 0.9);
441
+ memMax = Math.min(memMax, safeMem);
442
+ this.logger.info(`Adjusted memory to ${memMax}MB (90% of cgroup limit)`);
443
+ }
444
+
188
445
  const javaVersion = this.options.javaVersion || 'auto';
189
446
 
447
+ const baseArgs = [
448
+ `-Xms${this.config.memory.init}`,
449
+ `-Xmx${memMax}M`,
450
+ '-XX:+UseG1GC',
451
+ '-XX:MaxGCPauseMillis=50',
452
+ '-XX:+UseStringDeduplication',
453
+ '-XX:G1MixedGCLiveThresholdPercent=90',
454
+ '-Dio.netty.recycler.maxCapacity.default=0',
455
+ '-Dio.netty.leakDetectionLevel=disabled',
456
+ '-Dterminal.jline=false',
457
+ '-Dterminal.ansi=true'
458
+ ];
459
+
460
+ if (this.options.networkOptimization?.preferIPv4) {
461
+ baseArgs.unshift('-Djava.net.preferIPv4Stack=true');
462
+ }
463
+
464
+ if (this.options.networkOptimization?.tcpFastOpen) {
465
+ baseArgs.unshift('-Djava.net.tcpFastOpen=true');
466
+ }
467
+
468
+ if (this.options.networkOptimization?.tcpNoDelay) {
469
+ baseArgs.unshift('-Djava.net.tcp.nodelay=true');
470
+ }
471
+
472
+ if (this.options.networkOptimization?.compressionThreshold) {
473
+ baseArgs.push(`-Dnetwork-compression-threshold=${this.options.networkOptimization.compressionThreshold}`);
474
+ }
475
+
476
+ if (this.options.networkOptimization?.velocity?.enabled) {
477
+ baseArgs.push('-Dvelocity.secret=' + this.options.networkOptimization.velocity.secret);
478
+ }
479
+
190
480
  let gcArgs: string[] = [];
191
481
 
192
482
  if (memMax >= 16384) {
193
483
  gcArgs = [
194
- '-XX:+UseG1GC',
195
484
  '-XX:+ParallelRefProcEnabled',
196
- '-XX:MaxGCPauseMillis=100',
197
485
  '-XX:+UnlockExperimentalVMOptions',
198
486
  '-XX:+DisableExplicitGC',
199
487
  '-XX:+AlwaysPreTouch',
@@ -204,7 +492,6 @@ export class MinecraftServer extends EventEmitter {
204
492
  '-XX:G1HeapWastePercent=5',
205
493
  '-XX:G1MixedGCCountTarget=4',
206
494
  '-XX:InitiatingHeapOccupancyPercent=20',
207
- '-XX:G1MixedGCLiveThresholdPercent=90',
208
495
  '-XX:G1RSetUpdatingPauseTimePercent=5',
209
496
  '-XX:SurvivorRatio=32',
210
497
  '-XX:+PerfDisableSharedMem',
@@ -212,9 +499,7 @@ export class MinecraftServer extends EventEmitter {
212
499
  ];
213
500
  } else if (memMax >= 8192) {
214
501
  gcArgs = [
215
- '-XX:+UseG1GC',
216
502
  '-XX:+ParallelRefProcEnabled',
217
- '-XX:MaxGCPauseMillis=150',
218
503
  '-XX:+UnlockExperimentalVMOptions',
219
504
  '-XX:+DisableExplicitGC',
220
505
  '-XX:+AlwaysPreTouch',
@@ -225,42 +510,36 @@ export class MinecraftServer extends EventEmitter {
225
510
  '-XX:G1HeapWastePercent=5',
226
511
  '-XX:G1MixedGCCountTarget=4',
227
512
  '-XX:InitiatingHeapOccupancyPercent=15',
228
- '-XX:G1MixedGCLiveThresholdPercent=90',
229
513
  '-XX:G1RSetUpdatingPauseTimePercent=5',
230
514
  '-XX:SurvivorRatio=32',
231
515
  '-XX:+PerfDisableSharedMem',
232
516
  '-XX:MaxTenuringThreshold=1'
233
517
  ];
234
518
  } else {
235
- gcArgs = this.config.memory.useAikarsFlags ? [
236
- '-XX:+UseG1GC',
519
+ gcArgs = [
237
520
  '-XX:+ParallelRefProcEnabled',
238
- '-XX:MaxGCPauseMillis=200',
239
521
  '-XX:+UnlockExperimentalVMOptions',
240
522
  '-XX:+DisableExplicitGC',
241
523
  '-XX:+AlwaysPreTouch',
242
524
  '-XX:G1HeapWastePercent=5',
243
525
  '-XX:G1MixedGCCountTarget=4',
244
526
  '-XX:InitiatingHeapOccupancyPercent=15',
245
- '-XX:G1MixedGCLiveThresholdPercent=90',
246
527
  '-XX:G1RSetUpdatingPauseTimePercent=5',
247
528
  '-XX:SurvivorRatio=32',
248
529
  '-XX:+PerfDisableSharedMem',
249
- '-XX:MaxTenuringThreshold=1',
250
- '-Dusing.aikars.flags=https://mcflags.emc.gs',
251
- '-Daikars.new.flags=true'
252
- ] : [];
530
+ '-XX:MaxTenuringThreshold=1'
531
+ ];
532
+
533
+ if (this.config.memory.useAikarsFlags) {
534
+ gcArgs.push('-Dusing.aikars.flags=https://mcflags.emc.gs');
535
+ gcArgs.push('-Daikars.new.flags=true');
536
+ }
253
537
  }
254
538
 
255
539
  if (javaVersion === '21') {
256
540
  gcArgs.push('--enable-preview');
257
541
  }
258
542
 
259
- const baseArgs = [
260
- `-Xms${this.config.memory.init}`,
261
- `-Xmx${this.config.memory.max}`
262
- ];
263
-
264
543
  return [...baseArgs, ...gcArgs];
265
544
  }
266
545
 
@@ -268,7 +547,6 @@ export class MinecraftServer extends EventEmitter {
268
547
  const env: NodeJS.ProcessEnv = { ...process.env };
269
548
 
270
549
  env.MALLOC_ARENA_MAX = '2';
271
-
272
550
  env._JAVA_OPTIONS = `-Xmx${this.config.memory.max}`;
273
551
 
274
552
  if (this.javaInfo && this.javaInfo.type === 'portable') {
@@ -288,6 +566,81 @@ export class MinecraftServer extends EventEmitter {
288
566
  return env;
289
567
  }
290
568
 
569
+ private setupLogHandlers(): void {
570
+ this.process.stdout.on('data', (data: Buffer) => {
571
+ const output = data.toString();
572
+ process.stdout.write(output);
573
+
574
+ if (!this.serverReady) {
575
+ if (output.includes('Done') || output.includes('For help, type "help"')) {
576
+ this.serverReady = true;
577
+ this.serverInfo.status = 'running';
578
+ this.logger.success('Server started successfully!');
579
+
580
+ if (this.options.enableViaVersion !== false) {
581
+ this.logger.info('ViaVersion is active - players from older versions can connect');
582
+ }
583
+
584
+ if (this.options.enableSkinRestorer !== false) {
585
+ this.logger.info('SkinRestorer is active - player skins will be restored');
586
+ }
587
+
588
+ if (this.worldSize > 0) {
589
+ this.logger.info(`World size: ${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB`);
590
+ }
591
+
592
+ if (this.owners.size > 0) {
593
+ this.logger.info(`Owner commands enabled with prefix: ${this.ownerCommandPrefix}`);
594
+ }
595
+
596
+ if (this.options.ramdisk?.enabled) {
597
+ this.logger.info('RAM disk active - worlds/plugins running in memory');
598
+ }
599
+
600
+ this.emit('ready', this.serverInfo);
601
+ this.startMemoryMonitor();
602
+ }
603
+ }
604
+
605
+ if (output.includes('joined the game')) {
606
+ const match = output.match(/(\w+) joined the game/);
607
+ if (match) {
608
+ this.playerCount++;
609
+ this.handlePlayerJoin(match[1]);
610
+
611
+ if (this.owners.has(match[1].toLowerCase())) {
612
+ this.sendCommand(`tellraw ${match[1]} {"text":"Welcome Owner! Use ${this.ownerCommandPrefix}help for commands","color":"gold"}`);
613
+ }
614
+ }
615
+ }
616
+
617
+ if (output.includes('left the game')) {
618
+ const match = output.match(/(\w+) left the game/);
619
+ if (match) {
620
+ this.playerCount--;
621
+ this.handlePlayerLeave(match[1]);
622
+ }
623
+ }
624
+
625
+ if (output.includes('<') && output.includes('>')) {
626
+ const chatMatch = output.match(/<(\w+)>\s+(.+)/);
627
+ if (chatMatch) {
628
+ const player = chatMatch[1];
629
+ const message = chatMatch[2];
630
+
631
+ if (message.startsWith(this.ownerCommandPrefix)) {
632
+ const command = message.substring(this.ownerCommandPrefix.length);
633
+ this.processOwnerCommand(player, command);
634
+ }
635
+ }
636
+ }
637
+ });
638
+
639
+ this.process.stderr.on('data', (data: Buffer) => {
640
+ process.stderr.write(data.toString());
641
+ });
642
+ }
643
+
291
644
  private processOwnerCommand(player: string, command: string): void {
292
645
  if (!this.options.ownerCommands?.enabled) return;
293
646
  if (!this.owners.has(player.toLowerCase())) return;
@@ -584,13 +937,98 @@ export class MinecraftServer extends EventEmitter {
584
937
  this.emit('resource', this.serverInfo);
585
938
 
586
939
  } catch (error) {
587
- this.logger.error('Stats update error:', error);
940
+ const errorMessage = error instanceof Error ? error.message : String(error);
941
+ this.logger.error(`Stats update error: ${errorMessage}`);
588
942
  }
589
943
  }
590
944
 
945
+ private startMemoryMonitor(): void {
946
+ if (!this.options.memoryMonitor?.enabled) return;
947
+
948
+ const threshold = this.options.memoryMonitor.threshold || 90;
949
+ const interval = this.options.memoryMonitor.interval || 30000;
950
+ const action = this.options.memoryMonitor.action || 'warn';
951
+
952
+ this.memoryMonitorInterval = setInterval(async () => {
953
+ if (this.serverInfo.status !== 'running' || !this.process) return;
954
+
955
+ try {
956
+ await this.updateStats();
957
+
958
+ const memPercent = (this.serverInfo.memory.used / this.serverInfo.memory.max) * 100;
959
+
960
+ this.memoryUsageHistory.push(memPercent);
961
+ if (this.memoryUsageHistory.length > 10) {
962
+ this.memoryUsageHistory.shift();
963
+ }
964
+
965
+ if (memPercent > threshold) {
966
+ this.logger.warning(`High memory usage: ${memPercent.toFixed(1)}%`);
967
+
968
+ const isIncreasing = this.memoryUsageHistory.length > 5 &&
969
+ this.memoryUsageHistory[this.memoryUsageHistory.length - 1] >
970
+ this.memoryUsageHistory[0] * 1.2;
971
+
972
+ if (isIncreasing) {
973
+ this.logger.warning('Memory leak detected!');
974
+
975
+ if (action === 'restart' && this.players.size === 0) {
976
+ this.logger.info('No players online, restarting server due to memory leak...');
977
+ await this.gracefulRestart();
978
+ } else if (action === 'stop' && this.players.size === 0) {
979
+ this.logger.info('No players online, stopping server due to memory leak...');
980
+ await this.stop();
981
+ } else if (action === 'warn') {
982
+ this.logger.warning('Please restart server to free memory');
983
+ } else {
984
+ this.logger.warning('Cannot restart: players online');
985
+ }
986
+ }
987
+ }
988
+
989
+ } catch (error) {
990
+ const errorMessage = error instanceof Error ? error.message : String(error);
991
+ this.logger.error(`Memory monitor error: ${errorMessage}`);
992
+ }
993
+ }, interval);
994
+ }
995
+
996
+ private async gracefulRestart(): Promise<void> {
997
+ this.logger.info('Initiating graceful restart...');
998
+
999
+ this.sendCommand('say Server restarting in 30 seconds');
1000
+ this.sendCommand('save-all');
1001
+
1002
+ await new Promise(resolve => setTimeout(resolve, 10000));
1003
+
1004
+ this.sendCommand('say Server restarting in 20 seconds');
1005
+
1006
+ await new Promise(resolve => setTimeout(resolve, 10000));
1007
+
1008
+ this.sendCommand('say Server restarting in 10 seconds');
1009
+ this.sendCommand('save-all');
1010
+
1011
+ await new Promise(resolve => setTimeout(resolve, 5000));
1012
+
1013
+ this.sendCommand('say Server restarting in 5 seconds');
1014
+
1015
+ await new Promise(resolve => setTimeout(resolve, 5000));
1016
+
1017
+ await this.stop();
1018
+ await this.start();
1019
+ }
1020
+
591
1021
  public async start(): Promise<ServerInfo> {
592
1022
  this.logger.info(`Starting ${this.config.type} server v${this.config.version}...`);
593
1023
 
1024
+ await this.setupSymlinkMaster();
1025
+ await this.setupRamdisk();
1026
+
1027
+ if (this.options.cleanLogsInterval && this.options.cleanLogsInterval > 0) {
1028
+ this.cleanLogsCron = cron.schedule('0 */3 * * *', () => this.cleanOldLogs());
1029
+ this.logger.info('Log cleanup scheduled every 3 hours');
1030
+ }
1031
+
594
1032
  if (this.owners.size > 0) {
595
1033
  this.logger.info(`Owners configured: ${Array.from(this.owners).join(', ')}`);
596
1034
  }
@@ -623,7 +1061,23 @@ export class MinecraftServer extends EventEmitter {
623
1061
 
624
1062
  if (this.config.platform === 'all') {
625
1063
  await FileUtils.ensureDir(this.config.folders.plugins);
626
- await this.geyser.setup(this.config);
1064
+ await this.geyser.setup(this.config, this.isPterodactyl);
1065
+ }
1066
+
1067
+ const pluginsToInstall = [
1068
+ { name: 'ProtocolLib', enabled: this.options.enableProtocolLib, url: 'https://github.com/dmulloy2/ProtocolLib/releases/latest/download/ProtocolLib.jar' },
1069
+ { name: 'TCPShield', enabled: this.options.enableTCPShield, url: 'https://tcpshield.com/api/download/plugin' },
1070
+ { name: 'SpigotOptimizers', enabled: this.options.enableSpigotOptimizers, url: 'https://github.com/SpigotOptimizers/SpigotOptimizers/releases/latest/download/SpigotOptimizers.jar' },
1071
+ { name: 'Spark', enabled: this.options.enableSpark, url: 'https://spark.lucko.me/download' },
1072
+ { name: 'ViewDistanceTweaks', enabled: this.options.enableViewDistanceTweaks, url: 'https://github.com/ViewDistanceTweaks/ViewDistanceTweaks/releases/latest/download/ViewDistanceTweaks.jar' },
1073
+ { name: 'FarmControl', enabled: this.options.enableFarmControl, url: 'https://github.com/FarmControl/FarmControl/releases/latest/download/FarmControl.jar' },
1074
+ { name: 'EntityDetection', enabled: this.options.enableEntityDetection, url: 'https://github.com/EntityDetection/EntityDetection/releases/latest/download/EntityDetection.jar' }
1075
+ ];
1076
+
1077
+ for (const plugin of pluginsToInstall) {
1078
+ if (plugin.enabled) {
1079
+ await this.pluginManager.installPlugin(plugin.name, plugin.url, this.config.folders.plugins);
1080
+ }
627
1081
  }
628
1082
 
629
1083
  if (this.options.enableViaVersion !== false) {
@@ -657,14 +1111,6 @@ export class MinecraftServer extends EventEmitter {
657
1111
  ...serverArgs
658
1112
  ];
659
1113
 
660
- if (this.options.networkOptimization?.tcpFastOpen) {
661
- fullArgs.unshift('-Djava.net.preferIPv4Stack=true');
662
- }
663
-
664
- if (this.options.networkOptimization?.bungeeMode) {
665
- fullArgs.unshift('-Dnet.kyori.adventure.text.serializer.legacy.AMPMSupport=true');
666
- }
667
-
668
1114
  const env = this.buildEnvironment();
669
1115
 
670
1116
  this.logger.info(`Launching: ${this.javaCommand} ${fullArgs.join(' ')}`);
@@ -683,47 +1129,7 @@ export class MinecraftServer extends EventEmitter {
683
1129
  this.process.stdout.pipe(process.stdout);
684
1130
  this.process.stderr.pipe(process.stderr);
685
1131
  } else {
686
- this.process.stdout.on('data', (data: Buffer) => {
687
- const output = data.toString();
688
- process.stdout.write(output);
689
-
690
- if (output.includes('joined the game')) {
691
- const match = output.match(/(\w+) joined the game/);
692
- if (match) {
693
- this.playerCount++;
694
- this.handlePlayerJoin(match[1]);
695
-
696
- if (this.owners.has(match[1].toLowerCase())) {
697
- this.sendCommand(`tellraw ${match[1]} {"text":"Welcome Owner! Use ${this.ownerCommandPrefix}help for commands","color":"gold"}`);
698
- }
699
- }
700
- }
701
-
702
- if (output.includes('left the game')) {
703
- const match = output.match(/(\w+) left the game/);
704
- if (match) {
705
- this.playerCount--;
706
- this.handlePlayerLeave(match[1]);
707
- }
708
- }
709
-
710
- if (output.includes('<') && output.includes('>')) {
711
- const chatMatch = output.match(/<(\w+)>\s+(.+)/);
712
- if (chatMatch) {
713
- const player = chatMatch[1];
714
- const message = chatMatch[2];
715
-
716
- if (message.startsWith(this.ownerCommandPrefix)) {
717
- const command = message.substring(this.ownerCommandPrefix.length);
718
- this.processOwnerCommand(player, command);
719
- }
720
- }
721
- }
722
- });
723
-
724
- this.process.stderr.on('data', (data: Buffer) => {
725
- process.stderr.write(data.toString());
726
- });
1132
+ this.setupLogHandlers();
727
1133
  }
728
1134
 
729
1135
  this.process.on('exit', (code: number) => {
@@ -748,112 +1154,9 @@ export class MinecraftServer extends EventEmitter {
748
1154
  this.setupBackups();
749
1155
  }
750
1156
 
751
- setTimeout(() => {
752
- if (this.serverInfo.status === 'starting') {
753
- this.serverInfo.status = 'running';
754
- this.logger.success('Server started successfully!');
755
-
756
- if (this.options.enableViaVersion !== false) {
757
- this.logger.info('ViaVersion is active - players from older versions can connect');
758
- }
759
-
760
- if (this.options.enableSkinRestorer !== false) {
761
- this.logger.info('SkinRestorer is active - player skins will be restored');
762
- }
763
-
764
- if (this.worldSize > 0) {
765
- this.logger.info(`World size: ${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB`);
766
- }
767
-
768
- if (this.owners.size > 0) {
769
- this.logger.info(`Owner commands enabled with prefix: ${this.ownerCommandPrefix}`);
770
- }
771
-
772
- this.emit('ready', this.serverInfo);
773
- this.startMemoryMonitor();
774
- }
775
- }, 10000);
776
-
777
1157
  return this.serverInfo;
778
1158
  }
779
1159
 
780
- private startMemoryMonitor(): void {
781
- if (!this.options.memoryMonitor?.enabled) return;
782
-
783
- const threshold = this.options.memoryMonitor.threshold || 90;
784
- const interval = this.options.memoryMonitor.interval || 30000;
785
- const action = this.options.memoryMonitor.action || 'warn';
786
-
787
- this.memoryMonitorInterval = setInterval(async () => {
788
- if (this.serverInfo.status !== 'running' || !this.process) return;
789
-
790
- try {
791
- await this.updateStats();
792
-
793
- const memPercent = (this.serverInfo.memory.used / this.serverInfo.memory.max) * 100;
794
-
795
- this.memoryUsageHistory.push(memPercent);
796
- if (this.memoryUsageHistory.length > 10) {
797
- this.memoryUsageHistory.shift();
798
- }
799
-
800
- if (memPercent > threshold) {
801
- this.logger.warning(`High memory usage: ${memPercent.toFixed(1)}%`);
802
-
803
- const isIncreasing = this.memoryUsageHistory.length > 5 &&
804
- this.memoryUsageHistory[this.memoryUsageHistory.length - 1] >
805
- this.memoryUsageHistory[0] * 1.2;
806
-
807
- if (isIncreasing) {
808
- this.logger.warning('Memory leak detected!');
809
-
810
- switch (action) {
811
- case 'restart':
812
- this.logger.info('Restarting server due to memory leak...');
813
- await this.gracefulRestart();
814
- break;
815
- case 'stop':
816
- this.logger.info('Stopping server due to memory leak...');
817
- await this.stop();
818
- break;
819
- case 'warn':
820
- default:
821
- this.logger.warning('Please restart server to free memory');
822
- }
823
- }
824
- }
825
-
826
- } catch (error) {
827
- this.logger.error('Memory monitor error:', error);
828
- }
829
- }, interval);
830
- }
831
-
832
- private async gracefulRestart(): Promise<void> {
833
- this.logger.info('Initiating graceful restart...');
834
-
835
- this.sendCommand('say Server restarting in 30 seconds');
836
- this.sendCommand('save-all');
837
-
838
- await new Promise(resolve => setTimeout(resolve, 10000));
839
-
840
- this.sendCommand('say Server restarting in 20 seconds');
841
-
842
- await new Promise(resolve => setTimeout(resolve, 10000));
843
-
844
- this.sendCommand('say Server restarting in 10 seconds');
845
- this.sendCommand('save-all');
846
-
847
- await new Promise(resolve => setTimeout(resolve, 5000));
848
-
849
- this.sendCommand('say Server restarting in 5 seconds');
850
-
851
- await new Promise(resolve => setTimeout(resolve, 5000));
852
-
853
- await this.stop();
854
- await this.start();
855
- }
856
-
857
1160
  public async stop(): Promise<void> {
858
1161
  if (!this.process) {
859
1162
  this.logger.warning('Server not running');
@@ -877,6 +1180,14 @@ export class MinecraftServer extends EventEmitter {
877
1180
  this.backupCron.stop();
878
1181
  }
879
1182
 
1183
+ if (this.cleanLogsCron) {
1184
+ this.cleanLogsCron.stop();
1185
+ }
1186
+
1187
+ if (this.ramdiskBackupCron) {
1188
+ this.ramdiskBackupCron.stop();
1189
+ }
1190
+
880
1191
  if (this.memoryMonitorInterval) {
881
1192
  clearInterval(this.memoryMonitorInterval);
882
1193
  }
@@ -889,6 +1200,10 @@ export class MinecraftServer extends EventEmitter {
889
1200
  this.geyser.stop();
890
1201
  }
891
1202
 
1203
+ if (this.options.ramdisk?.enabled) {
1204
+ await this.backupRamdiskToMaster();
1205
+ }
1206
+
892
1207
  this.logger.success('Server stopped');
893
1208
  }
894
1209
 
@@ -999,33 +1314,4 @@ export class MinecraftServer extends EventEmitter {
999
1314
  }
1000
1315
  return value;
1001
1316
  }
1002
-
1003
- private async calculateWorldSize(): Promise<number> {
1004
- try {
1005
- const worldPath = path.join(process.cwd(), this.config.folders.world);
1006
- if (!await fs.pathExists(worldPath)) return 0;
1007
-
1008
- const getSize = async (dir: string): Promise<number> => {
1009
- let total = 0;
1010
- const files = await fs.readdir(dir);
1011
-
1012
- for (const file of files) {
1013
- const filePath = path.join(dir, file);
1014
- const stat = await fs.stat(filePath);
1015
-
1016
- if (stat.isDirectory()) {
1017
- total += await getSize(filePath);
1018
- } else {
1019
- total += stat.size;
1020
- }
1021
- }
1022
-
1023
- return total;
1024
- };
1025
-
1026
- return await getSize(worldPath);
1027
- } catch {
1028
- return 0;
1029
- }
1030
- }
1031
1317
  }