@dimzxzzx07/mc-headless 2.2.2 → 2.2.4

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.
@@ -3,7 +3,16 @@ import { spawn } from 'child_process';
3
3
  import * as cron from 'node-cron';
4
4
  import * as path from 'path';
5
5
  import * as fs from 'fs-extra';
6
- import { MinecraftConfig, ServerInfo, Player } from '../types';
6
+ import {
7
+ MinecraftConfig,
8
+ ServerInfo,
9
+ Player,
10
+ MinecraftServerOptions,
11
+ MemoryMonitorConfig,
12
+ NetworkOptimizationConfig,
13
+ OwnerConfig,
14
+ RamdiskConfig
15
+ } from '../types';
7
16
  import { ConfigHandler } from './ConfigHandler';
8
17
  import { JavaChecker, JavaInfo } from './JavaChecker';
9
18
  import { FileUtils } from '../utils/FileUtils';
@@ -17,38 +26,7 @@ import { ServerEngine } from '../engines/ServerEngine';
17
26
  import { GeyserBridge } from '../platforms/GeyserBridge';
18
27
  import { ViaVersionManager } from '../platforms/ViaVersion';
19
28
  import { SkinRestorerManager } from '../platforms/SkinRestorer';
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
- cleanLogsInterval?: number;
50
- optimizeForPterodactyl?: boolean;
51
- }
29
+ import { PluginManager } from '../platforms/PluginManager';
52
30
 
53
31
  export class MinecraftServer extends EventEmitter {
54
32
  private config: MinecraftConfig;
@@ -58,11 +36,13 @@ export class MinecraftServer extends EventEmitter {
58
36
  private geyser: GeyserBridge;
59
37
  private viaVersion: ViaVersionManager;
60
38
  private skinRestorer: SkinRestorerManager;
39
+ private pluginManager: PluginManager;
61
40
  private process: any = null;
62
41
  private serverInfo: ServerInfo;
63
42
  private players: Map<string, Player> = new Map();
64
43
  private backupCron: cron.ScheduledTask | null = null;
65
44
  private cleanLogsCron: cron.ScheduledTask | null = null;
45
+ private ramdiskBackupCron: cron.ScheduledTask | null = null;
66
46
  private startTime: Date | null = null;
67
47
  private memoryMonitorInterval: NodeJS.Timeout | null = null;
68
48
  private statsInterval: NodeJS.Timeout | null = null;
@@ -79,6 +59,8 @@ export class MinecraftServer extends EventEmitter {
79
59
  private cgroupMemory: number = 0;
80
60
  private cgroupCpu: number = 0;
81
61
  private isPterodactyl: boolean = false;
62
+ private ramdiskPaths: Map<string, string> = new Map();
63
+ private serverReady: boolean = false;
82
64
 
83
65
  constructor(userConfig: MinecraftServerOptions = {}) {
84
66
  super();
@@ -87,35 +69,73 @@ export class MinecraftServer extends EventEmitter {
87
69
 
88
70
  this.detectEnvironment();
89
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
+
90
113
  this.options = {
91
114
  javaVersion: 'auto',
92
115
  usePortableJava: !this.isPterodactyl,
93
- memoryMonitor: {
94
- enabled: true,
95
- threshold: 90,
96
- interval: 30000,
97
- action: 'warn'
98
- },
116
+ memoryMonitor: defaultMemoryMonitor,
99
117
  autoInstallJava: true,
100
- networkOptimization: {
101
- tcpFastOpen: true,
102
- bungeeMode: false,
103
- proxyProtocol: false
104
- },
118
+ networkOptimization: defaultNetworkOptimization,
105
119
  owners: [],
106
- ownerCommands: {
107
- prefix: '!',
108
- enabled: true
109
- },
120
+ ownerCommands: defaultOwnerCommands,
110
121
  silentMode: true,
111
122
  statsInterval: 30000,
112
123
  cleanLogsInterval: 3 * 60 * 60 * 1000,
113
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,
114
134
  ...userConfig
115
135
  };
116
136
 
117
137
  if (this.options.owners) {
118
- this.options.owners.forEach(owner => this.owners.add(owner.toLowerCase()));
138
+ this.options.owners.forEach((owner: string) => this.owners.add(owner.toLowerCase()));
119
139
  }
120
140
 
121
141
  if (this.options.ownerCommands?.prefix) {
@@ -133,6 +153,7 @@ export class MinecraftServer extends EventEmitter {
133
153
  this.geyser = new GeyserBridge();
134
154
  this.viaVersion = new ViaVersionManager();
135
155
  this.skinRestorer = new SkinRestorerManager();
156
+ this.pluginManager = new PluginManager();
136
157
 
137
158
  this.serverInfo = {
138
159
  pid: 0,
@@ -151,19 +172,53 @@ export class MinecraftServer extends EventEmitter {
151
172
  };
152
173
 
153
174
  this.detectCgroupLimits();
154
- this.cleanPaperCache();
155
175
  }
156
176
 
157
177
  private detectEnvironment(): void {
158
178
  this.isPterodactyl = !!(
159
179
  process.env.PTERODACTYL ||
160
180
  process.env.SERVER_PORT ||
161
- process.env.SERVER_MEMORY_MAX ||
162
181
  fs.existsSync('/home/container')
163
182
  );
164
183
 
165
184
  if (this.isPterodactyl) {
166
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
+
204
+ private detectCgroupLimits(): void {
205
+ try {
206
+ if (fs.existsSync('/sys/fs/cgroup/memory/memory.limit_in_bytes')) {
207
+ const limit = fs.readFileSync('/sys/fs/cgroup/memory/memory.limit_in_bytes', 'utf8');
208
+ this.cgroupMemory = parseInt(limit) / 1024 / 1024;
209
+ this.logger.debug(`Cgroup memory limit: ${this.cgroupMemory} MB`);
210
+ }
211
+
212
+ if (fs.existsSync('/sys/fs/cgroup/cpu/cpu.cfs_quota_us') && fs.existsSync('/sys/fs/cgroup/cpu/cpu.cfs_period_us')) {
213
+ const quota = parseInt(fs.readFileSync('/sys/fs/cgroup/cpu/cpu.cfs_quota_us', 'utf8'));
214
+ const period = parseInt(fs.readFileSync('/sys/fs/cgroup/cpu/cpu.cfs_period_us', 'utf8'));
215
+ if (quota > 0 && period > 0) {
216
+ this.cgroupCpu = quota / period;
217
+ this.logger.debug(`Cgroup CPU limit: ${this.cgroupCpu} cores`);
218
+ }
219
+ }
220
+ } catch (error) {
221
+ this.logger.debug('Not running in cgroup environment');
167
222
  }
168
223
  }
169
224
 
@@ -171,13 +226,14 @@ export class MinecraftServer extends EventEmitter {
171
226
  const pterodactylPort = process.env.SERVER_PORT;
172
227
  if (pterodactylPort) {
173
228
  this.config.network.port = parseInt(pterodactylPort);
174
- this.logger.info(`Using Pterodactyl port: ${pterodactylPort}`);
175
229
  }
176
230
 
177
231
  const pterodactylMemory = process.env.SERVER_MEMORY_MAX;
178
232
  if (pterodactylMemory) {
179
- this.config.memory.max = pterodactylMemory;
180
- this.logger.info(`Using Pterodactyl memory: ${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`);
181
237
  }
182
238
 
183
239
  const pterodactylInitMemory = process.env.SERVER_MEMORY_INIT;
@@ -187,29 +243,137 @@ export class MinecraftServer extends EventEmitter {
187
243
 
188
244
  this.config.world.viewDistance = 6;
189
245
  this.config.world.simulationDistance = 4;
190
-
191
- this.logger.info('Applied Pterodactyl optimizations');
192
246
  }
193
247
 
194
- private cleanPaperCache(): void {
195
- const cachePaths = [
196
- path.join(process.cwd(), 'plugins', '.paper-remapped'),
197
- path.join(process.cwd(), 'plugins', '.paper-remapped', 'geyser.jar'),
198
- path.join(process.cwd(), 'plugins', '.paper-remapped', 'ViaVersion.jar'),
199
- path.join(process.cwd(), 'cache', 'paper-remapped')
248
+ private createEngine(): ServerEngine {
249
+ switch (this.config.type) {
250
+ case 'paper':
251
+ case 'purpur':
252
+ case 'spigot':
253
+ return new PaperEngine();
254
+ case 'vanilla':
255
+ return new VanillaEngine();
256
+ case 'forge':
257
+ return new ForgeEngine();
258
+ case 'fabric':
259
+ return new FabricEngine();
260
+ default:
261
+ throw new Error(`Unsupported server type: ${this.config.type}`);
262
+ }
263
+ }
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 }
200
288
  ];
201
289
 
202
- cachePaths.forEach(cachePath => {
203
- if (fs.existsSync(cachePath)) {
204
- this.logger.info(`Cleaning corrupt cache: ${cachePath}`);
205
- try {
206
- fs.rmSync(cachePath, { recursive: true, force: true });
207
- this.logger.debug(`Cache cleaned: ${cachePath}`);
208
- } catch (err: any) {
209
- this.logger.warning(`Failed to clean cache: ${err.message}`);
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;
210
299
  }
211
300
  }
212
- });
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
+ }
213
377
  }
214
378
 
215
379
  private cleanOldLogs(): void {
@@ -230,139 +394,38 @@ export class MinecraftServer extends EventEmitter {
230
394
  }
231
395
  });
232
396
  this.logger.info('Old logs cleaned');
233
- } catch (err: any) {
234
- this.logger.warning(`Failed to clean logs: ${err.message}`);
235
- }
236
- }
237
-
238
- private generateOptimizedConfigs(): void {
239
- const serverDir = process.cwd();
240
-
241
- const paperGlobalYml = path.join(serverDir, 'paper-global.yml');
242
- if (!fs.existsSync(paperGlobalYml)) {
243
- const paperGlobalConfig = `# Paper Global Configuration
244
- # Optimized by MC-Headless
245
-
246
- chunk-loading:
247
- autoconfig-send-distance: false
248
- enable-frustum-priority: false
249
- global-max-chunk-load-rate: 100
250
- global-max-chunk-send-rate: 50
251
-
252
- entities:
253
- despawn-ranges:
254
- monster:
255
- soft: 28
256
- hard: 96
257
- animal:
258
- soft: 16
259
- hard: 32
260
- spawn-limits:
261
- ambient: 5
262
- axolotls: 5
263
- creature: 10
264
- monster: 15
265
- water_ambient: 5
266
- water_creature: 5
267
-
268
- misc:
269
- max-entity-speed: 100
270
- redstone-implementation: ALTERNATIVE_CURRENT
271
-
272
- player-auto-save: 6000
273
- player-auto-save-rate: 1200
274
- no-tick-view-distance: 12
275
- `;
276
- fs.writeFileSync(paperGlobalYml, paperGlobalConfig);
277
- this.logger.debug('Generated paper-global.yml');
278
- }
279
-
280
- const paperWorldDefaultsYml = path.join(serverDir, 'paper-world-defaults.yml');
281
- if (!fs.existsSync(paperWorldDefaultsYml)) {
282
- const paperWorldConfig = `# Paper World Defaults
283
- # Optimized by MC-Headless
284
-
285
- view-distance: 6
286
- no-tick-view-distance: 12
287
-
288
- despawn-ranges:
289
- monster:
290
- soft: 28
291
- hard: 96
292
- animal:
293
- soft: 16
294
- hard: 32
295
- ambient:
296
- soft: 16
297
- hard: 32
298
- `;
299
- fs.writeFileSync(paperWorldDefaultsYml, paperWorldConfig);
300
- this.logger.debug('Generated paper-world-defaults.yml');
301
- }
302
-
303
- const spigotYml = path.join(serverDir, 'spigot.yml');
304
- if (!fs.existsSync(spigotYml)) {
305
- const spigotConfig = `# Spigot Configuration
306
- # Optimized by MC-Headless
307
-
308
- entity-activation-range:
309
- animals: 16
310
- monsters: 24
311
- misc: 8
312
- water: 8
313
-
314
- mob-spawn-range: 4
315
-
316
- world-settings:
317
- default:
318
- view-distance: 6
319
- mob-spawn-range: 4
320
- entity-activation-range:
321
- animals: 16
322
- monsters: 24
323
- misc: 8
324
- water: 8
325
- `;
326
- fs.writeFileSync(spigotYml, spigotConfig);
327
- this.logger.debug('Generated spigot.yml');
397
+ } catch (err) {
398
+ const errorMessage = err instanceof Error ? err.message : String(err);
399
+ this.logger.warning(`Failed to clean logs: ${errorMessage}`);
328
400
  }
329
401
  }
330
402
 
331
- private detectCgroupLimits(): void {
403
+ private async calculateWorldSize(): Promise<number> {
332
404
  try {
333
- if (fs.existsSync('/sys/fs/cgroup/memory/memory.limit_in_bytes')) {
334
- const limit = fs.readFileSync('/sys/fs/cgroup/memory/memory.limit_in_bytes', 'utf8');
335
- this.cgroupMemory = parseInt(limit) / 1024 / 1024;
336
- this.logger.debug(`Cgroup memory limit: ${this.cgroupMemory} MB`);
337
- }
405
+ const worldPath = path.join(process.cwd(), this.config.folders.world);
406
+ if (!await fs.pathExists(worldPath)) return 0;
338
407
 
339
- if (fs.existsSync('/sys/fs/cgroup/cpu/cpu.cfs_quota_us') && fs.existsSync('/sys/fs/cgroup/cpu/cpu.cfs_period_us')) {
340
- const quota = parseInt(fs.readFileSync('/sys/fs/cgroup/cpu/cpu.cfs_quota_us', 'utf8'));
341
- const period = parseInt(fs.readFileSync('/sys/fs/cgroup/cpu/cpu.cfs_period_us', 'utf8'));
342
- if (quota > 0 && period > 0) {
343
- this.cgroupCpu = quota / period;
344
- this.logger.debug(`Cgroup CPU limit: ${this.cgroupCpu} cores`);
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
+ }
345
421
  }
346
- }
347
- } catch (error) {
348
- this.logger.debug('Not running in cgroup environment');
349
- }
350
- }
422
+
423
+ return total;
424
+ };
351
425
 
352
- private createEngine(): ServerEngine {
353
- switch (this.config.type) {
354
- case 'paper':
355
- case 'purpur':
356
- case 'spigot':
357
- return new PaperEngine();
358
- case 'vanilla':
359
- return new VanillaEngine();
360
- case 'forge':
361
- return new ForgeEngine();
362
- case 'fabric':
363
- return new FabricEngine();
364
- default:
365
- throw new Error(`Unsupported server type: ${this.config.type}`);
426
+ return await getSize(worldPath);
427
+ } catch {
428
+ return 0;
366
429
  }
367
430
  }
368
431
 
@@ -371,22 +434,55 @@ world-settings:
371
434
  return this.options.customJavaArgs;
372
435
  }
373
436
 
374
- 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
+
375
445
  const javaVersion = this.options.javaVersion || 'auto';
376
446
 
377
447
  const baseArgs = [
378
448
  `-Xms${this.config.memory.init}`,
379
- `-Xmx${this.config.memory.max}`,
380
- '-XX:+UseG1GC'
449
+ `-Xmx${memMax}M`,
450
+ '-XX:+UseG1GC',
451
+ '-XX:MaxGCPauseMillis=50',
452
+ '-Dio.netty.recycler.maxCapacity.default=0',
453
+ '-Dio.netty.leakDetectionLevel=disabled',
454
+ '-Dterminal.jline=false',
455
+ '-Dterminal.ansi=true'
381
456
  ];
382
457
 
458
+ if (this.options.networkOptimization?.preferIPv4) {
459
+ baseArgs.unshift('-Djava.net.preferIPv4Stack=true');
460
+ }
461
+
462
+ if (this.options.networkOptimization?.tcpFastOpen) {
463
+ baseArgs.unshift('-Djava.net.tcpFastOpen=true');
464
+ }
465
+
466
+ if (this.options.networkOptimization?.tcpNoDelay) {
467
+ baseArgs.unshift('-Djava.net.tcp.nodelay=true');
468
+ }
469
+
470
+ if (this.options.networkOptimization?.compressionThreshold) {
471
+ baseArgs.push(`-Dnetwork-compression-threshold=${this.options.networkOptimization.compressionThreshold}`);
472
+ }
473
+
474
+ if (this.options.networkOptimization?.velocity?.enabled) {
475
+ baseArgs.push('-Dvelocity.secret=' + this.options.networkOptimization.velocity.secret);
476
+ }
477
+
383
478
  let gcArgs: string[] = [];
384
479
 
385
480
  if (memMax >= 16384) {
386
481
  gcArgs = [
387
- '-XX:+ParallelRefProcEnabled',
388
- '-XX:MaxGCPauseMillis=100',
389
482
  '-XX:+UnlockExperimentalVMOptions',
483
+ '-XX:+UseStringDeduplication',
484
+ '-XX:G1MixedGCLiveThresholdPercent=90',
485
+ '-XX:+ParallelRefProcEnabled',
390
486
  '-XX:+DisableExplicitGC',
391
487
  '-XX:+AlwaysPreTouch',
392
488
  '-XX:G1NewSizePercent=40',
@@ -396,7 +492,6 @@ world-settings:
396
492
  '-XX:G1HeapWastePercent=5',
397
493
  '-XX:G1MixedGCCountTarget=4',
398
494
  '-XX:InitiatingHeapOccupancyPercent=20',
399
- '-XX:G1MixedGCLiveThresholdPercent=90',
400
495
  '-XX:G1RSetUpdatingPauseTimePercent=5',
401
496
  '-XX:SurvivorRatio=32',
402
497
  '-XX:+PerfDisableSharedMem',
@@ -404,9 +499,10 @@ world-settings:
404
499
  ];
405
500
  } else if (memMax >= 8192) {
406
501
  gcArgs = [
407
- '-XX:+ParallelRefProcEnabled',
408
- '-XX:MaxGCPauseMillis=150',
409
502
  '-XX:+UnlockExperimentalVMOptions',
503
+ '-XX:+UseStringDeduplication',
504
+ '-XX:G1MixedGCLiveThresholdPercent=90',
505
+ '-XX:+ParallelRefProcEnabled',
410
506
  '-XX:+DisableExplicitGC',
411
507
  '-XX:+AlwaysPreTouch',
412
508
  '-XX:G1NewSizePercent=30',
@@ -416,30 +512,32 @@ world-settings:
416
512
  '-XX:G1HeapWastePercent=5',
417
513
  '-XX:G1MixedGCCountTarget=4',
418
514
  '-XX:InitiatingHeapOccupancyPercent=15',
419
- '-XX:G1MixedGCLiveThresholdPercent=90',
420
515
  '-XX:G1RSetUpdatingPauseTimePercent=5',
421
516
  '-XX:SurvivorRatio=32',
422
517
  '-XX:+PerfDisableSharedMem',
423
518
  '-XX:MaxTenuringThreshold=1'
424
519
  ];
425
520
  } else {
426
- gcArgs = this.config.memory.useAikarsFlags ? [
427
- '-XX:+ParallelRefProcEnabled',
428
- '-XX:MaxGCPauseMillis=200',
521
+ gcArgs = [
429
522
  '-XX:+UnlockExperimentalVMOptions',
523
+ '-XX:+UseStringDeduplication',
524
+ '-XX:G1MixedGCLiveThresholdPercent=90',
525
+ '-XX:+ParallelRefProcEnabled',
430
526
  '-XX:+DisableExplicitGC',
431
527
  '-XX:+AlwaysPreTouch',
432
528
  '-XX:G1HeapWastePercent=5',
433
529
  '-XX:G1MixedGCCountTarget=4',
434
530
  '-XX:InitiatingHeapOccupancyPercent=15',
435
- '-XX:G1MixedGCLiveThresholdPercent=90',
436
531
  '-XX:G1RSetUpdatingPauseTimePercent=5',
437
532
  '-XX:SurvivorRatio=32',
438
533
  '-XX:+PerfDisableSharedMem',
439
- '-XX:MaxTenuringThreshold=1',
440
- '-Dusing.aikars.flags=https://mcflags.emc.gs',
441
- '-Daikars.new.flags=true'
442
- ] : [];
534
+ '-XX:MaxTenuringThreshold=1'
535
+ ];
536
+
537
+ if (this.config.memory.useAikarsFlags) {
538
+ gcArgs.push('-Dusing.aikars.flags=https://mcflags.emc.gs');
539
+ gcArgs.push('-Daikars.new.flags=true');
540
+ }
443
541
  }
444
542
 
445
543
  if (javaVersion === '21') {
@@ -472,167 +570,41 @@ world-settings:
472
570
  return env;
473
571
  }
474
572
 
475
- public async start(): Promise<ServerInfo> {
476
- this.logger.info(`Starting ${this.config.type} server v${this.config.version}...`);
477
-
478
- this.cleanPaperCache();
479
- this.generateOptimizedConfigs();
480
-
481
- if (this.options.cleanLogsInterval && this.options.cleanLogsInterval > 0) {
482
- this.cleanLogsCron = cron.schedule('0 */3 * * *', () => this.cleanOldLogs());
483
- this.logger.info('Log cleanup scheduled every 3 hours');
484
- }
485
-
486
- if (this.owners.size > 0) {
487
- this.logger.info(`Owners configured: ${Array.from(this.owners).join(', ')}`);
488
- }
489
-
490
- this.javaInfo = await JavaChecker.ensureJava(
491
- this.config.version,
492
- this.options.usePortableJava || false
493
- );
494
- this.javaCommand = this.javaInfo.path;
495
- this.logger.success(`Using Java ${this.javaInfo.version} (${this.javaInfo.type}) at ${this.javaInfo.path}`);
496
-
497
- const systemInfo = SystemDetector.getSystemInfo();
498
- this.logger.debug('System info:', systemInfo);
499
-
500
- const serverDir = process.cwd();
501
- await FileUtils.ensureServerStructure(this.config);
502
-
503
- this.worldSize = await this.calculateWorldSize();
504
- if (this.worldSize > 10 * 1024 * 1024 * 1024) {
505
- this.logger.warning(`Large world detected (${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB). Consider increasing memory allocation.`);
506
- }
507
-
508
- const jarPath = await this.engine.download(this.config, serverDir);
509
-
510
- if (this.config.type === 'forge') {
511
- await this.engine.prepare(this.config, serverDir, jarPath);
512
- } else {
513
- await this.engine.prepare(this.config, serverDir);
514
- }
515
-
516
- if (this.config.platform === 'all') {
517
- await FileUtils.ensureDir(this.config.folders.plugins);
518
- await this.geyser.setup(this.config);
519
- }
520
-
521
- if (this.options.enableViaVersion !== false) {
522
- this.logger.info('Enabling ViaVersion for client version compatibility...');
523
- await FileUtils.ensureDir(this.config.folders.plugins);
524
- await this.viaVersion.setup(this.config);
525
- await this.viaVersion.configureViaVersion(this.config);
526
-
527
- if (this.options.enableViaBackwards !== false) {
528
- this.logger.info('ViaBackwards will be installed');
529
- }
530
- if (this.options.enableViaRewind !== false) {
531
- this.logger.info('ViaRewind will be installed');
532
- }
533
- }
534
-
535
- if (this.options.enableSkinRestorer !== false) {
536
- this.logger.info('Enabling SkinRestorer for player skins...');
537
- await FileUtils.ensureDir(this.config.folders.plugins);
538
- await this.skinRestorer.setup(this.config);
539
- }
540
-
541
- const javaArgs = this.buildJavaArgs();
542
- const serverJar = this.engine.getServerJar(jarPath);
543
- const serverArgs = this.engine.getServerArgs();
544
-
545
- const fullArgs = [
546
- ...javaArgs,
547
- '-jar',
548
- serverJar,
549
- ...serverArgs
550
- ];
551
-
552
- if (this.options.networkOptimization?.tcpFastOpen) {
553
- fullArgs.unshift('-Djava.net.preferIPv4Stack=true');
554
- }
555
-
556
- if (this.options.networkOptimization?.bungeeMode) {
557
- fullArgs.unshift('-Dnet.kyori.adventure.text.serializer.legacy.AMPMSupport=true');
558
- }
559
-
560
- const env = this.buildEnvironment();
561
-
562
- this.logger.info(`Launching: ${this.javaCommand} ${fullArgs.join(' ')}`);
563
-
564
- this.process = spawn(this.javaCommand, fullArgs, {
565
- cwd: serverDir,
566
- env: env,
567
- stdio: ['pipe', 'pipe', 'pipe']
568
- });
569
-
570
- this.serverInfo.pid = this.process.pid!;
571
- this.serverInfo.status = 'starting';
572
- this.startTime = new Date();
573
-
574
- if (this.options.silentMode) {
575
- this.process.stdout.pipe(process.stdout);
576
- this.process.stderr.pipe(process.stderr);
577
- } else {
578
- this.setupLogHandlers();
579
- }
580
-
581
- this.process.on('exit', (code: number) => {
582
- this.serverInfo.status = 'stopped';
583
- this.logger.warning(`Server stopped with code ${code}`);
584
-
585
- if (this.config.autoRestart && code !== 0) {
586
- this.logger.info('Auto-restarting...');
587
- this.start();
588
- }
589
-
590
- this.emit('stop', { code });
591
- });
592
-
593
- if (this.options.statsInterval && this.options.statsInterval > 0) {
594
- this.statsInterval = setInterval(() => this.updateStats(), this.options.statsInterval);
595
- }
596
-
597
- this.monitorResources();
573
+ private setupLogHandlers(): void {
574
+ this.process.stdout.on('data', (data: Buffer) => {
575
+ const output = data.toString();
576
+ process.stdout.write(output);
598
577
 
599
- if (this.config.backup.enabled) {
600
- this.setupBackups();
601
- }
578
+ if (!this.serverReady) {
579
+ if (output.includes('Done') || output.includes('For help, type "help"')) {
580
+ this.serverReady = true;
581
+ this.serverInfo.status = 'running';
582
+ this.logger.success('Server started successfully!');
583
+
584
+ if (this.options.enableViaVersion !== false) {
585
+ this.logger.info('ViaVersion is active - players from older versions can connect');
586
+ }
587
+
588
+ if (this.options.enableSkinRestorer !== false) {
589
+ this.logger.info('SkinRestorer is active - player skins will be restored');
590
+ }
591
+
592
+ if (this.worldSize > 0) {
593
+ this.logger.info(`World size: ${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB`);
594
+ }
602
595
 
603
- setTimeout(() => {
604
- if (this.serverInfo.status === 'starting') {
605
- this.serverInfo.status = 'running';
606
- this.logger.success('Server started successfully!');
607
-
608
- if (this.options.enableViaVersion !== false) {
609
- this.logger.info('ViaVersion is active - players from older versions can connect');
610
- }
611
-
612
- if (this.options.enableSkinRestorer !== false) {
613
- this.logger.info('SkinRestorer is active - player skins will be restored');
614
- }
615
-
616
- if (this.worldSize > 0) {
617
- this.logger.info(`World size: ${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB`);
618
- }
596
+ if (this.owners.size > 0) {
597
+ this.logger.info(`Owner commands enabled with prefix: ${this.ownerCommandPrefix}`);
598
+ }
619
599
 
620
- if (this.owners.size > 0) {
621
- this.logger.info(`Owner commands enabled with prefix: ${this.ownerCommandPrefix}`);
600
+ if (this.options.ramdisk?.enabled) {
601
+ this.logger.info('RAM disk active - worlds/plugins running in memory');
602
+ }
603
+
604
+ this.emit('ready', this.serverInfo);
605
+ this.startMemoryMonitor();
622
606
  }
623
-
624
- this.emit('ready', this.serverInfo);
625
- this.startMemoryMonitor();
626
607
  }
627
- }, 10000);
628
-
629
- return this.serverInfo;
630
- }
631
-
632
- private setupLogHandlers(): void {
633
- this.process.stdout.on('data', (data: Buffer) => {
634
- const output = data.toString();
635
- process.stdout.write(output);
636
608
 
637
609
  if (output.includes('joined the game')) {
638
610
  const match = output.match(/(\w+) joined the game/);
@@ -969,7 +941,8 @@ world-settings:
969
941
  this.emit('resource', this.serverInfo);
970
942
 
971
943
  } catch (error) {
972
- this.logger.error('Stats update error:', error);
944
+ const errorMessage = error instanceof Error ? error.message : String(error);
945
+ this.logger.error(`Stats update error: ${errorMessage}`);
973
946
  }
974
947
  }
975
948
 
@@ -1003,24 +976,23 @@ world-settings:
1003
976
  if (isIncreasing) {
1004
977
  this.logger.warning('Memory leak detected!');
1005
978
 
1006
- switch (action) {
1007
- case 'restart':
1008
- this.logger.info('Restarting server due to memory leak...');
1009
- await this.gracefulRestart();
1010
- break;
1011
- case 'stop':
1012
- this.logger.info('Stopping server due to memory leak...');
1013
- await this.stop();
1014
- break;
1015
- case 'warn':
1016
- default:
1017
- this.logger.warning('Please restart server to free memory');
979
+ if (action === 'restart' && this.players.size === 0) {
980
+ this.logger.info('No players online, restarting server due to memory leak...');
981
+ await this.gracefulRestart();
982
+ } else if (action === 'stop' && this.players.size === 0) {
983
+ this.logger.info('No players online, stopping server due to memory leak...');
984
+ await this.stop();
985
+ } else if (action === 'warn') {
986
+ this.logger.warning('Please restart server to free memory');
987
+ } else {
988
+ this.logger.warning('Cannot restart: players online');
1018
989
  }
1019
990
  }
1020
991
  }
1021
992
 
1022
993
  } catch (error) {
1023
- this.logger.error('Memory monitor error:', error);
994
+ const errorMessage = error instanceof Error ? error.message : String(error);
995
+ this.logger.error(`Memory monitor error: ${errorMessage}`);
1024
996
  }
1025
997
  }, interval);
1026
998
  }
@@ -1050,6 +1022,145 @@ world-settings:
1050
1022
  await this.start();
1051
1023
  }
1052
1024
 
1025
+ public async start(): Promise<ServerInfo> {
1026
+ this.logger.info(`Starting ${this.config.type} server v${this.config.version}...`);
1027
+
1028
+ await this.setupSymlinkMaster();
1029
+ await this.setupRamdisk();
1030
+
1031
+ if (this.options.cleanLogsInterval && this.options.cleanLogsInterval > 0) {
1032
+ this.cleanLogsCron = cron.schedule('0 */3 * * *', () => this.cleanOldLogs());
1033
+ this.logger.info('Log cleanup scheduled every 3 hours');
1034
+ }
1035
+
1036
+ if (this.owners.size > 0) {
1037
+ this.logger.info(`Owners configured: ${Array.from(this.owners).join(', ')}`);
1038
+ }
1039
+
1040
+ this.javaInfo = await JavaChecker.ensureJava(
1041
+ this.config.version,
1042
+ this.options.usePortableJava || false
1043
+ );
1044
+ this.javaCommand = this.javaInfo.path;
1045
+ this.logger.success(`Using Java ${this.javaInfo.version} (${this.javaInfo.type}) at ${this.javaInfo.path}`);
1046
+
1047
+ const systemInfo = SystemDetector.getSystemInfo();
1048
+ this.logger.debug('System info:', systemInfo);
1049
+
1050
+ const serverDir = process.cwd();
1051
+ await FileUtils.ensureServerStructure(this.config);
1052
+
1053
+ this.worldSize = await this.calculateWorldSize();
1054
+ if (this.worldSize > 10 * 1024 * 1024 * 1024) {
1055
+ this.logger.warning(`Large world detected (${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB). Consider increasing memory allocation.`);
1056
+ }
1057
+
1058
+ const jarPath = await this.engine.download(this.config, serverDir);
1059
+
1060
+ if (this.config.type === 'forge') {
1061
+ await this.engine.prepare(this.config, serverDir, jarPath);
1062
+ } else {
1063
+ await this.engine.prepare(this.config, serverDir);
1064
+ }
1065
+
1066
+ if (this.config.platform === 'all') {
1067
+ await FileUtils.ensureDir(this.config.folders.plugins);
1068
+ await this.geyser.setup(this.config, this.isPterodactyl);
1069
+ }
1070
+
1071
+ const pluginsToInstall = [
1072
+ { name: 'ProtocolLib', enabled: this.options.enableProtocolLib, url: 'https://github.com/dmulloy2/ProtocolLib/releases/latest/download/ProtocolLib.jar' },
1073
+ { name: 'TCPShield', enabled: this.options.enableTCPShield, url: 'https://tcpshield.com/api/download/plugin' },
1074
+ { name: 'SpigotOptimizers', enabled: this.options.enableSpigotOptimizers, url: 'https://github.com/SpigotOptimizers/SpigotOptimizers/releases/latest/download/SpigotOptimizers.jar' },
1075
+ { name: 'Spark', enabled: this.options.enableSpark, url: 'https://spark.lucko.me/download' },
1076
+ { name: 'ViewDistanceTweaks', enabled: this.options.enableViewDistanceTweaks, url: 'https://github.com/ViewDistanceTweaks/ViewDistanceTweaks/releases/latest/download/ViewDistanceTweaks.jar' },
1077
+ { name: 'FarmControl', enabled: this.options.enableFarmControl, url: 'https://github.com/FarmControl/FarmControl/releases/latest/download/FarmControl.jar' },
1078
+ { name: 'EntityDetection', enabled: this.options.enableEntityDetection, url: 'https://github.com/EntityDetection/EntityDetection/releases/latest/download/EntityDetection.jar' }
1079
+ ];
1080
+
1081
+ for (const plugin of pluginsToInstall) {
1082
+ if (plugin.enabled) {
1083
+ await this.pluginManager.installPlugin(plugin.name, plugin.url, this.config.folders.plugins);
1084
+ }
1085
+ }
1086
+
1087
+ if (this.options.enableViaVersion !== false) {
1088
+ this.logger.info('Enabling ViaVersion for client version compatibility...');
1089
+ await FileUtils.ensureDir(this.config.folders.plugins);
1090
+ await this.viaVersion.setup(this.config);
1091
+ await this.viaVersion.configureViaVersion(this.config);
1092
+
1093
+ if (this.options.enableViaBackwards !== false) {
1094
+ this.logger.info('ViaBackwards will be installed');
1095
+ }
1096
+ if (this.options.enableViaRewind !== false) {
1097
+ this.logger.info('ViaRewind will be installed');
1098
+ }
1099
+ }
1100
+
1101
+ if (this.options.enableSkinRestorer !== false) {
1102
+ this.logger.info('Enabling SkinRestorer for player skins...');
1103
+ await FileUtils.ensureDir(this.config.folders.plugins);
1104
+ await this.skinRestorer.setup(this.config);
1105
+ }
1106
+
1107
+ const javaArgs = this.buildJavaArgs();
1108
+ const serverJar = this.engine.getServerJar(jarPath);
1109
+ const serverArgs = this.engine.getServerArgs();
1110
+
1111
+ const fullArgs = [
1112
+ ...javaArgs,
1113
+ '-jar',
1114
+ serverJar,
1115
+ ...serverArgs
1116
+ ];
1117
+
1118
+ const env = this.buildEnvironment();
1119
+
1120
+ this.logger.info(`Launching: ${this.javaCommand} ${fullArgs.join(' ')}`);
1121
+
1122
+ this.process = spawn(this.javaCommand, fullArgs, {
1123
+ cwd: serverDir,
1124
+ env: env,
1125
+ stdio: ['pipe', 'pipe', 'pipe']
1126
+ });
1127
+
1128
+ this.serverInfo.pid = this.process.pid!;
1129
+ this.serverInfo.status = 'starting';
1130
+ this.startTime = new Date();
1131
+
1132
+ if (this.options.silentMode) {
1133
+ this.process.stdout.pipe(process.stdout);
1134
+ this.process.stderr.pipe(process.stderr);
1135
+ } else {
1136
+ this.setupLogHandlers();
1137
+ }
1138
+
1139
+ this.process.on('exit', (code: number) => {
1140
+ this.serverInfo.status = 'stopped';
1141
+ this.logger.warning(`Server stopped with code ${code}`);
1142
+
1143
+ if (this.config.autoRestart && code !== 0) {
1144
+ this.logger.info('Auto-restarting...');
1145
+ this.start();
1146
+ }
1147
+
1148
+ this.emit('stop', { code });
1149
+ });
1150
+
1151
+ if (this.options.statsInterval && this.options.statsInterval > 0) {
1152
+ this.statsInterval = setInterval(() => this.updateStats(), this.options.statsInterval);
1153
+ }
1154
+
1155
+ this.monitorResources();
1156
+
1157
+ if (this.config.backup.enabled) {
1158
+ this.setupBackups();
1159
+ }
1160
+
1161
+ return this.serverInfo;
1162
+ }
1163
+
1053
1164
  public async stop(): Promise<void> {
1054
1165
  if (!this.process) {
1055
1166
  this.logger.warning('Server not running');
@@ -1077,6 +1188,10 @@ world-settings:
1077
1188
  this.cleanLogsCron.stop();
1078
1189
  }
1079
1190
 
1191
+ if (this.ramdiskBackupCron) {
1192
+ this.ramdiskBackupCron.stop();
1193
+ }
1194
+
1080
1195
  if (this.memoryMonitorInterval) {
1081
1196
  clearInterval(this.memoryMonitorInterval);
1082
1197
  }
@@ -1089,7 +1204,10 @@ world-settings:
1089
1204
  this.geyser.stop();
1090
1205
  }
1091
1206
 
1092
- this.cleanPaperCache();
1207
+ if (this.options.ramdisk?.enabled) {
1208
+ await this.backupRamdiskToMaster();
1209
+ }
1210
+
1093
1211
  this.logger.success('Server stopped');
1094
1212
  }
1095
1213
 
@@ -1200,33 +1318,4 @@ world-settings:
1200
1318
  }
1201
1319
  return value;
1202
1320
  }
1203
-
1204
- private async calculateWorldSize(): Promise<number> {
1205
- try {
1206
- const worldPath = path.join(process.cwd(), this.config.folders.world);
1207
- if (!await fs.pathExists(worldPath)) return 0;
1208
-
1209
- const getSize = async (dir: string): Promise<number> => {
1210
- let total = 0;
1211
- const files = await fs.readdir(dir);
1212
-
1213
- for (const file of files) {
1214
- const filePath = path.join(dir, file);
1215
- const stat = await fs.stat(filePath);
1216
-
1217
- if (stat.isDirectory()) {
1218
- total += await getSize(filePath);
1219
- } else {
1220
- total += stat.size;
1221
- }
1222
- }
1223
-
1224
- return total;
1225
- };
1226
-
1227
- return await getSize(worldPath);
1228
- } catch {
1229
- return 0;
1230
- }
1231
- }
1232
1321
  }