@dimzxzzx07/mc-headless 1.6.0 → 1.8.0

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.
@@ -53,6 +53,7 @@ const FabricEngine_1 = require("../engines/FabricEngine");
53
53
  const GeyserBridge_1 = require("../platforms/GeyserBridge");
54
54
  const ViaVersion_1 = require("../platforms/ViaVersion");
55
55
  const path = __importStar(require("path"));
56
+ const fs = __importStar(require("fs-extra"));
56
57
  class MinecraftServer extends events_1.EventEmitter {
57
58
  config;
58
59
  options;
@@ -65,11 +66,44 @@ class MinecraftServer extends events_1.EventEmitter {
65
66
  players = new Map();
66
67
  backupCron = null;
67
68
  startTime = null;
69
+ memoryMonitorInterval = null;
70
+ memoryUsageHistory = [];
71
+ worldSize = 0;
72
+ playerCount = 0;
73
+ javaCommand = 'java';
74
+ owners = new Set();
75
+ ownerCommandPrefix = '!';
68
76
  constructor(userConfig = {}) {
69
77
  super();
70
78
  this.logger = Logger_1.Logger.getInstance();
71
79
  this.logger.banner();
72
- this.options = userConfig;
80
+ this.options = {
81
+ javaVersion: 'auto',
82
+ memoryMonitor: {
83
+ enabled: true,
84
+ threshold: 90,
85
+ interval: 10000,
86
+ action: 'restart'
87
+ },
88
+ autoInstallJava: true,
89
+ networkOptimization: {
90
+ tcpFastOpen: true,
91
+ bungeeMode: false,
92
+ proxyProtocol: false
93
+ },
94
+ owners: [],
95
+ ownerCommands: {
96
+ prefix: '!',
97
+ enabled: true
98
+ },
99
+ ...userConfig
100
+ };
101
+ if (this.options.owners) {
102
+ this.options.owners.forEach(owner => this.owners.add(owner.toLowerCase()));
103
+ }
104
+ if (this.options.ownerCommands?.prefix) {
105
+ this.ownerCommandPrefix = this.options.ownerCommands.prefix;
106
+ }
73
107
  const handler = new ConfigHandler_1.ConfigHandler(userConfig);
74
108
  this.config = handler.getConfig();
75
109
  this.engine = this.createEngine();
@@ -106,13 +140,478 @@ class MinecraftServer extends events_1.EventEmitter {
106
140
  throw new Error(`Unsupported server type: ${this.config.type}`);
107
141
  }
108
142
  }
143
+ async detectJavaVersion() {
144
+ try {
145
+ const output = (0, child_process_1.execSync)('java -version 2>&1').toString();
146
+ if (output.includes('version "21')) {
147
+ return '21';
148
+ }
149
+ else if (output.includes('version "17')) {
150
+ return '17';
151
+ }
152
+ else if (output.includes('version "11')) {
153
+ return '11';
154
+ }
155
+ else if (output.includes('version "8')) {
156
+ return '8';
157
+ }
158
+ return 'unknown';
159
+ }
160
+ catch {
161
+ return 'none';
162
+ }
163
+ }
164
+ async ensureJava() {
165
+ if (!this.options.autoInstallJava) {
166
+ await JavaChecker_1.JavaChecker.ensureJava();
167
+ return;
168
+ }
169
+ const hasJava = await JavaChecker_1.JavaChecker.checkJava();
170
+ if (!hasJava) {
171
+ this.logger.info('Java not found, attempting to install...');
172
+ const osType = SystemDetector_1.SystemDetector.getOS();
173
+ const distro = SystemDetector_1.SystemDetector.getDistro();
174
+ const targetVersion = this.options.javaVersion === 'auto' ? '17' : this.options.javaVersion || '17';
175
+ if (osType === 'linux' || osType === 'android') {
176
+ await this.installJavaLinux(distro, targetVersion);
177
+ }
178
+ else if (osType === 'darwin') {
179
+ await this.installJavaMac(targetVersion);
180
+ }
181
+ else if (osType === 'windows') {
182
+ this.logger.error('Windows detected. Please install Java manually from https://adoptium.net');
183
+ throw new Error('Java not installed');
184
+ }
185
+ }
186
+ else {
187
+ const detectedVersion = await this.detectJavaVersion();
188
+ this.logger.info(`Detected Java version: ${detectedVersion}`);
189
+ if (this.options.javaVersion !== 'auto' && detectedVersion !== this.options.javaVersion) {
190
+ this.logger.warning(`Server configured for Java ${this.options.javaVersion} but detected ${detectedVersion}. This may cause issues.`);
191
+ }
192
+ if (detectedVersion === '21' || detectedVersion === '17') {
193
+ this.javaCommand = 'java';
194
+ this.logger.success(`Using Java ${detectedVersion}`);
195
+ }
196
+ else if (detectedVersion === '11' || detectedVersion === '8') {
197
+ this.logger.warning(`Java ${detectedVersion} is too old. Minecraft 1.21+ requires Java 17 or 21.`);
198
+ this.logger.info('Attempting to install Java 17...');
199
+ const distro = SystemDetector_1.SystemDetector.getDistro();
200
+ await this.installJavaLinux(distro, '17');
201
+ }
202
+ }
203
+ const finalVersion = await this.detectJavaVersion();
204
+ this.logger.success(`Java version: ${finalVersion}`);
205
+ }
206
+ async installJavaLinux(distro, version) {
207
+ return new Promise((resolve, reject) => {
208
+ let command = '';
209
+ const javaPackage = version === '21' ? 'openjdk-21-jre-headless' : 'openjdk-17-jre-headless';
210
+ if (distro === 'ubuntu' || distro === 'debian') {
211
+ command = `apt update && apt install -y ${javaPackage}`;
212
+ }
213
+ else if (distro === 'centos' || distro === 'fedora') {
214
+ command = `yum install -y java-${version}-openjdk-headless`;
215
+ }
216
+ else if (distro === 'arch') {
217
+ const archPackage = version === '21' ? 'jre21-openjdk-headless' : 'jre17-openjdk-headless';
218
+ command = `pacman -S --noconfirm ${archPackage}`;
219
+ }
220
+ else if (distro === 'termux') {
221
+ command = 'pkg install -y openjdk-17';
222
+ }
223
+ else {
224
+ this.logger.error('Unsupported Linux distribution. Please install Java manually.');
225
+ reject(new Error('Unsupported distribution'));
226
+ return;
227
+ }
228
+ this.logger.info(`Installing Java ${version} with: ${command}`);
229
+ const install = (0, child_process_1.exec)(command, (error) => {
230
+ if (error) {
231
+ reject(error);
232
+ }
233
+ else {
234
+ resolve();
235
+ }
236
+ });
237
+ if (install.stdout) {
238
+ install.stdout.pipe(process.stdout);
239
+ }
240
+ if (install.stderr) {
241
+ install.stderr.pipe(process.stderr);
242
+ }
243
+ });
244
+ }
245
+ async installJavaMac(version) {
246
+ return new Promise((resolve, reject) => {
247
+ const command = `brew install openjdk@${version}`;
248
+ this.logger.info(`Installing Java ${version} with: ${command}`);
249
+ const install = (0, child_process_1.exec)(command, (error) => {
250
+ if (error) {
251
+ reject(error);
252
+ }
253
+ else {
254
+ resolve();
255
+ }
256
+ });
257
+ if (install.stdout) {
258
+ install.stdout.pipe(process.stdout);
259
+ }
260
+ if (install.stderr) {
261
+ install.stderr.pipe(process.stderr);
262
+ }
263
+ });
264
+ }
265
+ async calculateWorldSize() {
266
+ try {
267
+ const worldPath = path.join(process.cwd(), this.config.folders.world);
268
+ if (!await fs.pathExists(worldPath))
269
+ return 0;
270
+ const getSize = async (dir) => {
271
+ let total = 0;
272
+ const files = await fs.readdir(dir);
273
+ for (const file of files) {
274
+ const filePath = path.join(dir, file);
275
+ const stat = await fs.stat(filePath);
276
+ if (stat.isDirectory()) {
277
+ total += await getSize(filePath);
278
+ }
279
+ else {
280
+ total += stat.size;
281
+ }
282
+ }
283
+ return total;
284
+ };
285
+ return await getSize(worldPath);
286
+ }
287
+ catch {
288
+ return 0;
289
+ }
290
+ }
291
+ buildJavaArgs() {
292
+ if (this.options.customJavaArgs && this.options.customJavaArgs.length > 0) {
293
+ return this.options.customJavaArgs;
294
+ }
295
+ const memMax = this.parseMemory(this.config.memory.max);
296
+ const javaVersion = this.options.javaVersion || 'auto';
297
+ let gcArgs = [];
298
+ if (memMax >= 16384) {
299
+ gcArgs = [
300
+ '-XX:+UseG1GC',
301
+ '-XX:+ParallelRefProcEnabled',
302
+ '-XX:MaxGCPauseMillis=100',
303
+ '-XX:+UnlockExperimentalVMOptions',
304
+ '-XX:+DisableExplicitGC',
305
+ '-XX:+AlwaysPreTouch',
306
+ '-XX:G1NewSizePercent=40',
307
+ '-XX:G1MaxNewSizePercent=50',
308
+ '-XX:G1HeapRegionSize=16M',
309
+ '-XX:G1ReservePercent=15',
310
+ '-XX:G1HeapWastePercent=5',
311
+ '-XX:G1MixedGCCountTarget=4',
312
+ '-XX:InitiatingHeapOccupancyPercent=20',
313
+ '-XX:G1MixedGCLiveThresholdPercent=90',
314
+ '-XX:G1RSetUpdatingPauseTimePercent=5',
315
+ '-XX:SurvivorRatio=32',
316
+ '-XX:+PerfDisableSharedMem',
317
+ '-XX:MaxTenuringThreshold=1'
318
+ ];
319
+ }
320
+ else if (memMax >= 8192) {
321
+ gcArgs = [
322
+ '-XX:+UseG1GC',
323
+ '-XX:+ParallelRefProcEnabled',
324
+ '-XX:MaxGCPauseMillis=150',
325
+ '-XX:+UnlockExperimentalVMOptions',
326
+ '-XX:+DisableExplicitGC',
327
+ '-XX:+AlwaysPreTouch',
328
+ '-XX:G1NewSizePercent=30',
329
+ '-XX:G1MaxNewSizePercent=40',
330
+ '-XX:G1HeapRegionSize=8M',
331
+ '-XX:G1ReservePercent=10',
332
+ '-XX:G1HeapWastePercent=5',
333
+ '-XX:G1MixedGCCountTarget=4',
334
+ '-XX:InitiatingHeapOccupancyPercent=15',
335
+ '-XX:G1MixedGCLiveThresholdPercent=90',
336
+ '-XX:G1RSetUpdatingPauseTimePercent=5',
337
+ '-XX:SurvivorRatio=32',
338
+ '-XX:+PerfDisableSharedMem',
339
+ '-XX:MaxTenuringThreshold=1'
340
+ ];
341
+ }
342
+ else {
343
+ gcArgs = this.config.memory.useAikarsFlags ? [
344
+ '-XX:+UseG1GC',
345
+ '-XX:+ParallelRefProcEnabled',
346
+ '-XX:MaxGCPauseMillis=200',
347
+ '-XX:+UnlockExperimentalVMOptions',
348
+ '-XX:+DisableExplicitGC',
349
+ '-XX:+AlwaysPreTouch',
350
+ '-XX:G1HeapWastePercent=5',
351
+ '-XX:G1MixedGCCountTarget=4',
352
+ '-XX:InitiatingHeapOccupancyPercent=15',
353
+ '-XX:G1MixedGCLiveThresholdPercent=90',
354
+ '-XX:G1RSetUpdatingPauseTimePercent=5',
355
+ '-XX:SurvivorRatio=32',
356
+ '-XX:+PerfDisableSharedMem',
357
+ '-XX:MaxTenuringThreshold=1',
358
+ '-Dusing.aikars.flags=https://mcflags.emc.gs',
359
+ '-Daikars.new.flags=true'
360
+ ] : [];
361
+ }
362
+ if (javaVersion === '21') {
363
+ gcArgs.push('--enable-preview');
364
+ }
365
+ const baseArgs = [
366
+ `-Xms${this.config.memory.init}`,
367
+ `-Xmx${this.config.memory.max}`
368
+ ];
369
+ return [...baseArgs, ...gcArgs];
370
+ }
371
+ processOwnerCommand(player, command) {
372
+ if (!this.options.ownerCommands?.enabled)
373
+ return;
374
+ if (!this.owners.has(player.toLowerCase()))
375
+ return;
376
+ const cmd = command.toLowerCase().trim();
377
+ const args = cmd.split(' ');
378
+ switch (args[0]) {
379
+ case 'gamemode':
380
+ case 'gm':
381
+ this.handleGamemodeCommand(player, args);
382
+ break;
383
+ case 'tp':
384
+ case 'teleport':
385
+ this.handleTeleportCommand(player, args);
386
+ break;
387
+ case 'give':
388
+ this.handleGiveCommand(player, args);
389
+ break;
390
+ case 'time':
391
+ this.handleTimeCommand(player, args);
392
+ break;
393
+ case 'weather':
394
+ this.handleWeatherCommand(player, args);
395
+ break;
396
+ case 'kill':
397
+ this.handleKillCommand(player, args);
398
+ break;
399
+ case 'ban':
400
+ this.handleBanCommand(player, args);
401
+ break;
402
+ case 'kick':
403
+ this.handleKickCommand(player, args);
404
+ break;
405
+ case 'op':
406
+ this.handleOpCommand(player, args);
407
+ break;
408
+ case 'deop':
409
+ this.handleDeopCommand(player, args);
410
+ break;
411
+ case 'reload':
412
+ this.sendCommand('reload');
413
+ this.logger.info(`${player} reloaded the server`);
414
+ break;
415
+ case 'save':
416
+ this.sendCommand('save-all');
417
+ this.logger.info(`${player} saved the world`);
418
+ break;
419
+ case 'list':
420
+ this.sendCommand('list');
421
+ break;
422
+ case 'help':
423
+ this.sendOwnerHelp(player);
424
+ break;
425
+ default:
426
+ this.sendCommand(command);
427
+ }
428
+ }
429
+ handleGamemodeCommand(player, args) {
430
+ if (args.length < 2) {
431
+ this.sendCommand(`tellraw ${player} {"text":"Usage: ${this.ownerCommandPrefix}gamemode <survival|creative|adventure|spectator> [player]","color":"red"}`);
432
+ return;
433
+ }
434
+ const gamemode = args[1];
435
+ const target = args.length > 2 ? args[2] : player;
436
+ let gamemodeNum = 0;
437
+ switch (gamemode) {
438
+ case 'survival':
439
+ case '0':
440
+ gamemodeNum = 0;
441
+ break;
442
+ case 'creative':
443
+ case '1':
444
+ gamemodeNum = 1;
445
+ break;
446
+ case 'adventure':
447
+ case '2':
448
+ gamemodeNum = 2;
449
+ break;
450
+ case 'spectator':
451
+ case '3':
452
+ gamemodeNum = 3;
453
+ break;
454
+ default:
455
+ this.sendCommand(`tellraw ${player} {"text":"Invalid gamemode. Use: survival, creative, adventure, spectator","color":"red"}`);
456
+ return;
457
+ }
458
+ this.sendCommand(`gamemode ${gamemodeNum} ${target}`);
459
+ this.logger.info(`${player} set gamemode of ${target} to ${gamemode}`);
460
+ }
461
+ handleTeleportCommand(player, args) {
462
+ if (args.length < 2) {
463
+ this.sendCommand(`tellraw ${player} {"text":"Usage: ${this.ownerCommandPrefix}tp <player> [x y z]","color":"red"}`);
464
+ return;
465
+ }
466
+ const target = args[1];
467
+ if (args.length >= 4) {
468
+ const x = args[2];
469
+ const y = args[3];
470
+ const z = args[4] || '0';
471
+ this.sendCommand(`tp ${target} ${x} ${y} ${z}`);
472
+ }
473
+ else {
474
+ this.sendCommand(`tp ${player} ${target}`);
475
+ }
476
+ this.logger.info(`${player} teleported to ${target}`);
477
+ }
478
+ handleGiveCommand(player, args) {
479
+ if (args.length < 3) {
480
+ this.sendCommand(`tellraw ${player} {"text":"Usage: ${this.ownerCommandPrefix}give <player> <item> [amount]","color":"red"}`);
481
+ return;
482
+ }
483
+ const target = args[1];
484
+ const item = args[2];
485
+ const amount = args.length > 3 ? args[3] : '1';
486
+ this.sendCommand(`give ${target} ${item} ${amount}`);
487
+ this.logger.info(`${player} gave ${amount} x ${item} to ${target}`);
488
+ }
489
+ handleTimeCommand(player, args) {
490
+ if (args.length < 2) {
491
+ this.sendCommand(`tellraw ${player} {"text":"Usage: ${this.ownerCommandPrefix}time <set|add|query> <value>","color":"red"}`);
492
+ return;
493
+ }
494
+ const subCmd = args[1];
495
+ const value = args.length > 2 ? args[2] : '';
496
+ if (subCmd === 'set') {
497
+ if (value === 'day') {
498
+ this.sendCommand('time set day');
499
+ }
500
+ else if (value === 'night') {
501
+ this.sendCommand('time set night');
502
+ }
503
+ else {
504
+ this.sendCommand(`time set ${value}`);
505
+ }
506
+ }
507
+ else if (subCmd === 'add') {
508
+ this.sendCommand(`time add ${value}`);
509
+ }
510
+ else if (subCmd === 'query') {
511
+ this.sendCommand('time query daytime');
512
+ }
513
+ this.logger.info(`${player} changed time: ${subCmd} ${value}`);
514
+ }
515
+ handleWeatherCommand(player, args) {
516
+ if (args.length < 2) {
517
+ this.sendCommand(`tellraw ${player} {"text":"Usage: ${this.ownerCommandPrefix}weather <clear|rain|thunder> [duration]","color":"red"}`);
518
+ return;
519
+ }
520
+ const weather = args[1];
521
+ const duration = args.length > 2 ? args[2] : '';
522
+ if (weather === 'clear') {
523
+ this.sendCommand('weather clear');
524
+ }
525
+ else if (weather === 'rain') {
526
+ this.sendCommand('weather rain');
527
+ }
528
+ else if (weather === 'thunder') {
529
+ this.sendCommand('weather thunder');
530
+ }
531
+ if (duration) {
532
+ this.sendCommand(`weather ${weather} ${duration}`);
533
+ }
534
+ this.logger.info(`${player} changed weather to ${weather}`);
535
+ }
536
+ handleKillCommand(player, args) {
537
+ const target = args.length > 1 ? args[1] : player;
538
+ this.sendCommand(`kill ${target}`);
539
+ this.logger.info(`${player} killed ${target}`);
540
+ }
541
+ handleBanCommand(player, args) {
542
+ if (args.length < 2) {
543
+ this.sendCommand(`tellraw ${player} {"text":"Usage: ${this.ownerCommandPrefix}ban <player> [reason]","color":"red"}`);
544
+ return;
545
+ }
546
+ const target = args[1];
547
+ const reason = args.slice(2).join(' ') || 'Banned by owner';
548
+ this.sendCommand(`ban ${target} ${reason}`);
549
+ this.logger.info(`${player} banned ${target}: ${reason}`);
550
+ }
551
+ handleKickCommand(player, args) {
552
+ if (args.length < 2) {
553
+ this.sendCommand(`tellraw ${player} {"text":"Usage: ${this.ownerCommandPrefix}kick <player> [reason]","color":"red"}`);
554
+ return;
555
+ }
556
+ const target = args[1];
557
+ const reason = args.slice(2).join(' ') || 'Kicked by owner';
558
+ this.sendCommand(`kick ${target} ${reason}`);
559
+ this.logger.info(`${player} kicked ${target}: ${reason}`);
560
+ }
561
+ handleOpCommand(player, args) {
562
+ if (args.length < 2) {
563
+ this.sendCommand(`tellraw ${player} {"text":"Usage: ${this.ownerCommandPrefix}op <player>","color":"red"}`);
564
+ return;
565
+ }
566
+ const target = args[1];
567
+ this.sendCommand(`op ${target}`);
568
+ this.logger.info(`${player} opped ${target}`);
569
+ }
570
+ handleDeopCommand(player, args) {
571
+ if (args.length < 2) {
572
+ this.sendCommand(`tellraw ${player} {"text":"Usage: ${this.ownerCommandPrefix}deop <player>","color":"red"}`);
573
+ return;
574
+ }
575
+ const target = args[1];
576
+ this.sendCommand(`deop ${target}`);
577
+ this.logger.info(`${player} deopped ${target}`);
578
+ }
579
+ sendOwnerHelp(player) {
580
+ const commands = [
581
+ `{"text":"\\n=== Owner Commands ===\\n","color":"gold","bold":true}`,
582
+ `{"text":"${this.ownerCommandPrefix}gamemode <mode> [player] - Change gamemode\\n","color":"yellow"}`,
583
+ `{"text":"${this.ownerCommandPrefix}tp <player> [x y z] - Teleport\\n","color":"yellow"}`,
584
+ `{"text":"${this.ownerCommandPrefix}give <player> <item> [amount] - Give items\\n","color":"yellow"}`,
585
+ `{"text":"${this.ownerCommandPrefix}time <set|add> <value> - Change time\\n","color":"yellow"}`,
586
+ `{"text":"${this.ownerCommandPrefix}weather <clear|rain|thunder> - Change weather\\n","color":"yellow"}`,
587
+ `{"text":"${this.ownerCommandPrefix}kill [player] - Kill player\\n","color":"yellow"}`,
588
+ `{"text":"${this.ownerCommandPrefix}ban <player> [reason] - Ban player\\n","color":"yellow"}`,
589
+ `{"text":"${this.ownerCommandPrefix}kick <player> [reason] - Kick player\\n","color":"yellow"}`,
590
+ `{"text":"${this.ownerCommandPrefix}op <player> - Give operator\\n","color":"yellow"}`,
591
+ `{"text":"${this.ownerCommandPrefix}deop <player> - Remove operator\\n","color":"yellow"}`,
592
+ `{"text":"${this.ownerCommandPrefix}reload - Reload server\\n","color":"yellow"}`,
593
+ `{"text":"${this.ownerCommandPrefix}save - Save world\\n","color":"yellow"}`,
594
+ `{"text":"${this.ownerCommandPrefix}list - List players\\n","color":"yellow"}`,
595
+ `{"text":"${this.ownerCommandPrefix}help - Show this help\\n","color":"yellow"}`
596
+ ];
597
+ commands.forEach(cmd => {
598
+ this.sendCommand(`tellraw ${player} ${cmd}`);
599
+ });
600
+ }
109
601
  async start() {
110
602
  this.logger.info(`Starting ${this.config.type} server v${this.config.version}...`);
111
- await JavaChecker_1.JavaChecker.ensureJava();
603
+ if (this.owners.size > 0) {
604
+ this.logger.info(`Owners configured: ${Array.from(this.owners).join(', ')}`);
605
+ }
606
+ await this.ensureJava();
112
607
  const systemInfo = SystemDetector_1.SystemDetector.getSystemInfo();
113
608
  this.logger.debug('System info:', systemInfo);
114
609
  const serverDir = process.cwd();
115
610
  await FileUtils_1.FileUtils.ensureServerStructure(this.config);
611
+ this.worldSize = await this.calculateWorldSize();
612
+ if (this.worldSize > 10 * 1024 * 1024 * 1024) {
613
+ this.logger.warning(`Large world detected (${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB). Consider increasing memory allocation.`);
614
+ }
116
615
  const jarPath = await this.engine.download(this.config, serverDir);
117
616
  if (this.config.type === 'forge') {
118
617
  await this.engine.prepare(this.config, serverDir, jarPath);
@@ -136,7 +635,7 @@ class MinecraftServer extends events_1.EventEmitter {
136
635
  this.logger.info('ViaRewind will be installed');
137
636
  }
138
637
  }
139
- const javaArgs = this.engine.getJavaArgs(this.config);
638
+ const javaArgs = this.buildJavaArgs();
140
639
  const serverJar = this.engine.getServerJar(jarPath);
141
640
  const serverArgs = this.engine.getServerArgs();
142
641
  const fullArgs = [
@@ -145,10 +644,16 @@ class MinecraftServer extends events_1.EventEmitter {
145
644
  serverJar,
146
645
  ...serverArgs
147
646
  ];
148
- this.logger.info(`Launching: java ${fullArgs.join(' ')}`);
149
- this.process = (0, child_process_1.spawn)('java', fullArgs, {
647
+ if (this.options.networkOptimization?.tcpFastOpen) {
648
+ fullArgs.unshift('-Djava.net.preferIPv4Stack=true');
649
+ }
650
+ if (this.options.networkOptimization?.bungeeMode) {
651
+ fullArgs.unshift('-Dnet.kyori.adventure.text.serializer.legacy.AMPMSupport=true');
652
+ }
653
+ this.logger.info(`Launching: ${this.javaCommand} ${fullArgs.join(' ')}`);
654
+ this.process = (0, child_process_1.spawn)(this.javaCommand, fullArgs, {
150
655
  cwd: serverDir,
151
- stdio: 'pipe'
656
+ stdio: ['pipe', 'pipe', 'pipe']
152
657
  });
153
658
  this.serverInfo.pid = this.process.pid;
154
659
  this.serverInfo.status = 'starting';
@@ -162,20 +667,43 @@ class MinecraftServer extends events_1.EventEmitter {
162
667
  if (this.options.enableViaVersion !== false) {
163
668
  this.logger.info('ViaVersion is active - players from older versions can connect');
164
669
  }
670
+ if (this.worldSize > 0) {
671
+ this.logger.info(`World size: ${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB`);
672
+ }
673
+ if (this.owners.size > 0) {
674
+ this.logger.info(`Owner commands enabled with prefix: ${this.ownerCommandPrefix}`);
675
+ }
165
676
  this.emit('ready', this.serverInfo);
677
+ this.startMemoryMonitor();
166
678
  }
167
679
  if (output.includes('joined the game')) {
168
680
  const match = output.match(/(\w+) joined the game/);
169
681
  if (match) {
682
+ this.playerCount++;
170
683
  this.handlePlayerJoin(match[1]);
684
+ if (this.owners.has(match[1].toLowerCase())) {
685
+ this.sendCommand(`tellraw ${match[1]} {"text":"Welcome Owner! Use ${this.ownerCommandPrefix}help for commands","color":"gold"}`);
686
+ }
171
687
  }
172
688
  }
173
689
  if (output.includes('left the game')) {
174
690
  const match = output.match(/(\w+) left the game/);
175
691
  if (match) {
692
+ this.playerCount--;
176
693
  this.handlePlayerLeave(match[1]);
177
694
  }
178
695
  }
696
+ if (output.includes('<') && output.includes('>')) {
697
+ const chatMatch = output.match(/<(\w+)>\s+(.+)/);
698
+ if (chatMatch) {
699
+ const player = chatMatch[1];
700
+ const message = chatMatch[2];
701
+ if (message.startsWith(this.ownerCommandPrefix)) {
702
+ const command = message.substring(this.ownerCommandPrefix.length);
703
+ this.processOwnerCommand(player, command);
704
+ }
705
+ }
706
+ }
179
707
  if (output.includes('[ViaVersion]')) {
180
708
  this.logger.debug(`[ViaVersion] ${output.trim()}`);
181
709
  }
@@ -198,6 +726,66 @@ class MinecraftServer extends events_1.EventEmitter {
198
726
  }
199
727
  return this.serverInfo;
200
728
  }
729
+ startMemoryMonitor() {
730
+ if (!this.options.memoryMonitor?.enabled)
731
+ return;
732
+ const threshold = this.options.memoryMonitor.threshold || 90;
733
+ const interval = this.options.memoryMonitor.interval || 10000;
734
+ const action = this.options.memoryMonitor.action || 'warn';
735
+ this.memoryMonitorInterval = setInterval(async () => {
736
+ if (this.serverInfo.status !== 'running' || !this.process)
737
+ return;
738
+ try {
739
+ const stats = await (0, pidusage_1.default)(this.process.pid);
740
+ const memMax = this.parseMemory(this.config.memory.max);
741
+ const memPercent = (stats.memory / (memMax * 1024 * 1024)) * 100;
742
+ this.memoryUsageHistory.push(memPercent);
743
+ if (this.memoryUsageHistory.length > 10) {
744
+ this.memoryUsageHistory.shift();
745
+ }
746
+ if (memPercent > threshold) {
747
+ this.logger.warning(`High memory usage: ${memPercent.toFixed(1)}%`);
748
+ const isIncreasing = this.memoryUsageHistory.length > 5 &&
749
+ this.memoryUsageHistory[this.memoryUsageHistory.length - 1] >
750
+ this.memoryUsageHistory[0] * 1.2;
751
+ if (isIncreasing) {
752
+ this.logger.warning('Memory leak detected!');
753
+ switch (action) {
754
+ case 'restart':
755
+ this.logger.info('Restarting server due to memory leak...');
756
+ await this.gracefulRestart();
757
+ break;
758
+ case 'stop':
759
+ this.logger.info('Stopping server due to memory leak...');
760
+ await this.stop();
761
+ break;
762
+ case 'warn':
763
+ default:
764
+ this.logger.warning('Please restart server to free memory');
765
+ }
766
+ }
767
+ }
768
+ }
769
+ catch (error) {
770
+ this.logger.error('Memory monitor error:', error);
771
+ }
772
+ }, interval);
773
+ }
774
+ async gracefulRestart() {
775
+ this.logger.info('Initiating graceful restart...');
776
+ this.sendCommand('say Server restarting in 30 seconds');
777
+ this.sendCommand('save-all');
778
+ await new Promise(resolve => setTimeout(resolve, 10000));
779
+ this.sendCommand('say Server restarting in 20 seconds');
780
+ await new Promise(resolve => setTimeout(resolve, 10000));
781
+ this.sendCommand('say Server restarting in 10 seconds');
782
+ this.sendCommand('save-all');
783
+ await new Promise(resolve => setTimeout(resolve, 5000));
784
+ this.sendCommand('say Server restarting in 5 seconds');
785
+ await new Promise(resolve => setTimeout(resolve, 5000));
786
+ await this.stop();
787
+ await this.start();
788
+ }
201
789
  async stop() {
202
790
  if (!this.process) {
203
791
  this.logger.warning('Server not running');
@@ -205,8 +793,9 @@ class MinecraftServer extends events_1.EventEmitter {
205
793
  }
206
794
  this.logger.info('Stopping server...');
207
795
  this.serverInfo.status = 'stopping';
796
+ this.sendCommand('save-all');
208
797
  this.sendCommand('stop');
209
- await new Promise(resolve => setTimeout(resolve, 5000));
798
+ await new Promise(resolve => setTimeout(resolve, 10000));
210
799
  if (this.process) {
211
800
  this.process.kill();
212
801
  this.process = null;
@@ -214,6 +803,9 @@ class MinecraftServer extends events_1.EventEmitter {
214
803
  if (this.backupCron) {
215
804
  this.backupCron.stop();
216
805
  }
806
+ if (this.memoryMonitorInterval) {
807
+ clearInterval(this.memoryMonitorInterval);
808
+ }
217
809
  if (this.config.platform === 'all') {
218
810
  this.geyser.stop();
219
811
  }