@dimzxzzx07/mc-headless 2.2.2 → 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.
@@ -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}`);
397
+ } catch (err) {
398
+ const errorMessage = err instanceof Error ? err.message : String(err);
399
+ this.logger.warning(`Failed to clean logs: ${errorMessage}`);
235
400
  }
236
401
  }
237
402
 
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');
328
- }
329
- }
330
-
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,21 +434,54 @@ 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
+ '-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'
381
458
  ];
382
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
+
383
480
  let gcArgs: string[] = [];
384
481
 
385
482
  if (memMax >= 16384) {
386
483
  gcArgs = [
387
484
  '-XX:+ParallelRefProcEnabled',
388
- '-XX:MaxGCPauseMillis=100',
389
485
  '-XX:+UnlockExperimentalVMOptions',
390
486
  '-XX:+DisableExplicitGC',
391
487
  '-XX:+AlwaysPreTouch',
@@ -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',
@@ -405,7 +500,6 @@ world-settings:
405
500
  } else if (memMax >= 8192) {
406
501
  gcArgs = [
407
502
  '-XX:+ParallelRefProcEnabled',
408
- '-XX:MaxGCPauseMillis=150',
409
503
  '-XX:+UnlockExperimentalVMOptions',
410
504
  '-XX:+DisableExplicitGC',
411
505
  '-XX:+AlwaysPreTouch',
@@ -416,30 +510,30 @@ world-settings:
416
510
  '-XX:G1HeapWastePercent=5',
417
511
  '-XX:G1MixedGCCountTarget=4',
418
512
  '-XX:InitiatingHeapOccupancyPercent=15',
419
- '-XX:G1MixedGCLiveThresholdPercent=90',
420
513
  '-XX:G1RSetUpdatingPauseTimePercent=5',
421
514
  '-XX:SurvivorRatio=32',
422
515
  '-XX:+PerfDisableSharedMem',
423
516
  '-XX:MaxTenuringThreshold=1'
424
517
  ];
425
518
  } else {
426
- gcArgs = this.config.memory.useAikarsFlags ? [
519
+ gcArgs = [
427
520
  '-XX:+ParallelRefProcEnabled',
428
- '-XX:MaxGCPauseMillis=200',
429
521
  '-XX:+UnlockExperimentalVMOptions',
430
522
  '-XX:+DisableExplicitGC',
431
523
  '-XX:+AlwaysPreTouch',
432
524
  '-XX:G1HeapWastePercent=5',
433
525
  '-XX:G1MixedGCCountTarget=4',
434
526
  '-XX:InitiatingHeapOccupancyPercent=15',
435
- '-XX:G1MixedGCLiveThresholdPercent=90',
436
527
  '-XX:G1RSetUpdatingPauseTimePercent=5',
437
528
  '-XX:SurvivorRatio=32',
438
529
  '-XX:+PerfDisableSharedMem',
439
- '-XX:MaxTenuringThreshold=1',
440
- '-Dusing.aikars.flags=https://mcflags.emc.gs',
441
- '-Daikars.new.flags=true'
442
- ] : [];
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
+ }
443
537
  }
444
538
 
445
539
  if (javaVersion === '21') {
@@ -472,167 +566,41 @@ world-settings:
472
566
  return env;
473
567
  }
474
568
 
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();
569
+ private setupLogHandlers(): void {
570
+ this.process.stdout.on('data', (data: Buffer) => {
571
+ const output = data.toString();
572
+ process.stdout.write(output);
598
573
 
599
- if (this.config.backup.enabled) {
600
- this.setupBackups();
601
- }
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
+ }
602
591
 
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
- }
592
+ if (this.owners.size > 0) {
593
+ this.logger.info(`Owner commands enabled with prefix: ${this.ownerCommandPrefix}`);
594
+ }
619
595
 
620
- if (this.owners.size > 0) {
621
- this.logger.info(`Owner commands enabled with prefix: ${this.ownerCommandPrefix}`);
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();
622
602
  }
623
-
624
- this.emit('ready', this.serverInfo);
625
- this.startMemoryMonitor();
626
603
  }
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
604
 
637
605
  if (output.includes('joined the game')) {
638
606
  const match = output.match(/(\w+) joined the game/);
@@ -969,7 +937,8 @@ world-settings:
969
937
  this.emit('resource', this.serverInfo);
970
938
 
971
939
  } catch (error) {
972
- 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}`);
973
942
  }
974
943
  }
975
944
 
@@ -1003,24 +972,23 @@ world-settings:
1003
972
  if (isIncreasing) {
1004
973
  this.logger.warning('Memory leak detected!');
1005
974
 
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');
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');
1018
985
  }
1019
986
  }
1020
987
  }
1021
988
 
1022
989
  } catch (error) {
1023
- this.logger.error('Memory monitor error:', error);
990
+ const errorMessage = error instanceof Error ? error.message : String(error);
991
+ this.logger.error(`Memory monitor error: ${errorMessage}`);
1024
992
  }
1025
993
  }, interval);
1026
994
  }
@@ -1050,6 +1018,145 @@ world-settings:
1050
1018
  await this.start();
1051
1019
  }
1052
1020
 
1021
+ public async start(): Promise<ServerInfo> {
1022
+ this.logger.info(`Starting ${this.config.type} server v${this.config.version}...`);
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
+
1032
+ if (this.owners.size > 0) {
1033
+ this.logger.info(`Owners configured: ${Array.from(this.owners).join(', ')}`);
1034
+ }
1035
+
1036
+ this.javaInfo = await JavaChecker.ensureJava(
1037
+ this.config.version,
1038
+ this.options.usePortableJava || false
1039
+ );
1040
+ this.javaCommand = this.javaInfo.path;
1041
+ this.logger.success(`Using Java ${this.javaInfo.version} (${this.javaInfo.type}) at ${this.javaInfo.path}`);
1042
+
1043
+ const systemInfo = SystemDetector.getSystemInfo();
1044
+ this.logger.debug('System info:', systemInfo);
1045
+
1046
+ const serverDir = process.cwd();
1047
+ await FileUtils.ensureServerStructure(this.config);
1048
+
1049
+ this.worldSize = await this.calculateWorldSize();
1050
+ if (this.worldSize > 10 * 1024 * 1024 * 1024) {
1051
+ this.logger.warning(`Large world detected (${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB). Consider increasing memory allocation.`);
1052
+ }
1053
+
1054
+ const jarPath = await this.engine.download(this.config, serverDir);
1055
+
1056
+ if (this.config.type === 'forge') {
1057
+ await this.engine.prepare(this.config, serverDir, jarPath);
1058
+ } else {
1059
+ await this.engine.prepare(this.config, serverDir);
1060
+ }
1061
+
1062
+ if (this.config.platform === 'all') {
1063
+ await FileUtils.ensureDir(this.config.folders.plugins);
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
+ }
1081
+ }
1082
+
1083
+ if (this.options.enableViaVersion !== false) {
1084
+ this.logger.info('Enabling ViaVersion for client version compatibility...');
1085
+ await FileUtils.ensureDir(this.config.folders.plugins);
1086
+ await this.viaVersion.setup(this.config);
1087
+ await this.viaVersion.configureViaVersion(this.config);
1088
+
1089
+ if (this.options.enableViaBackwards !== false) {
1090
+ this.logger.info('ViaBackwards will be installed');
1091
+ }
1092
+ if (this.options.enableViaRewind !== false) {
1093
+ this.logger.info('ViaRewind will be installed');
1094
+ }
1095
+ }
1096
+
1097
+ if (this.options.enableSkinRestorer !== false) {
1098
+ this.logger.info('Enabling SkinRestorer for player skins...');
1099
+ await FileUtils.ensureDir(this.config.folders.plugins);
1100
+ await this.skinRestorer.setup(this.config);
1101
+ }
1102
+
1103
+ const javaArgs = this.buildJavaArgs();
1104
+ const serverJar = this.engine.getServerJar(jarPath);
1105
+ const serverArgs = this.engine.getServerArgs();
1106
+
1107
+ const fullArgs = [
1108
+ ...javaArgs,
1109
+ '-jar',
1110
+ serverJar,
1111
+ ...serverArgs
1112
+ ];
1113
+
1114
+ const env = this.buildEnvironment();
1115
+
1116
+ this.logger.info(`Launching: ${this.javaCommand} ${fullArgs.join(' ')}`);
1117
+
1118
+ this.process = spawn(this.javaCommand, fullArgs, {
1119
+ cwd: serverDir,
1120
+ env: env,
1121
+ stdio: ['pipe', 'pipe', 'pipe']
1122
+ });
1123
+
1124
+ this.serverInfo.pid = this.process.pid!;
1125
+ this.serverInfo.status = 'starting';
1126
+ this.startTime = new Date();
1127
+
1128
+ if (this.options.silentMode) {
1129
+ this.process.stdout.pipe(process.stdout);
1130
+ this.process.stderr.pipe(process.stderr);
1131
+ } else {
1132
+ this.setupLogHandlers();
1133
+ }
1134
+
1135
+ this.process.on('exit', (code: number) => {
1136
+ this.serverInfo.status = 'stopped';
1137
+ this.logger.warning(`Server stopped with code ${code}`);
1138
+
1139
+ if (this.config.autoRestart && code !== 0) {
1140
+ this.logger.info('Auto-restarting...');
1141
+ this.start();
1142
+ }
1143
+
1144
+ this.emit('stop', { code });
1145
+ });
1146
+
1147
+ if (this.options.statsInterval && this.options.statsInterval > 0) {
1148
+ this.statsInterval = setInterval(() => this.updateStats(), this.options.statsInterval);
1149
+ }
1150
+
1151
+ this.monitorResources();
1152
+
1153
+ if (this.config.backup.enabled) {
1154
+ this.setupBackups();
1155
+ }
1156
+
1157
+ return this.serverInfo;
1158
+ }
1159
+
1053
1160
  public async stop(): Promise<void> {
1054
1161
  if (!this.process) {
1055
1162
  this.logger.warning('Server not running');
@@ -1077,6 +1184,10 @@ world-settings:
1077
1184
  this.cleanLogsCron.stop();
1078
1185
  }
1079
1186
 
1187
+ if (this.ramdiskBackupCron) {
1188
+ this.ramdiskBackupCron.stop();
1189
+ }
1190
+
1080
1191
  if (this.memoryMonitorInterval) {
1081
1192
  clearInterval(this.memoryMonitorInterval);
1082
1193
  }
@@ -1089,7 +1200,10 @@ world-settings:
1089
1200
  this.geyser.stop();
1090
1201
  }
1091
1202
 
1092
- this.cleanPaperCache();
1203
+ if (this.options.ramdisk?.enabled) {
1204
+ await this.backupRamdiskToMaster();
1205
+ }
1206
+
1093
1207
  this.logger.success('Server stopped');
1094
1208
  }
1095
1209
 
@@ -1200,33 +1314,4 @@ world-settings:
1200
1314
  }
1201
1315
  return value;
1202
1316
  }
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
1317
  }