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