@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.
@@ -51,6 +51,7 @@ const FabricEngine_1 = require("../engines/FabricEngine");
51
51
  const GeyserBridge_1 = require("../platforms/GeyserBridge");
52
52
  const ViaVersion_1 = require("../platforms/ViaVersion");
53
53
  const SkinRestorer_1 = require("../platforms/SkinRestorer");
54
+ const PluginManager_1 = require("../platforms/PluginManager");
54
55
  class MinecraftServer extends events_1.EventEmitter {
55
56
  config;
56
57
  options;
@@ -59,11 +60,13 @@ class MinecraftServer extends events_1.EventEmitter {
59
60
  geyser;
60
61
  viaVersion;
61
62
  skinRestorer;
63
+ pluginManager;
62
64
  process = null;
63
65
  serverInfo;
64
66
  players = new Map();
65
67
  backupCron = null;
66
68
  cleanLogsCron = null;
69
+ ramdiskBackupCron = null;
67
70
  startTime = null;
68
71
  memoryMonitorInterval = null;
69
72
  statsInterval = null;
@@ -80,39 +83,75 @@ class MinecraftServer extends events_1.EventEmitter {
80
83
  cgroupMemory = 0;
81
84
  cgroupCpu = 0;
82
85
  isPterodactyl = false;
86
+ ramdiskPaths = new Map();
87
+ serverReady = false;
83
88
  constructor(userConfig = {}) {
84
89
  super();
85
90
  this.logger = Logger_1.Logger.getInstance();
86
91
  this.logger.banner();
87
92
  this.detectEnvironment();
93
+ const defaultMemoryMonitor = {
94
+ enabled: true,
95
+ threshold: 90,
96
+ interval: 30000,
97
+ action: 'warn',
98
+ maxMemoryLeakRestarts: 3,
99
+ gcOnWarning: true
100
+ };
101
+ const defaultNetworkOptimization = {
102
+ tcpFastOpen: true,
103
+ tcpNoDelay: true,
104
+ preferIPv4: true,
105
+ compressionThreshold: 64,
106
+ bungeeMode: false,
107
+ proxyProtocol: false,
108
+ velocity: {
109
+ enabled: false,
110
+ secret: '',
111
+ forwardingMode: 'modern'
112
+ }
113
+ };
114
+ const defaultOwnerCommands = {
115
+ prefix: '!',
116
+ enabled: true,
117
+ usernames: [],
118
+ permissions: []
119
+ };
120
+ const defaultRamdisk = {
121
+ enabled: false,
122
+ world: false,
123
+ plugins: false,
124
+ addons: false,
125
+ backupInterval: 300000,
126
+ masterStorage: '/home/minecraft-master',
127
+ size: '1G',
128
+ mountPoint: '/dev/shm/minecraft'
129
+ };
88
130
  this.options = {
89
131
  javaVersion: 'auto',
90
132
  usePortableJava: !this.isPterodactyl,
91
- memoryMonitor: {
92
- enabled: true,
93
- threshold: 90,
94
- interval: 30000,
95
- action: 'warn'
96
- },
133
+ memoryMonitor: defaultMemoryMonitor,
97
134
  autoInstallJava: true,
98
- networkOptimization: {
99
- tcpFastOpen: true,
100
- bungeeMode: false,
101
- proxyProtocol: false
102
- },
135
+ networkOptimization: defaultNetworkOptimization,
103
136
  owners: [],
104
- ownerCommands: {
105
- prefix: '!',
106
- enabled: true
107
- },
137
+ ownerCommands: defaultOwnerCommands,
108
138
  silentMode: true,
109
139
  statsInterval: 30000,
110
140
  cleanLogsInterval: 3 * 60 * 60 * 1000,
111
141
  optimizeForPterodactyl: true,
142
+ ramdisk: defaultRamdisk,
143
+ symlinkMaster: '/home/minecraft-master',
144
+ enableProtocolLib: false,
145
+ enableTCPShield: false,
146
+ enableSpigotOptimizers: false,
147
+ enableSpark: false,
148
+ enableViewDistanceTweaks: false,
149
+ enableFarmControl: false,
150
+ enableEntityDetection: false,
112
151
  ...userConfig
113
152
  };
114
153
  if (this.options.owners) {
115
- this.options.owners.forEach(owner => this.owners.add(owner.toLowerCase()));
154
+ this.options.owners.forEach((owner) => this.owners.add(owner.toLowerCase()));
116
155
  }
117
156
  if (this.options.ownerCommands?.prefix) {
118
157
  this.ownerCommandPrefix = this.options.ownerCommands.prefix;
@@ -126,6 +165,7 @@ class MinecraftServer extends events_1.EventEmitter {
126
165
  this.geyser = new GeyserBridge_1.GeyserBridge();
127
166
  this.viaVersion = new ViaVersion_1.ViaVersionManager();
128
167
  this.skinRestorer = new SkinRestorer_1.SkinRestorerManager();
168
+ this.pluginManager = new PluginManager_1.PluginManager();
129
169
  this.serverInfo = {
130
170
  pid: 0,
131
171
  ip: this.config.network.ip,
@@ -142,27 +182,60 @@ class MinecraftServer extends events_1.EventEmitter {
142
182
  status: 'stopped'
143
183
  };
144
184
  this.detectCgroupLimits();
145
- this.cleanPaperCache();
146
185
  }
147
186
  detectEnvironment() {
148
187
  this.isPterodactyl = !!(process.env.PTERODACTYL ||
149
188
  process.env.SERVER_PORT ||
150
- process.env.SERVER_MEMORY_MAX ||
151
189
  fs.existsSync('/home/container'));
152
190
  if (this.isPterodactyl) {
153
191
  this.logger.info('Pterodactyl environment detected');
192
+ const serverPort = process.env.SERVER_PORT;
193
+ if (serverPort) {
194
+ this.config.network.port = parseInt(serverPort);
195
+ this.logger.info(`Using Pterodactyl Java port: ${serverPort}`);
196
+ }
197
+ const bedrockPort = process.env.BEDROCK_PORT;
198
+ if (bedrockPort) {
199
+ this.config.network.bedrockPort = parseInt(bedrockPort);
200
+ this.logger.info(`Using Pterodactyl Bedrock port: ${bedrockPort}`);
201
+ }
202
+ else {
203
+ this.logger.warning('No BEDROCK_PORT environment variable found. Geyser will share Java port.');
204
+ this.logger.warning('Make sure UDP is enabled for this port in Pterodactyl panel');
205
+ }
206
+ }
207
+ }
208
+ detectCgroupLimits() {
209
+ try {
210
+ if (fs.existsSync('/sys/fs/cgroup/memory/memory.limit_in_bytes')) {
211
+ const limit = fs.readFileSync('/sys/fs/cgroup/memory/memory.limit_in_bytes', 'utf8');
212
+ this.cgroupMemory = parseInt(limit) / 1024 / 1024;
213
+ this.logger.debug(`Cgroup memory limit: ${this.cgroupMemory} MB`);
214
+ }
215
+ if (fs.existsSync('/sys/fs/cgroup/cpu/cpu.cfs_quota_us') && fs.existsSync('/sys/fs/cgroup/cpu/cpu.cfs_period_us')) {
216
+ const quota = parseInt(fs.readFileSync('/sys/fs/cgroup/cpu/cpu.cfs_quota_us', 'utf8'));
217
+ const period = parseInt(fs.readFileSync('/sys/fs/cgroup/cpu/cpu.cfs_period_us', 'utf8'));
218
+ if (quota > 0 && period > 0) {
219
+ this.cgroupCpu = quota / period;
220
+ this.logger.debug(`Cgroup CPU limit: ${this.cgroupCpu} cores`);
221
+ }
222
+ }
223
+ }
224
+ catch (error) {
225
+ this.logger.debug('Not running in cgroup environment');
154
226
  }
155
227
  }
156
228
  optimizeForPterodactylPanel() {
157
229
  const pterodactylPort = process.env.SERVER_PORT;
158
230
  if (pterodactylPort) {
159
231
  this.config.network.port = parseInt(pterodactylPort);
160
- this.logger.info(`Using Pterodactyl port: ${pterodactylPort}`);
161
232
  }
162
233
  const pterodactylMemory = process.env.SERVER_MEMORY_MAX;
163
234
  if (pterodactylMemory) {
164
- this.config.memory.max = pterodactylMemory;
165
- this.logger.info(`Using Pterodactyl memory: ${pterodactylMemory}`);
235
+ const memValue = parseInt(pterodactylMemory);
236
+ const safeMemory = Math.floor(memValue * 0.9);
237
+ this.config.memory.max = `${safeMemory}M`;
238
+ this.logger.info(`Pterodactyl memory limit detected, using 90% safe value: ${safeMemory}MB`);
166
239
  }
167
240
  const pterodactylInitMemory = process.env.SERVER_MEMORY_INIT;
168
241
  if (pterodactylInitMemory) {
@@ -170,27 +243,119 @@ class MinecraftServer extends events_1.EventEmitter {
170
243
  }
171
244
  this.config.world.viewDistance = 6;
172
245
  this.config.world.simulationDistance = 4;
173
- this.logger.info('Applied Pterodactyl optimizations');
174
246
  }
175
- cleanPaperCache() {
176
- const cachePaths = [
177
- path.join(process.cwd(), 'plugins', '.paper-remapped'),
178
- path.join(process.cwd(), 'plugins', '.paper-remapped', 'geyser.jar'),
179
- path.join(process.cwd(), 'plugins', '.paper-remapped', 'ViaVersion.jar'),
180
- path.join(process.cwd(), 'cache', 'paper-remapped')
247
+ createEngine() {
248
+ switch (this.config.type) {
249
+ case 'paper':
250
+ case 'purpur':
251
+ case 'spigot':
252
+ return new PaperEngine_1.PaperEngine();
253
+ case 'vanilla':
254
+ return new VanillaEngine_1.VanillaEngine();
255
+ case 'forge':
256
+ return new ForgeEngine_1.ForgeEngine();
257
+ case 'fabric':
258
+ return new FabricEngine_1.FabricEngine();
259
+ default:
260
+ throw new Error(`Unsupported server type: ${this.config.type}`);
261
+ }
262
+ }
263
+ async setupSymlinkMaster() {
264
+ if (!this.options.symlinkMaster)
265
+ return;
266
+ const masterDir = this.options.symlinkMaster;
267
+ await fs.ensureDir(masterDir);
268
+ const subDirs = ['worlds', 'plugins', 'addons', 'mods'];
269
+ for (const dir of subDirs) {
270
+ const masterSubDir = path.join(masterDir, dir);
271
+ await fs.ensureDir(masterSubDir);
272
+ }
273
+ this.logger.info(`Symlink master directory created at ${masterDir}`);
274
+ }
275
+ async restoreFromMaster() {
276
+ if (!this.options.ramdisk?.enabled)
277
+ return;
278
+ const ramdiskBase = '/dev/shm/minecraft';
279
+ const items = [
280
+ { name: 'world', target: this.config.folders.world },
281
+ { name: 'plugins', target: this.config.folders.plugins },
282
+ { name: 'addons', target: this.config.folders.addons }
181
283
  ];
182
- cachePaths.forEach(cachePath => {
183
- if (fs.existsSync(cachePath)) {
184
- this.logger.info(`Cleaning corrupt cache: ${cachePath}`);
185
- try {
186
- fs.rmSync(cachePath, { recursive: true, force: true });
187
- this.logger.debug(`Cache cleaned: ${cachePath}`);
284
+ for (const item of items) {
285
+ const ramdiskPath = path.join(ramdiskBase, item.name);
286
+ const masterPath = path.join(this.options.ramdisk.masterStorage, item.name);
287
+ if (fs.existsSync(ramdiskPath)) {
288
+ const ramdiskFiles = fs.readdirSync(ramdiskPath);
289
+ if (ramdiskFiles.length > 0) {
290
+ this.logger.debug(`RAM disk ${item.name} already has data, skipping restore`);
291
+ continue;
188
292
  }
189
- catch (err) {
190
- this.logger.warning(`Failed to clean cache: ${err.message}`);
293
+ }
294
+ if (fs.existsSync(masterPath)) {
295
+ const masterFiles = fs.readdirSync(masterPath);
296
+ if (masterFiles.length > 0) {
297
+ this.logger.info(`Restoring ${item.name} from master storage...`);
298
+ await fs.ensureDir(ramdiskPath);
299
+ await fs.copy(masterPath, ramdiskPath, { overwrite: true });
300
+ if (!fs.existsSync(item.target)) {
301
+ await fs.symlink(ramdiskPath, item.target, 'dir');
302
+ }
191
303
  }
192
304
  }
193
- });
305
+ }
306
+ }
307
+ async setupRamdisk() {
308
+ if (!this.options.ramdisk?.enabled)
309
+ return;
310
+ const ramdiskBase = '/dev/shm/minecraft';
311
+ await fs.ensureDir(ramdiskBase);
312
+ await this.restoreFromMaster();
313
+ const items = [
314
+ { name: 'world', enabled: this.options.ramdisk.world, target: this.config.folders.world },
315
+ { name: 'plugins', enabled: this.options.ramdisk.plugins, target: this.config.folders.plugins },
316
+ { name: 'addons', enabled: this.options.ramdisk.addons, target: this.config.folders.addons }
317
+ ];
318
+ for (const item of items) {
319
+ if (!item.enabled)
320
+ continue;
321
+ const ramdiskPath = path.join(ramdiskBase, item.name);
322
+ const masterPath = path.join(this.options.ramdisk.masterStorage, item.name);
323
+ await fs.ensureDir(masterPath);
324
+ if (fs.existsSync(item.target)) {
325
+ const stat = await fs.lstat(item.target);
326
+ if (stat.isSymbolicLink()) {
327
+ continue;
328
+ }
329
+ await fs.copy(item.target, masterPath, { overwrite: true });
330
+ await fs.remove(item.target);
331
+ }
332
+ await fs.ensureDir(ramdiskPath);
333
+ await fs.symlink(ramdiskPath, item.target, 'dir');
334
+ this.ramdiskPaths.set(item.name, ramdiskPath);
335
+ this.logger.info(`RAM disk created for ${item.name} at ${ramdiskPath}`);
336
+ }
337
+ if (this.options.ramdisk.backupInterval > 0) {
338
+ const minutes = Math.floor(this.options.ramdisk.backupInterval / 60000);
339
+ this.ramdiskBackupCron = cron.schedule(`*/${minutes} * * * *`, () => {
340
+ this.backupRamdiskToMaster();
341
+ });
342
+ this.logger.info(`RAM disk backup scheduled every ${minutes} minutes`);
343
+ }
344
+ }
345
+ async backupRamdiskToMaster() {
346
+ if (!this.options.ramdisk?.enabled)
347
+ return;
348
+ for (const [name, ramdiskPath] of this.ramdiskPaths) {
349
+ const masterPath = path.join(this.options.ramdisk.masterStorage, name);
350
+ try {
351
+ await fs.copy(ramdiskPath, masterPath, { overwrite: true });
352
+ this.logger.debug(`RAM disk ${name} backed up to master storage`);
353
+ }
354
+ catch (error) {
355
+ const errorMessage = error instanceof Error ? error.message : String(error);
356
+ this.logger.error(`Failed to backup ${name} RAM disk: ${errorMessage}`);
357
+ }
358
+ }
194
359
  }
195
360
  cleanOldLogs() {
196
361
  const logsDir = path.join(process.cwd(), 'logs');
@@ -211,150 +376,78 @@ class MinecraftServer extends events_1.EventEmitter {
211
376
  this.logger.info('Old logs cleaned');
212
377
  }
213
378
  catch (err) {
214
- this.logger.warning(`Failed to clean logs: ${err.message}`);
379
+ const errorMessage = err instanceof Error ? err.message : String(err);
380
+ this.logger.warning(`Failed to clean logs: ${errorMessage}`);
215
381
  }
216
382
  }
217
- generateOptimizedConfigs() {
218
- const serverDir = process.cwd();
219
- const paperGlobalYml = path.join(serverDir, 'paper-global.yml');
220
- if (!fs.existsSync(paperGlobalYml)) {
221
- const paperGlobalConfig = `# Paper Global Configuration
222
- # Optimized by MC-Headless
223
-
224
- chunk-loading:
225
- autoconfig-send-distance: false
226
- enable-frustum-priority: false
227
- global-max-chunk-load-rate: 100
228
- global-max-chunk-send-rate: 50
229
-
230
- entities:
231
- despawn-ranges:
232
- monster:
233
- soft: 28
234
- hard: 96
235
- animal:
236
- soft: 16
237
- hard: 32
238
- spawn-limits:
239
- ambient: 5
240
- axolotls: 5
241
- creature: 10
242
- monster: 15
243
- water_ambient: 5
244
- water_creature: 5
245
-
246
- misc:
247
- max-entity-speed: 100
248
- redstone-implementation: ALTERNATIVE_CURRENT
249
-
250
- player-auto-save: 6000
251
- player-auto-save-rate: 1200
252
- no-tick-view-distance: 12
253
- `;
254
- fs.writeFileSync(paperGlobalYml, paperGlobalConfig);
255
- this.logger.debug('Generated paper-global.yml');
256
- }
257
- const paperWorldDefaultsYml = path.join(serverDir, 'paper-world-defaults.yml');
258
- if (!fs.existsSync(paperWorldDefaultsYml)) {
259
- const paperWorldConfig = `# Paper World Defaults
260
- # Optimized by MC-Headless
261
-
262
- view-distance: 6
263
- no-tick-view-distance: 12
264
-
265
- despawn-ranges:
266
- monster:
267
- soft: 28
268
- hard: 96
269
- animal:
270
- soft: 16
271
- hard: 32
272
- ambient:
273
- soft: 16
274
- hard: 32
275
- `;
276
- fs.writeFileSync(paperWorldDefaultsYml, paperWorldConfig);
277
- this.logger.debug('Generated paper-world-defaults.yml');
278
- }
279
- const spigotYml = path.join(serverDir, 'spigot.yml');
280
- if (!fs.existsSync(spigotYml)) {
281
- const spigotConfig = `# Spigot Configuration
282
- # Optimized by MC-Headless
283
-
284
- entity-activation-range:
285
- animals: 16
286
- monsters: 24
287
- misc: 8
288
- water: 8
289
-
290
- mob-spawn-range: 4
291
-
292
- world-settings:
293
- default:
294
- view-distance: 6
295
- mob-spawn-range: 4
296
- entity-activation-range:
297
- animals: 16
298
- monsters: 24
299
- misc: 8
300
- water: 8
301
- `;
302
- fs.writeFileSync(spigotYml, spigotConfig);
303
- this.logger.debug('Generated spigot.yml');
304
- }
305
- }
306
- detectCgroupLimits() {
383
+ async calculateWorldSize() {
307
384
  try {
308
- if (fs.existsSync('/sys/fs/cgroup/memory/memory.limit_in_bytes')) {
309
- const limit = fs.readFileSync('/sys/fs/cgroup/memory/memory.limit_in_bytes', 'utf8');
310
- this.cgroupMemory = parseInt(limit) / 1024 / 1024;
311
- this.logger.debug(`Cgroup memory limit: ${this.cgroupMemory} MB`);
312
- }
313
- if (fs.existsSync('/sys/fs/cgroup/cpu/cpu.cfs_quota_us') && fs.existsSync('/sys/fs/cgroup/cpu/cpu.cfs_period_us')) {
314
- const quota = parseInt(fs.readFileSync('/sys/fs/cgroup/cpu/cpu.cfs_quota_us', 'utf8'));
315
- const period = parseInt(fs.readFileSync('/sys/fs/cgroup/cpu/cpu.cfs_period_us', 'utf8'));
316
- if (quota > 0 && period > 0) {
317
- this.cgroupCpu = quota / period;
318
- this.logger.debug(`Cgroup CPU limit: ${this.cgroupCpu} cores`);
385
+ const worldPath = path.join(process.cwd(), this.config.folders.world);
386
+ if (!await fs.pathExists(worldPath))
387
+ return 0;
388
+ const getSize = async (dir) => {
389
+ let total = 0;
390
+ const files = await fs.readdir(dir);
391
+ for (const file of files) {
392
+ const filePath = path.join(dir, file);
393
+ const stat = await fs.stat(filePath);
394
+ if (stat.isDirectory()) {
395
+ total += await getSize(filePath);
396
+ }
397
+ else {
398
+ total += stat.size;
399
+ }
319
400
  }
320
- }
321
- }
322
- catch (error) {
323
- this.logger.debug('Not running in cgroup environment');
401
+ return total;
402
+ };
403
+ return await getSize(worldPath);
324
404
  }
325
- }
326
- createEngine() {
327
- switch (this.config.type) {
328
- case 'paper':
329
- case 'purpur':
330
- case 'spigot':
331
- return new PaperEngine_1.PaperEngine();
332
- case 'vanilla':
333
- return new VanillaEngine_1.VanillaEngine();
334
- case 'forge':
335
- return new ForgeEngine_1.ForgeEngine();
336
- case 'fabric':
337
- return new FabricEngine_1.FabricEngine();
338
- default:
339
- throw new Error(`Unsupported server type: ${this.config.type}`);
405
+ catch {
406
+ return 0;
340
407
  }
341
408
  }
342
409
  buildJavaArgs() {
343
410
  if (this.options.customJavaArgs && this.options.customJavaArgs.length > 0) {
344
411
  return this.options.customJavaArgs;
345
412
  }
346
- const memMax = this.parseMemory(this.config.memory.max);
413
+ let memMax = this.parseMemory(this.config.memory.max);
414
+ if (this.isPterodactyl && this.cgroupMemory > 0) {
415
+ const safeMem = Math.floor(this.cgroupMemory * 0.9);
416
+ memMax = Math.min(memMax, safeMem);
417
+ this.logger.info(`Adjusted memory to ${memMax}MB (90% of cgroup limit)`);
418
+ }
347
419
  const javaVersion = this.options.javaVersion || 'auto';
348
420
  const baseArgs = [
349
421
  `-Xms${this.config.memory.init}`,
350
- `-Xmx${this.config.memory.max}`,
351
- '-XX:+UseG1GC'
422
+ `-Xmx${memMax}M`,
423
+ '-XX:+UseG1GC',
424
+ '-XX:MaxGCPauseMillis=50',
425
+ '-XX:+UseStringDeduplication',
426
+ '-XX:G1MixedGCLiveThresholdPercent=90',
427
+ '-Dio.netty.recycler.maxCapacity.default=0',
428
+ '-Dio.netty.leakDetectionLevel=disabled',
429
+ '-Dterminal.jline=false',
430
+ '-Dterminal.ansi=true'
352
431
  ];
432
+ if (this.options.networkOptimization?.preferIPv4) {
433
+ baseArgs.unshift('-Djava.net.preferIPv4Stack=true');
434
+ }
435
+ if (this.options.networkOptimization?.tcpFastOpen) {
436
+ baseArgs.unshift('-Djava.net.tcpFastOpen=true');
437
+ }
438
+ if (this.options.networkOptimization?.tcpNoDelay) {
439
+ baseArgs.unshift('-Djava.net.tcp.nodelay=true');
440
+ }
441
+ if (this.options.networkOptimization?.compressionThreshold) {
442
+ baseArgs.push(`-Dnetwork-compression-threshold=${this.options.networkOptimization.compressionThreshold}`);
443
+ }
444
+ if (this.options.networkOptimization?.velocity?.enabled) {
445
+ baseArgs.push('-Dvelocity.secret=' + this.options.networkOptimization.velocity.secret);
446
+ }
353
447
  let gcArgs = [];
354
448
  if (memMax >= 16384) {
355
449
  gcArgs = [
356
450
  '-XX:+ParallelRefProcEnabled',
357
- '-XX:MaxGCPauseMillis=100',
358
451
  '-XX:+UnlockExperimentalVMOptions',
359
452
  '-XX:+DisableExplicitGC',
360
453
  '-XX:+AlwaysPreTouch',
@@ -365,7 +458,6 @@ world-settings:
365
458
  '-XX:G1HeapWastePercent=5',
366
459
  '-XX:G1MixedGCCountTarget=4',
367
460
  '-XX:InitiatingHeapOccupancyPercent=20',
368
- '-XX:G1MixedGCLiveThresholdPercent=90',
369
461
  '-XX:G1RSetUpdatingPauseTimePercent=5',
370
462
  '-XX:SurvivorRatio=32',
371
463
  '-XX:+PerfDisableSharedMem',
@@ -375,7 +467,6 @@ world-settings:
375
467
  else if (memMax >= 8192) {
376
468
  gcArgs = [
377
469
  '-XX:+ParallelRefProcEnabled',
378
- '-XX:MaxGCPauseMillis=150',
379
470
  '-XX:+UnlockExperimentalVMOptions',
380
471
  '-XX:+DisableExplicitGC',
381
472
  '-XX:+AlwaysPreTouch',
@@ -386,7 +477,6 @@ world-settings:
386
477
  '-XX:G1HeapWastePercent=5',
387
478
  '-XX:G1MixedGCCountTarget=4',
388
479
  '-XX:InitiatingHeapOccupancyPercent=15',
389
- '-XX:G1MixedGCLiveThresholdPercent=90',
390
480
  '-XX:G1RSetUpdatingPauseTimePercent=5',
391
481
  '-XX:SurvivorRatio=32',
392
482
  '-XX:+PerfDisableSharedMem',
@@ -394,23 +484,23 @@ world-settings:
394
484
  ];
395
485
  }
396
486
  else {
397
- gcArgs = this.config.memory.useAikarsFlags ? [
487
+ gcArgs = [
398
488
  '-XX:+ParallelRefProcEnabled',
399
- '-XX:MaxGCPauseMillis=200',
400
489
  '-XX:+UnlockExperimentalVMOptions',
401
490
  '-XX:+DisableExplicitGC',
402
491
  '-XX:+AlwaysPreTouch',
403
492
  '-XX:G1HeapWastePercent=5',
404
493
  '-XX:G1MixedGCCountTarget=4',
405
494
  '-XX:InitiatingHeapOccupancyPercent=15',
406
- '-XX:G1MixedGCLiveThresholdPercent=90',
407
495
  '-XX:G1RSetUpdatingPauseTimePercent=5',
408
496
  '-XX:SurvivorRatio=32',
409
497
  '-XX:+PerfDisableSharedMem',
410
- '-XX:MaxTenuringThreshold=1',
411
- '-Dusing.aikars.flags=https://mcflags.emc.gs',
412
- '-Daikars.new.flags=true'
413
- ] : [];
498
+ '-XX:MaxTenuringThreshold=1'
499
+ ];
500
+ if (this.config.memory.useAikarsFlags) {
501
+ gcArgs.push('-Dusing.aikars.flags=https://mcflags.emc.gs');
502
+ gcArgs.push('-Daikars.new.flags=true');
503
+ }
414
504
  }
415
505
  if (javaVersion === '21') {
416
506
  gcArgs.push('--enable-preview');
@@ -434,130 +524,34 @@ world-settings:
434
524
  }
435
525
  return env;
436
526
  }
437
- async start() {
438
- this.logger.info(`Starting ${this.config.type} server v${this.config.version}...`);
439
- this.cleanPaperCache();
440
- this.generateOptimizedConfigs();
441
- if (this.options.cleanLogsInterval && this.options.cleanLogsInterval > 0) {
442
- this.cleanLogsCron = cron.schedule('0 */3 * * *', () => this.cleanOldLogs());
443
- this.logger.info('Log cleanup scheduled every 3 hours');
444
- }
445
- if (this.owners.size > 0) {
446
- this.logger.info(`Owners configured: ${Array.from(this.owners).join(', ')}`);
447
- }
448
- this.javaInfo = await JavaChecker_1.JavaChecker.ensureJava(this.config.version, this.options.usePortableJava || false);
449
- this.javaCommand = this.javaInfo.path;
450
- this.logger.success(`Using Java ${this.javaInfo.version} (${this.javaInfo.type}) at ${this.javaInfo.path}`);
451
- const systemInfo = SystemDetector_1.SystemDetector.getSystemInfo();
452
- this.logger.debug('System info:', systemInfo);
453
- const serverDir = process.cwd();
454
- await FileUtils_1.FileUtils.ensureServerStructure(this.config);
455
- this.worldSize = await this.calculateWorldSize();
456
- if (this.worldSize > 10 * 1024 * 1024 * 1024) {
457
- this.logger.warning(`Large world detected (${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB). Consider increasing memory allocation.`);
458
- }
459
- const jarPath = await this.engine.download(this.config, serverDir);
460
- if (this.config.type === 'forge') {
461
- await this.engine.prepare(this.config, serverDir, jarPath);
462
- }
463
- else {
464
- await this.engine.prepare(this.config, serverDir);
465
- }
466
- if (this.config.platform === 'all') {
467
- await FileUtils_1.FileUtils.ensureDir(this.config.folders.plugins);
468
- await this.geyser.setup(this.config);
469
- }
470
- if (this.options.enableViaVersion !== false) {
471
- this.logger.info('Enabling ViaVersion for client version compatibility...');
472
- await FileUtils_1.FileUtils.ensureDir(this.config.folders.plugins);
473
- await this.viaVersion.setup(this.config);
474
- await this.viaVersion.configureViaVersion(this.config);
475
- if (this.options.enableViaBackwards !== false) {
476
- this.logger.info('ViaBackwards will be installed');
477
- }
478
- if (this.options.enableViaRewind !== false) {
479
- this.logger.info('ViaRewind will be installed');
480
- }
481
- }
482
- if (this.options.enableSkinRestorer !== false) {
483
- this.logger.info('Enabling SkinRestorer for player skins...');
484
- await FileUtils_1.FileUtils.ensureDir(this.config.folders.plugins);
485
- await this.skinRestorer.setup(this.config);
486
- }
487
- const javaArgs = this.buildJavaArgs();
488
- const serverJar = this.engine.getServerJar(jarPath);
489
- const serverArgs = this.engine.getServerArgs();
490
- const fullArgs = [
491
- ...javaArgs,
492
- '-jar',
493
- serverJar,
494
- ...serverArgs
495
- ];
496
- if (this.options.networkOptimization?.tcpFastOpen) {
497
- fullArgs.unshift('-Djava.net.preferIPv4Stack=true');
498
- }
499
- if (this.options.networkOptimization?.bungeeMode) {
500
- fullArgs.unshift('-Dnet.kyori.adventure.text.serializer.legacy.AMPMSupport=true');
501
- }
502
- const env = this.buildEnvironment();
503
- this.logger.info(`Launching: ${this.javaCommand} ${fullArgs.join(' ')}`);
504
- this.process = (0, child_process_1.spawn)(this.javaCommand, fullArgs, {
505
- cwd: serverDir,
506
- env: env,
507
- stdio: ['pipe', 'pipe', 'pipe']
508
- });
509
- this.serverInfo.pid = this.process.pid;
510
- this.serverInfo.status = 'starting';
511
- this.startTime = new Date();
512
- if (this.options.silentMode) {
513
- this.process.stdout.pipe(process.stdout);
514
- this.process.stderr.pipe(process.stderr);
515
- }
516
- else {
517
- this.setupLogHandlers();
518
- }
519
- this.process.on('exit', (code) => {
520
- this.serverInfo.status = 'stopped';
521
- this.logger.warning(`Server stopped with code ${code}`);
522
- if (this.config.autoRestart && code !== 0) {
523
- this.logger.info('Auto-restarting...');
524
- this.start();
525
- }
526
- this.emit('stop', { code });
527
- });
528
- if (this.options.statsInterval && this.options.statsInterval > 0) {
529
- this.statsInterval = setInterval(() => this.updateStats(), this.options.statsInterval);
530
- }
531
- this.monitorResources();
532
- if (this.config.backup.enabled) {
533
- this.setupBackups();
534
- }
535
- setTimeout(() => {
536
- if (this.serverInfo.status === 'starting') {
537
- this.serverInfo.status = 'running';
538
- this.logger.success('Server started successfully!');
539
- if (this.options.enableViaVersion !== false) {
540
- this.logger.info('ViaVersion is active - players from older versions can connect');
541
- }
542
- if (this.options.enableSkinRestorer !== false) {
543
- this.logger.info('SkinRestorer is active - player skins will be restored');
544
- }
545
- if (this.worldSize > 0) {
546
- this.logger.info(`World size: ${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB`);
547
- }
548
- if (this.owners.size > 0) {
549
- this.logger.info(`Owner commands enabled with prefix: ${this.ownerCommandPrefix}`);
550
- }
551
- this.emit('ready', this.serverInfo);
552
- this.startMemoryMonitor();
553
- }
554
- }, 10000);
555
- return this.serverInfo;
556
- }
557
527
  setupLogHandlers() {
558
528
  this.process.stdout.on('data', (data) => {
559
529
  const output = data.toString();
560
530
  process.stdout.write(output);
531
+ if (!this.serverReady) {
532
+ if (output.includes('Done') || output.includes('For help, type "help"')) {
533
+ this.serverReady = true;
534
+ this.serverInfo.status = 'running';
535
+ this.logger.success('Server started successfully!');
536
+ if (this.options.enableViaVersion !== false) {
537
+ this.logger.info('ViaVersion is active - players from older versions can connect');
538
+ }
539
+ if (this.options.enableSkinRestorer !== false) {
540
+ this.logger.info('SkinRestorer is active - player skins will be restored');
541
+ }
542
+ if (this.worldSize > 0) {
543
+ this.logger.info(`World size: ${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB`);
544
+ }
545
+ if (this.owners.size > 0) {
546
+ this.logger.info(`Owner commands enabled with prefix: ${this.ownerCommandPrefix}`);
547
+ }
548
+ if (this.options.ramdisk?.enabled) {
549
+ this.logger.info('RAM disk active - worlds/plugins running in memory');
550
+ }
551
+ this.emit('ready', this.serverInfo);
552
+ this.startMemoryMonitor();
553
+ }
554
+ }
561
555
  if (output.includes('joined the game')) {
562
556
  const match = output.match(/(\w+) joined the game/);
563
557
  if (match) {
@@ -856,7 +850,8 @@ world-settings:
856
850
  this.emit('resource', this.serverInfo);
857
851
  }
858
852
  catch (error) {
859
- this.logger.error('Stats update error:', error);
853
+ const errorMessage = error instanceof Error ? error.message : String(error);
854
+ this.logger.error(`Stats update error: ${errorMessage}`);
860
855
  }
861
856
  }
862
857
  startMemoryMonitor() {
@@ -882,24 +877,26 @@ world-settings:
882
877
  this.memoryUsageHistory[0] * 1.2;
883
878
  if (isIncreasing) {
884
879
  this.logger.warning('Memory leak detected!');
885
- switch (action) {
886
- case 'restart':
887
- this.logger.info('Restarting server due to memory leak...');
888
- await this.gracefulRestart();
889
- break;
890
- case 'stop':
891
- this.logger.info('Stopping server due to memory leak...');
892
- await this.stop();
893
- break;
894
- case 'warn':
895
- default:
896
- this.logger.warning('Please restart server to free memory');
880
+ if (action === 'restart' && this.players.size === 0) {
881
+ this.logger.info('No players online, restarting server due to memory leak...');
882
+ await this.gracefulRestart();
883
+ }
884
+ else if (action === 'stop' && this.players.size === 0) {
885
+ this.logger.info('No players online, stopping server due to memory leak...');
886
+ await this.stop();
887
+ }
888
+ else if (action === 'warn') {
889
+ this.logger.warning('Please restart server to free memory');
890
+ }
891
+ else {
892
+ this.logger.warning('Cannot restart: players online');
897
893
  }
898
894
  }
899
895
  }
900
896
  }
901
897
  catch (error) {
902
- this.logger.error('Memory monitor error:', error);
898
+ const errorMessage = error instanceof Error ? error.message : String(error);
899
+ this.logger.error(`Memory monitor error: ${errorMessage}`);
903
900
  }
904
901
  }, interval);
905
902
  }
@@ -918,6 +915,114 @@ world-settings:
918
915
  await this.stop();
919
916
  await this.start();
920
917
  }
918
+ async start() {
919
+ this.logger.info(`Starting ${this.config.type} server v${this.config.version}...`);
920
+ await this.setupSymlinkMaster();
921
+ await this.setupRamdisk();
922
+ if (this.options.cleanLogsInterval && this.options.cleanLogsInterval > 0) {
923
+ this.cleanLogsCron = cron.schedule('0 */3 * * *', () => this.cleanOldLogs());
924
+ this.logger.info('Log cleanup scheduled every 3 hours');
925
+ }
926
+ if (this.owners.size > 0) {
927
+ this.logger.info(`Owners configured: ${Array.from(this.owners).join(', ')}`);
928
+ }
929
+ this.javaInfo = await JavaChecker_1.JavaChecker.ensureJava(this.config.version, this.options.usePortableJava || false);
930
+ this.javaCommand = this.javaInfo.path;
931
+ this.logger.success(`Using Java ${this.javaInfo.version} (${this.javaInfo.type}) at ${this.javaInfo.path}`);
932
+ const systemInfo = SystemDetector_1.SystemDetector.getSystemInfo();
933
+ this.logger.debug('System info:', systemInfo);
934
+ const serverDir = process.cwd();
935
+ await FileUtils_1.FileUtils.ensureServerStructure(this.config);
936
+ this.worldSize = await this.calculateWorldSize();
937
+ if (this.worldSize > 10 * 1024 * 1024 * 1024) {
938
+ this.logger.warning(`Large world detected (${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB). Consider increasing memory allocation.`);
939
+ }
940
+ const jarPath = await this.engine.download(this.config, serverDir);
941
+ if (this.config.type === 'forge') {
942
+ await this.engine.prepare(this.config, serverDir, jarPath);
943
+ }
944
+ else {
945
+ await this.engine.prepare(this.config, serverDir);
946
+ }
947
+ if (this.config.platform === 'all') {
948
+ await FileUtils_1.FileUtils.ensureDir(this.config.folders.plugins);
949
+ await this.geyser.setup(this.config, this.isPterodactyl);
950
+ }
951
+ const pluginsToInstall = [
952
+ { name: 'ProtocolLib', enabled: this.options.enableProtocolLib, url: 'https://github.com/dmulloy2/ProtocolLib/releases/latest/download/ProtocolLib.jar' },
953
+ { name: 'TCPShield', enabled: this.options.enableTCPShield, url: 'https://tcpshield.com/api/download/plugin' },
954
+ { name: 'SpigotOptimizers', enabled: this.options.enableSpigotOptimizers, url: 'https://github.com/SpigotOptimizers/SpigotOptimizers/releases/latest/download/SpigotOptimizers.jar' },
955
+ { name: 'Spark', enabled: this.options.enableSpark, url: 'https://spark.lucko.me/download' },
956
+ { name: 'ViewDistanceTweaks', enabled: this.options.enableViewDistanceTweaks, url: 'https://github.com/ViewDistanceTweaks/ViewDistanceTweaks/releases/latest/download/ViewDistanceTweaks.jar' },
957
+ { name: 'FarmControl', enabled: this.options.enableFarmControl, url: 'https://github.com/FarmControl/FarmControl/releases/latest/download/FarmControl.jar' },
958
+ { name: 'EntityDetection', enabled: this.options.enableEntityDetection, url: 'https://github.com/EntityDetection/EntityDetection/releases/latest/download/EntityDetection.jar' }
959
+ ];
960
+ for (const plugin of pluginsToInstall) {
961
+ if (plugin.enabled) {
962
+ await this.pluginManager.installPlugin(plugin.name, plugin.url, this.config.folders.plugins);
963
+ }
964
+ }
965
+ if (this.options.enableViaVersion !== false) {
966
+ this.logger.info('Enabling ViaVersion for client version compatibility...');
967
+ await FileUtils_1.FileUtils.ensureDir(this.config.folders.plugins);
968
+ await this.viaVersion.setup(this.config);
969
+ await this.viaVersion.configureViaVersion(this.config);
970
+ if (this.options.enableViaBackwards !== false) {
971
+ this.logger.info('ViaBackwards will be installed');
972
+ }
973
+ if (this.options.enableViaRewind !== false) {
974
+ this.logger.info('ViaRewind will be installed');
975
+ }
976
+ }
977
+ if (this.options.enableSkinRestorer !== false) {
978
+ this.logger.info('Enabling SkinRestorer for player skins...');
979
+ await FileUtils_1.FileUtils.ensureDir(this.config.folders.plugins);
980
+ await this.skinRestorer.setup(this.config);
981
+ }
982
+ const javaArgs = this.buildJavaArgs();
983
+ const serverJar = this.engine.getServerJar(jarPath);
984
+ const serverArgs = this.engine.getServerArgs();
985
+ const fullArgs = [
986
+ ...javaArgs,
987
+ '-jar',
988
+ serverJar,
989
+ ...serverArgs
990
+ ];
991
+ const env = this.buildEnvironment();
992
+ this.logger.info(`Launching: ${this.javaCommand} ${fullArgs.join(' ')}`);
993
+ this.process = (0, child_process_1.spawn)(this.javaCommand, fullArgs, {
994
+ cwd: serverDir,
995
+ env: env,
996
+ stdio: ['pipe', 'pipe', 'pipe']
997
+ });
998
+ this.serverInfo.pid = this.process.pid;
999
+ this.serverInfo.status = 'starting';
1000
+ this.startTime = new Date();
1001
+ if (this.options.silentMode) {
1002
+ this.process.stdout.pipe(process.stdout);
1003
+ this.process.stderr.pipe(process.stderr);
1004
+ }
1005
+ else {
1006
+ this.setupLogHandlers();
1007
+ }
1008
+ this.process.on('exit', (code) => {
1009
+ this.serverInfo.status = 'stopped';
1010
+ this.logger.warning(`Server stopped with code ${code}`);
1011
+ if (this.config.autoRestart && code !== 0) {
1012
+ this.logger.info('Auto-restarting...');
1013
+ this.start();
1014
+ }
1015
+ this.emit('stop', { code });
1016
+ });
1017
+ if (this.options.statsInterval && this.options.statsInterval > 0) {
1018
+ this.statsInterval = setInterval(() => this.updateStats(), this.options.statsInterval);
1019
+ }
1020
+ this.monitorResources();
1021
+ if (this.config.backup.enabled) {
1022
+ this.setupBackups();
1023
+ }
1024
+ return this.serverInfo;
1025
+ }
921
1026
  async stop() {
922
1027
  if (!this.process) {
923
1028
  this.logger.warning('Server not running');
@@ -938,6 +1043,9 @@ world-settings:
938
1043
  if (this.cleanLogsCron) {
939
1044
  this.cleanLogsCron.stop();
940
1045
  }
1046
+ if (this.ramdiskBackupCron) {
1047
+ this.ramdiskBackupCron.stop();
1048
+ }
941
1049
  if (this.memoryMonitorInterval) {
942
1050
  clearInterval(this.memoryMonitorInterval);
943
1051
  }
@@ -947,7 +1055,9 @@ world-settings:
947
1055
  if (this.config.platform === 'all') {
948
1056
  this.geyser.stop();
949
1057
  }
950
- this.cleanPaperCache();
1058
+ if (this.options.ramdisk?.enabled) {
1059
+ await this.backupRamdiskToMaster();
1060
+ }
951
1061
  this.logger.success('Server stopped');
952
1062
  }
953
1063
  sendCommand(command) {
@@ -1043,32 +1153,6 @@ world-settings:
1043
1153
  }
1044
1154
  return value;
1045
1155
  }
1046
- async calculateWorldSize() {
1047
- try {
1048
- const worldPath = path.join(process.cwd(), this.config.folders.world);
1049
- if (!await fs.pathExists(worldPath))
1050
- return 0;
1051
- const getSize = async (dir) => {
1052
- let total = 0;
1053
- const files = await fs.readdir(dir);
1054
- for (const file of files) {
1055
- const filePath = path.join(dir, file);
1056
- const stat = await fs.stat(filePath);
1057
- if (stat.isDirectory()) {
1058
- total += await getSize(filePath);
1059
- }
1060
- else {
1061
- total += stat.size;
1062
- }
1063
- }
1064
- return total;
1065
- };
1066
- return await getSize(worldPath);
1067
- }
1068
- catch {
1069
- return 0;
1070
- }
1071
- }
1072
1156
  }
1073
1157
  exports.MinecraftServer = MinecraftServer;
1074
1158
  //# sourceMappingURL=MinecraftServer.js.map