@dimzxzzx07/mc-headless 1.7.0 → 1.9.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.
Files changed (38) hide show
  1. package/README.md +317 -703
  2. package/dist/core/JavaChecker.d.ts +16 -3
  3. package/dist/core/JavaChecker.d.ts.map +1 -1
  4. package/dist/core/JavaChecker.js +179 -31
  5. package/dist/core/JavaChecker.js.map +1 -1
  6. package/dist/core/MinecraftServer.d.ts +61 -0
  7. package/dist/core/MinecraftServer.d.ts.map +1 -1
  8. package/dist/core/MinecraftServer.js +742 -60
  9. package/dist/core/MinecraftServer.js.map +1 -1
  10. package/dist/index.d.ts +3 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +31 -16
  13. package/dist/index.js.map +1 -1
  14. package/dist/platforms/BedrockServer.d.ts.map +1 -1
  15. package/dist/platforms/BedrockServer.js +2 -0
  16. package/dist/platforms/BedrockServer.js.map +1 -1
  17. package/dist/platforms/JavaServer.d.ts.map +1 -1
  18. package/dist/platforms/JavaServer.js +2 -0
  19. package/dist/platforms/JavaServer.js.map +1 -1
  20. package/dist/platforms/SkinRestorer.d.ts +14 -0
  21. package/dist/platforms/SkinRestorer.d.ts.map +1 -0
  22. package/dist/platforms/SkinRestorer.js +145 -0
  23. package/dist/platforms/SkinRestorer.js.map +1 -0
  24. package/dist/platforms/SkinsRestorer.d.ts +14 -0
  25. package/dist/platforms/SkinsRestorer.d.ts.map +1 -0
  26. package/dist/platforms/SkinsRestorer.js +145 -0
  27. package/dist/platforms/SkinsRestorer.js.map +1 -0
  28. package/dist/types/index.d.ts +2 -0
  29. package/dist/types/index.d.ts.map +1 -1
  30. package/package.json +1 -1
  31. package/src/core/JavaChecker.ts +170 -34
  32. package/src/core/MinecraftServer.ts +854 -64
  33. package/src/index.ts +33 -17
  34. package/src/platforms/BedrockServer.ts +2 -0
  35. package/src/platforms/JavaServer.ts +2 -0
  36. package/src/platforms/SkinRestorer.ts +127 -0
  37. package/src/scripts/install-java.sh +97 -32
  38. package/src/types/index.ts +2 -0
@@ -1,10 +1,9 @@
1
1
  import { EventEmitter } from 'events';
2
- import { spawn } from 'child_process';
3
- import pidusage from 'pidusage';
2
+ import { spawn, exec, execSync } from 'child_process';
4
3
  import * as cron from 'node-cron';
5
4
  import { MinecraftConfig, ServerInfo, Player } from '../types';
6
5
  import { ConfigHandler } from './ConfigHandler';
7
- import { JavaChecker } from './JavaChecker';
6
+ import { JavaChecker, JavaInfo } from './JavaChecker';
8
7
  import { FileUtils } from '../utils/FileUtils';
9
8
  import { Logger } from '../utils/Logger';
10
9
  import { SystemDetector } from '../utils/SystemDetector';
@@ -15,13 +14,38 @@ import { FabricEngine } from '../engines/FabricEngine';
15
14
  import { ServerEngine } from '../engines/ServerEngine';
16
15
  import { GeyserBridge } from '../platforms/GeyserBridge';
17
16
  import { ViaVersionManager } from '../platforms/ViaVersion';
17
+ import { SkinRestorerManager } from '../platforms/SkinRestorer';
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;
25
+ enableSkinRestorer?: boolean;
24
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;
25
49
  }
26
50
 
27
51
  export class MinecraftServer extends EventEmitter {
@@ -31,24 +55,72 @@ export class MinecraftServer extends EventEmitter {
31
55
  private engine: ServerEngine;
32
56
  private geyser: GeyserBridge;
33
57
  private viaVersion: ViaVersionManager;
58
+ private skinRestorer: SkinRestorerManager;
34
59
  private process: any = null;
35
60
  private serverInfo: ServerInfo;
36
61
  private players: Map<string, Player> = new Map();
37
62
  private backupCron: cron.ScheduledTask | null = null;
38
63
  private startTime: Date | null = null;
64
+ private memoryMonitorInterval: NodeJS.Timeout | null = null;
65
+ private statsInterval: NodeJS.Timeout | null = null;
66
+ private memoryUsageHistory: number[] = [];
67
+ private worldSize: number = 0;
68
+ private playerCount: number = 0;
69
+ private javaCommand: string = 'java';
70
+ private javaInfo: JavaInfo | null = null;
71
+ private owners: Set<string> = new Set();
72
+ private ownerCommandPrefix: string = '!';
73
+ private lastCpuTotal: number = 0;
74
+ private lastCpuTime: number = 0;
75
+ private cpuUsage: number = 0;
76
+ private cgroupMemory: number = 0;
77
+ private cgroupCpu: number = 0;
39
78
 
40
79
  constructor(userConfig: MinecraftServerOptions = {}) {
41
80
  super();
42
81
  this.logger = Logger.getInstance();
43
82
  this.logger.banner();
44
83
 
45
- this.options = userConfig;
84
+ this.options = {
85
+ javaVersion: 'auto',
86
+ usePortableJava: true,
87
+ memoryMonitor: {
88
+ enabled: true,
89
+ threshold: 90,
90
+ interval: 30000,
91
+ action: 'warn'
92
+ },
93
+ autoInstallJava: true,
94
+ networkOptimization: {
95
+ tcpFastOpen: true,
96
+ bungeeMode: false,
97
+ proxyProtocol: false
98
+ },
99
+ owners: [],
100
+ ownerCommands: {
101
+ prefix: '!',
102
+ enabled: true
103
+ },
104
+ silentMode: true,
105
+ statsInterval: 30000,
106
+ ...userConfig
107
+ };
108
+
109
+ if (this.options.owners) {
110
+ this.options.owners.forEach(owner => this.owners.add(owner.toLowerCase()));
111
+ }
112
+
113
+ if (this.options.ownerCommands?.prefix) {
114
+ this.ownerCommandPrefix = this.options.ownerCommands.prefix;
115
+ }
116
+
46
117
  const handler = new ConfigHandler(userConfig);
47
118
  this.config = handler.getConfig();
48
119
 
49
120
  this.engine = this.createEngine();
50
121
  this.geyser = new GeyserBridge();
51
122
  this.viaVersion = new ViaVersionManager();
123
+ this.skinRestorer = new SkinRestorerManager();
52
124
 
53
125
  this.serverInfo = {
54
126
  pid: 0,
@@ -62,8 +134,32 @@ export class MinecraftServer extends EventEmitter {
62
134
  maxPlayers: this.config.world.maxPlayers,
63
135
  uptime: 0,
64
136
  memory: { used: 0, max: 0 },
137
+ cpu: 0,
65
138
  status: 'stopped'
66
139
  };
140
+
141
+ this.detectCgroupLimits();
142
+ }
143
+
144
+ private detectCgroupLimits(): void {
145
+ try {
146
+ if (fs.existsSync('/sys/fs/cgroup/memory/memory.limit_in_bytes')) {
147
+ const limit = fs.readFileSync('/sys/fs/cgroup/memory/memory.limit_in_bytes', 'utf8');
148
+ this.cgroupMemory = parseInt(limit) / 1024 / 1024;
149
+ this.logger.debug(`Cgroup memory limit: ${this.cgroupMemory} MB`);
150
+ }
151
+
152
+ if (fs.existsSync('/sys/fs/cgroup/cpu/cpu.cfs_quota_us') && fs.existsSync('/sys/fs/cgroup/cpu/cpu.cfs_period_us')) {
153
+ const quota = parseInt(fs.readFileSync('/sys/fs/cgroup/cpu/cpu.cfs_quota_us', 'utf8'));
154
+ const period = parseInt(fs.readFileSync('/sys/fs/cgroup/cpu/cpu.cfs_period_us', 'utf8'));
155
+ if (quota > 0 && period > 0) {
156
+ this.cgroupCpu = quota / period;
157
+ this.logger.debug(`Cgroup CPU limit: ${this.cgroupCpu} cores`);
158
+ }
159
+ }
160
+ } catch (error) {
161
+ this.logger.debug('Not running in cgroup environment');
162
+ }
67
163
  }
68
164
 
69
165
  private createEngine(): ServerEngine {
@@ -83,10 +179,578 @@ export class MinecraftServer extends EventEmitter {
83
179
  }
84
180
  }
85
181
 
182
+ private async detectJavaVersion(): Promise<string> {
183
+ try {
184
+ const output = execSync('java -version 2>&1').toString();
185
+ if (output.includes('version "21')) {
186
+ return '21';
187
+ } else if (output.includes('version "17')) {
188
+ return '17';
189
+ } else if (output.includes('version "11')) {
190
+ return '11';
191
+ } else if (output.includes('version "8')) {
192
+ return '8';
193
+ }
194
+ return 'unknown';
195
+ } catch {
196
+ return 'none';
197
+ }
198
+ }
199
+
200
+ private async ensureJava(): Promise<void> {
201
+ if (!this.options.autoInstallJava) {
202
+ await JavaChecker.ensureJava();
203
+ return;
204
+ }
205
+
206
+ const targetVersion = this.options.javaVersion === 'auto' ? '17' : this.options.javaVersion || '17';
207
+
208
+ if (this.options.usePortableJava) {
209
+ this.javaInfo = await JavaChecker.getOrDownloadPortableJava(targetVersion);
210
+ this.javaCommand = this.javaInfo.path;
211
+ this.logger.success(`Using portable Java ${this.javaInfo.version} from ${this.javaInfo.path}`);
212
+ } else {
213
+ const hasJava = await JavaChecker.checkJava();
214
+ if (!hasJava) {
215
+ this.logger.info('Java not found, attempting to install system Java...');
216
+ const osType = SystemDetector.getOS();
217
+ const distro = SystemDetector.getDistro();
218
+
219
+ if (osType === 'linux' || osType === 'android') {
220
+ await this.installJavaLinux(distro, targetVersion);
221
+ } else if (osType === 'darwin') {
222
+ await this.installJavaMac(targetVersion);
223
+ } else if (osType === 'windows') {
224
+ this.logger.error('Windows detected. Please install Java manually from https://adoptium.net');
225
+ throw new Error('Java not installed');
226
+ }
227
+ } else {
228
+ const detectedVersion = await this.detectJavaVersion();
229
+ this.logger.info(`Detected system Java version: ${detectedVersion}`);
230
+
231
+ if (detectedVersion === '21' || detectedVersion === '17') {
232
+ this.javaCommand = 'java';
233
+ this.logger.success(`Using system Java ${detectedVersion}`);
234
+ } else {
235
+ this.logger.warning(`System Java ${detectedVersion} is not optimal. Switching to portable Java...`);
236
+ this.javaInfo = await JavaChecker.getOrDownloadPortableJava(targetVersion);
237
+ this.javaCommand = this.javaInfo.path;
238
+ }
239
+ }
240
+ }
241
+
242
+ const finalVersion = this.javaInfo ? this.javaInfo.version : await this.detectJavaVersion();
243
+ this.logger.success(`Java version: ${finalVersion}`);
244
+ }
245
+
246
+ private async installJavaLinux(distro: string, version: string): Promise<void> {
247
+ return new Promise((resolve, reject) => {
248
+ let command = '';
249
+ const javaPackage = version === '21' ? 'openjdk-21-jre-headless' : 'openjdk-17-jre-headless';
250
+
251
+ if (distro === 'ubuntu' || distro === 'debian') {
252
+ command = `apt update && apt install -y ${javaPackage}`;
253
+ } else if (distro === 'centos' || distro === 'fedora') {
254
+ command = `yum install -y java-${version}-openjdk-headless`;
255
+ } else if (distro === 'arch') {
256
+ const archPackage = version === '21' ? 'jre21-openjdk-headless' : 'jre17-openjdk-headless';
257
+ command = `pacman -S --noconfirm ${archPackage}`;
258
+ } else if (distro === 'termux') {
259
+ command = 'pkg install -y openjdk-17';
260
+ } else {
261
+ this.logger.error('Unsupported Linux distribution. Please install Java manually.');
262
+ reject(new Error('Unsupported distribution'));
263
+ return;
264
+ }
265
+
266
+ this.logger.info(`Installing Java ${version} with: ${command}`);
267
+
268
+ const install = exec(command, (error) => {
269
+ if (error) {
270
+ reject(error);
271
+ } else {
272
+ resolve();
273
+ }
274
+ });
275
+
276
+ if (install.stdout) {
277
+ install.stdout.pipe(process.stdout);
278
+ }
279
+ if (install.stderr) {
280
+ install.stderr.pipe(process.stderr);
281
+ }
282
+ });
283
+ }
284
+
285
+ private async installJavaMac(version: string): Promise<void> {
286
+ return new Promise((resolve, reject) => {
287
+ const command = `brew install openjdk@${version}`;
288
+
289
+ this.logger.info(`Installing Java ${version} with: ${command}`);
290
+
291
+ const install = exec(command, (error) => {
292
+ if (error) {
293
+ reject(error);
294
+ } else {
295
+ resolve();
296
+ }
297
+ });
298
+
299
+ if (install.stdout) {
300
+ install.stdout.pipe(process.stdout);
301
+ }
302
+ if (install.stderr) {
303
+ install.stderr.pipe(process.stderr);
304
+ }
305
+ });
306
+ }
307
+
308
+ private async calculateWorldSize(): Promise<number> {
309
+ try {
310
+ const worldPath = path.join(process.cwd(), this.config.folders.world);
311
+ if (!await fs.pathExists(worldPath)) return 0;
312
+
313
+ const getSize = async (dir: string): Promise<number> => {
314
+ let total = 0;
315
+ const files = await fs.readdir(dir);
316
+
317
+ for (const file of files) {
318
+ const filePath = path.join(dir, file);
319
+ const stat = await fs.stat(filePath);
320
+
321
+ if (stat.isDirectory()) {
322
+ total += await getSize(filePath);
323
+ } else {
324
+ total += stat.size;
325
+ }
326
+ }
327
+
328
+ return total;
329
+ };
330
+
331
+ return await getSize(worldPath);
332
+ } catch {
333
+ return 0;
334
+ }
335
+ }
336
+
337
+ private buildJavaArgs(): string[] {
338
+ if (this.options.customJavaArgs && this.options.customJavaArgs.length > 0) {
339
+ return this.options.customJavaArgs;
340
+ }
341
+
342
+ const memMax = this.parseMemory(this.config.memory.max);
343
+ const javaVersion = this.options.javaVersion || 'auto';
344
+
345
+ let gcArgs: string[] = [];
346
+
347
+ if (memMax >= 16384) {
348
+ gcArgs = [
349
+ '-XX:+UseG1GC',
350
+ '-XX:+ParallelRefProcEnabled',
351
+ '-XX:MaxGCPauseMillis=100',
352
+ '-XX:+UnlockExperimentalVMOptions',
353
+ '-XX:+DisableExplicitGC',
354
+ '-XX:+AlwaysPreTouch',
355
+ '-XX:G1NewSizePercent=40',
356
+ '-XX:G1MaxNewSizePercent=50',
357
+ '-XX:G1HeapRegionSize=16M',
358
+ '-XX:G1ReservePercent=15',
359
+ '-XX:G1HeapWastePercent=5',
360
+ '-XX:G1MixedGCCountTarget=4',
361
+ '-XX:InitiatingHeapOccupancyPercent=20',
362
+ '-XX:G1MixedGCLiveThresholdPercent=90',
363
+ '-XX:G1RSetUpdatingPauseTimePercent=5',
364
+ '-XX:SurvivorRatio=32',
365
+ '-XX:+PerfDisableSharedMem',
366
+ '-XX:MaxTenuringThreshold=1'
367
+ ];
368
+ } else if (memMax >= 8192) {
369
+ gcArgs = [
370
+ '-XX:+UseG1GC',
371
+ '-XX:+ParallelRefProcEnabled',
372
+ '-XX:MaxGCPauseMillis=150',
373
+ '-XX:+UnlockExperimentalVMOptions',
374
+ '-XX:+DisableExplicitGC',
375
+ '-XX:+AlwaysPreTouch',
376
+ '-XX:G1NewSizePercent=30',
377
+ '-XX:G1MaxNewSizePercent=40',
378
+ '-XX:G1HeapRegionSize=8M',
379
+ '-XX:G1ReservePercent=10',
380
+ '-XX:G1HeapWastePercent=5',
381
+ '-XX:G1MixedGCCountTarget=4',
382
+ '-XX:InitiatingHeapOccupancyPercent=15',
383
+ '-XX:G1MixedGCLiveThresholdPercent=90',
384
+ '-XX:G1RSetUpdatingPauseTimePercent=5',
385
+ '-XX:SurvivorRatio=32',
386
+ '-XX:+PerfDisableSharedMem',
387
+ '-XX:MaxTenuringThreshold=1'
388
+ ];
389
+ } else {
390
+ gcArgs = this.config.memory.useAikarsFlags ? [
391
+ '-XX:+UseG1GC',
392
+ '-XX:+ParallelRefProcEnabled',
393
+ '-XX:MaxGCPauseMillis=200',
394
+ '-XX:+UnlockExperimentalVMOptions',
395
+ '-XX:+DisableExplicitGC',
396
+ '-XX:+AlwaysPreTouch',
397
+ '-XX:G1HeapWastePercent=5',
398
+ '-XX:G1MixedGCCountTarget=4',
399
+ '-XX:InitiatingHeapOccupancyPercent=15',
400
+ '-XX:G1MixedGCLiveThresholdPercent=90',
401
+ '-XX:G1RSetUpdatingPauseTimePercent=5',
402
+ '-XX:SurvivorRatio=32',
403
+ '-XX:+PerfDisableSharedMem',
404
+ '-XX:MaxTenuringThreshold=1',
405
+ '-Dusing.aikars.flags=https://mcflags.emc.gs',
406
+ '-Daikars.new.flags=true'
407
+ ] : [];
408
+ }
409
+
410
+ if (javaVersion === '21') {
411
+ gcArgs.push('--enable-preview');
412
+ }
413
+
414
+ const baseArgs = [
415
+ `-Xms${this.config.memory.init}`,
416
+ `-Xmx${this.config.memory.max}`
417
+ ];
418
+
419
+ return [...baseArgs, ...gcArgs];
420
+ }
421
+
422
+ private buildEnvironment(): NodeJS.ProcessEnv {
423
+ const env: NodeJS.ProcessEnv = { ...process.env };
424
+
425
+ env.MALLOC_ARENA_MAX = '2';
426
+
427
+ env._JAVA_OPTIONS = `-Xmx${this.config.memory.max}`;
428
+
429
+ if (this.javaInfo && this.javaInfo.type === 'portable') {
430
+ env.JAVA_HOME = path.dirname(path.dirname(this.javaInfo.path));
431
+ env.PATH = `${path.dirname(this.javaInfo.path)}:${env.PATH}`;
432
+ }
433
+
434
+ if (this.cgroupMemory > 0) {
435
+ const memLimit = Math.min(this.parseMemory(this.config.memory.max), this.cgroupMemory);
436
+ env._JAVA_OPTIONS += ` -XX:MaxRAM=${memLimit}M`;
437
+ }
438
+
439
+ if (this.cgroupCpu > 0) {
440
+ env._JAVA_OPTIONS += ` -XX:ActiveProcessorCount=${Math.floor(this.cgroupCpu)}`;
441
+ }
442
+
443
+ return env;
444
+ }
445
+
446
+ private processOwnerCommand(player: string, command: string): void {
447
+ if (!this.options.ownerCommands?.enabled) return;
448
+ if (!this.owners.has(player.toLowerCase())) return;
449
+
450
+ const cmd = command.toLowerCase().trim();
451
+ const args = cmd.split(' ');
452
+
453
+ switch (args[0]) {
454
+ case 'gamemode':
455
+ case 'gm':
456
+ this.handleGamemodeCommand(player, args);
457
+ break;
458
+ case 'tp':
459
+ case 'teleport':
460
+ this.handleTeleportCommand(player, args);
461
+ break;
462
+ case 'give':
463
+ this.handleGiveCommand(player, args);
464
+ break;
465
+ case 'time':
466
+ this.handleTimeCommand(player, args);
467
+ break;
468
+ case 'weather':
469
+ this.handleWeatherCommand(player, args);
470
+ break;
471
+ case 'kill':
472
+ this.handleKillCommand(player, args);
473
+ break;
474
+ case 'ban':
475
+ this.handleBanCommand(player, args);
476
+ break;
477
+ case 'kick':
478
+ this.handleKickCommand(player, args);
479
+ break;
480
+ case 'op':
481
+ this.handleOpCommand(player, args);
482
+ break;
483
+ case 'deop':
484
+ this.handleDeopCommand(player, args);
485
+ break;
486
+ case 'reload':
487
+ this.sendCommand('reload');
488
+ this.logger.info(`${player} reloaded the server`);
489
+ break;
490
+ case 'save':
491
+ this.sendCommand('save-all');
492
+ this.logger.info(`${player} saved the world`);
493
+ break;
494
+ case 'list':
495
+ this.sendCommand('list');
496
+ break;
497
+ case 'help':
498
+ this.sendOwnerHelp(player);
499
+ break;
500
+ default:
501
+ this.sendCommand(command);
502
+ }
503
+ }
504
+
505
+ private handleGamemodeCommand(player: string, args: string[]): void {
506
+ if (args.length < 2) {
507
+ this.sendCommand(`tellraw ${player} {"text":"Usage: ${this.ownerCommandPrefix}gamemode <survival|creative|adventure|spectator> [player]","color":"red"}`);
508
+ return;
509
+ }
510
+
511
+ const gamemode = args[1];
512
+ const target = args.length > 2 ? args[2] : player;
513
+
514
+ let gamemodeNum = 0;
515
+ switch (gamemode) {
516
+ case 'survival':
517
+ case '0':
518
+ gamemodeNum = 0;
519
+ break;
520
+ case 'creative':
521
+ case '1':
522
+ gamemodeNum = 1;
523
+ break;
524
+ case 'adventure':
525
+ case '2':
526
+ gamemodeNum = 2;
527
+ break;
528
+ case 'spectator':
529
+ case '3':
530
+ gamemodeNum = 3;
531
+ break;
532
+ default:
533
+ this.sendCommand(`tellraw ${player} {"text":"Invalid gamemode. Use: survival, creative, adventure, spectator","color":"red"}`);
534
+ return;
535
+ }
536
+
537
+ this.sendCommand(`gamemode ${gamemodeNum} ${target}`);
538
+ this.logger.info(`${player} set gamemode of ${target} to ${gamemode}`);
539
+ }
540
+
541
+ private handleTeleportCommand(player: string, args: string[]): void {
542
+ if (args.length < 2) {
543
+ this.sendCommand(`tellraw ${player} {"text":"Usage: ${this.ownerCommandPrefix}tp <player> [x y z]","color":"red"}`);
544
+ return;
545
+ }
546
+
547
+ const target = args[1];
548
+ if (args.length >= 4) {
549
+ const x = args[2];
550
+ const y = args[3];
551
+ const z = args[4] || '0';
552
+ this.sendCommand(`tp ${target} ${x} ${y} ${z}`);
553
+ } else {
554
+ this.sendCommand(`tp ${player} ${target}`);
555
+ }
556
+ this.logger.info(`${player} teleported to ${target}`);
557
+ }
558
+
559
+ private handleGiveCommand(player: string, args: string[]): void {
560
+ if (args.length < 3) {
561
+ this.sendCommand(`tellraw ${player} {"text":"Usage: ${this.ownerCommandPrefix}give <player> <item> [amount]","color":"red"}`);
562
+ return;
563
+ }
564
+
565
+ const target = args[1];
566
+ const item = args[2];
567
+ const amount = args.length > 3 ? args[3] : '1';
568
+
569
+ this.sendCommand(`give ${target} ${item} ${amount}`);
570
+ this.logger.info(`${player} gave ${amount} x ${item} to ${target}`);
571
+ }
572
+
573
+ private handleTimeCommand(player: string, args: string[]): void {
574
+ if (args.length < 2) {
575
+ this.sendCommand(`tellraw ${player} {"text":"Usage: ${this.ownerCommandPrefix}time <set|add|query> <value>","color":"red"}`);
576
+ return;
577
+ }
578
+
579
+ const subCmd = args[1];
580
+ const value = args.length > 2 ? args[2] : '';
581
+
582
+ if (subCmd === 'set') {
583
+ if (value === 'day') {
584
+ this.sendCommand('time set day');
585
+ } else if (value === 'night') {
586
+ this.sendCommand('time set night');
587
+ } else {
588
+ this.sendCommand(`time set ${value}`);
589
+ }
590
+ } else if (subCmd === 'add') {
591
+ this.sendCommand(`time add ${value}`);
592
+ } else if (subCmd === 'query') {
593
+ this.sendCommand('time query daytime');
594
+ }
595
+
596
+ this.logger.info(`${player} changed time: ${subCmd} ${value}`);
597
+ }
598
+
599
+ private handleWeatherCommand(player: string, args: string[]): void {
600
+ if (args.length < 2) {
601
+ this.sendCommand(`tellraw ${player} {"text":"Usage: ${this.ownerCommandPrefix}weather <clear|rain|thunder> [duration]","color":"red"}`);
602
+ return;
603
+ }
604
+
605
+ const weather = args[1];
606
+ const duration = args.length > 2 ? args[2] : '';
607
+
608
+ if (weather === 'clear') {
609
+ this.sendCommand('weather clear');
610
+ } else if (weather === 'rain') {
611
+ this.sendCommand('weather rain');
612
+ } else if (weather === 'thunder') {
613
+ this.sendCommand('weather thunder');
614
+ }
615
+
616
+ if (duration) {
617
+ this.sendCommand(`weather ${weather} ${duration}`);
618
+ }
619
+
620
+ this.logger.info(`${player} changed weather to ${weather}`);
621
+ }
622
+
623
+ private handleKillCommand(player: string, args: string[]): void {
624
+ const target = args.length > 1 ? args[1] : player;
625
+ this.sendCommand(`kill ${target}`);
626
+ this.logger.info(`${player} killed ${target}`);
627
+ }
628
+
629
+ private handleBanCommand(player: string, args: string[]): void {
630
+ if (args.length < 2) {
631
+ this.sendCommand(`tellraw ${player} {"text":"Usage: ${this.ownerCommandPrefix}ban <player> [reason]","color":"red"}`);
632
+ return;
633
+ }
634
+
635
+ const target = args[1];
636
+ const reason = args.slice(2).join(' ') || 'Banned by owner';
637
+
638
+ this.sendCommand(`ban ${target} ${reason}`);
639
+ this.logger.info(`${player} banned ${target}: ${reason}`);
640
+ }
641
+
642
+ private handleKickCommand(player: string, args: string[]): void {
643
+ if (args.length < 2) {
644
+ this.sendCommand(`tellraw ${player} {"text":"Usage: ${this.ownerCommandPrefix}kick <player> [reason]","color":"red"}`);
645
+ return;
646
+ }
647
+
648
+ const target = args[1];
649
+ const reason = args.slice(2).join(' ') || 'Kicked by owner';
650
+
651
+ this.sendCommand(`kick ${target} ${reason}`);
652
+ this.logger.info(`${player} kicked ${target}: ${reason}`);
653
+ }
654
+
655
+ private handleOpCommand(player: string, args: string[]): void {
656
+ if (args.length < 2) {
657
+ this.sendCommand(`tellraw ${player} {"text":"Usage: ${this.ownerCommandPrefix}op <player>","color":"red"}`);
658
+ return;
659
+ }
660
+
661
+ const target = args[1];
662
+ this.sendCommand(`op ${target}`);
663
+ this.logger.info(`${player} opped ${target}`);
664
+ }
665
+
666
+ private handleDeopCommand(player: string, args: string[]): void {
667
+ if (args.length < 2) {
668
+ this.sendCommand(`tellraw ${player} {"text":"Usage: ${this.ownerCommandPrefix}deop <player>","color":"red"}`);
669
+ return;
670
+ }
671
+
672
+ const target = args[1];
673
+ this.sendCommand(`deop ${target}`);
674
+ this.logger.info(`${player} deopped ${target}`);
675
+ }
676
+
677
+ private sendOwnerHelp(player: string): void {
678
+ const commands = [
679
+ `{"text":"\\n=== Owner Commands ===\\n","color":"gold","bold":true}`,
680
+ `{"text":"${this.ownerCommandPrefix}gamemode <mode> [player] - Change gamemode\\n","color":"yellow"}`,
681
+ `{"text":"${this.ownerCommandPrefix}tp <player> [x y z] - Teleport\\n","color":"yellow"}`,
682
+ `{"text":"${this.ownerCommandPrefix}give <player> <item> [amount] - Give items\\n","color":"yellow"}`,
683
+ `{"text":"${this.ownerCommandPrefix}time <set|add> <value> - Change time\\n","color":"yellow"}`,
684
+ `{"text":"${this.ownerCommandPrefix}weather <clear|rain|thunder> - Change weather\\n","color":"yellow"}`,
685
+ `{"text":"${this.ownerCommandPrefix}kill [player] - Kill player\\n","color":"yellow"}`,
686
+ `{"text":"${this.ownerCommandPrefix}ban <player> [reason] - Ban player\\n","color":"yellow"}`,
687
+ `{"text":"${this.ownerCommandPrefix}kick <player> [reason] - Kick player\\n","color":"yellow"}`,
688
+ `{"text":"${this.ownerCommandPrefix}op <player> - Give operator\\n","color":"yellow"}`,
689
+ `{"text":"${this.ownerCommandPrefix}deop <player> - Remove operator\\n","color":"yellow"}`,
690
+ `{"text":"${this.ownerCommandPrefix}reload - Reload server\\n","color":"yellow"}`,
691
+ `{"text":"${this.ownerCommandPrefix}save - Save world\\n","color":"yellow"}`,
692
+ `{"text":"${this.ownerCommandPrefix}list - List players\\n","color":"yellow"}`,
693
+ `{"text":"${this.ownerCommandPrefix}help - Show this help\\n","color":"yellow"}`
694
+ ];
695
+
696
+ commands.forEach(cmd => {
697
+ this.sendCommand(`tellraw ${player} ${cmd}`);
698
+ });
699
+ }
700
+
701
+ private async updateStats(): Promise<void> {
702
+ if (!this.process || this.serverInfo.status !== 'running') return;
703
+
704
+ try {
705
+ const memMax = this.parseMemory(this.config.memory.max);
706
+
707
+ if (fs.existsSync('/sys/fs/cgroup/memory/memory.usage_in_bytes')) {
708
+ const usage = parseInt(fs.readFileSync('/sys/fs/cgroup/memory/memory.usage_in_bytes', 'utf8'));
709
+ this.serverInfo.memory.used = Math.round(usage / 1024 / 1024);
710
+ } else {
711
+ const stats = await import('pidusage');
712
+ const usage = await stats.default(this.process.pid);
713
+ this.serverInfo.memory.used = Math.round(usage.memory / 1024 / 1024);
714
+ }
715
+
716
+ this.serverInfo.memory.max = memMax;
717
+
718
+ if (fs.existsSync('/sys/fs/cgroup/cpuacct/cpuacct.usage')) {
719
+ const cpuTotal = parseInt(fs.readFileSync('/sys/fs/cgroup/cpuacct/cpuacct.usage', 'utf8'));
720
+ const now = Date.now();
721
+
722
+ if (this.lastCpuTotal > 0) {
723
+ const cpuDiff = cpuTotal - this.lastCpuTotal;
724
+ const timeDiff = now - this.lastCpuTime;
725
+ this.cpuUsage = (cpuDiff / timeDiff / 1e6) * 100;
726
+ if (this.cgroupCpu > 0) {
727
+ this.cpuUsage = this.cpuUsage / this.cgroupCpu;
728
+ }
729
+ this.serverInfo.cpu = Math.min(100, Math.max(0, Math.round(this.cpuUsage)));
730
+ }
731
+
732
+ this.lastCpuTotal = cpuTotal;
733
+ this.lastCpuTime = now;
734
+ }
735
+
736
+ this.serverInfo.uptime = Math.floor((Date.now() - (this.startTime?.getTime() || 0)) / 1000);
737
+ this.serverInfo.players = this.players.size;
738
+
739
+ this.emit('resource', this.serverInfo);
740
+
741
+ } catch (error) {
742
+ this.logger.error('Stats update error:', error);
743
+ }
744
+ }
745
+
86
746
  public async start(): Promise<ServerInfo> {
87
747
  this.logger.info(`Starting ${this.config.type} server v${this.config.version}...`);
88
748
 
89
- await JavaChecker.ensureJava();
749
+ if (this.owners.size > 0) {
750
+ this.logger.info(`Owners configured: ${Array.from(this.owners).join(', ')}`);
751
+ }
752
+
753
+ await this.ensureJava();
90
754
 
91
755
  const systemInfo = SystemDetector.getSystemInfo();
92
756
  this.logger.debug('System info:', systemInfo);
@@ -94,6 +758,11 @@ export class MinecraftServer extends EventEmitter {
94
758
  const serverDir = process.cwd();
95
759
  await FileUtils.ensureServerStructure(this.config);
96
760
 
761
+ this.worldSize = await this.calculateWorldSize();
762
+ if (this.worldSize > 10 * 1024 * 1024 * 1024) {
763
+ this.logger.warning(`Large world detected (${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB). Consider increasing memory allocation.`);
764
+ }
765
+
97
766
  const jarPath = await this.engine.download(this.config, serverDir);
98
767
 
99
768
  if (this.config.type === 'forge') {
@@ -121,7 +790,13 @@ export class MinecraftServer extends EventEmitter {
121
790
  }
122
791
  }
123
792
 
124
- const javaArgs = this.engine.getJavaArgs(this.config);
793
+ if (this.options.enableSkinRestorer !== false) {
794
+ this.logger.info('Enabling SkinRestorer for player skins...');
795
+ await FileUtils.ensureDir(this.config.folders.plugins);
796
+ await this.skinRestorer.setup(this.config);
797
+ }
798
+
799
+ const javaArgs = this.buildJavaArgs();
125
800
  const serverJar = this.engine.getServerJar(jarPath);
126
801
  const serverArgs = this.engine.getServerArgs();
127
802
 
@@ -132,54 +807,74 @@ export class MinecraftServer extends EventEmitter {
132
807
  ...serverArgs
133
808
  ];
134
809
 
135
- this.logger.info(`Launching: java ${fullArgs.join(' ')}`);
810
+ if (this.options.networkOptimization?.tcpFastOpen) {
811
+ fullArgs.unshift('-Djava.net.preferIPv4Stack=true');
812
+ }
136
813
 
137
- this.process = spawn('java', fullArgs, {
814
+ if (this.options.networkOptimization?.bungeeMode) {
815
+ fullArgs.unshift('-Dnet.kyori.adventure.text.serializer.legacy.AMPMSupport=true');
816
+ }
817
+
818
+ const env = this.buildEnvironment();
819
+
820
+ this.logger.info(`Launching: ${this.javaCommand} ${fullArgs.join(' ')}`);
821
+
822
+ this.process = spawn(this.javaCommand, fullArgs, {
138
823
  cwd: serverDir,
139
- stdio: 'pipe'
824
+ env: env,
825
+ stdio: ['pipe', 'pipe', 'pipe']
140
826
  });
141
827
 
142
828
  this.serverInfo.pid = this.process.pid!;
143
829
  this.serverInfo.status = 'starting';
144
830
  this.startTime = new Date();
145
831
 
146
- this.process.stdout.on('data', (data: Buffer) => {
147
- const output = data.toString();
148
- process.stdout.write(output);
832
+ if (this.options.silentMode) {
833
+ this.process.stdout.pipe(process.stdout);
834
+ this.process.stderr.pipe(process.stderr);
835
+ } else {
836
+ this.process.stdout.on('data', (data: Buffer) => {
837
+ const output = data.toString();
838
+ process.stdout.write(output);
149
839
 
150
- if (output.includes('Done') || output.includes('For help, type "help"')) {
151
- this.serverInfo.status = 'running';
152
- this.logger.success('Server started successfully!');
153
-
154
- if (this.options.enableViaVersion !== false) {
155
- this.logger.info('ViaVersion is active - players from older versions can connect');
840
+ if (output.includes('joined the game')) {
841
+ const match = output.match(/(\w+) joined the game/);
842
+ if (match) {
843
+ this.playerCount++;
844
+ this.handlePlayerJoin(match[1]);
845
+
846
+ if (this.owners.has(match[1].toLowerCase())) {
847
+ this.sendCommand(`tellraw ${match[1]} {"text":"Welcome Owner! Use ${this.ownerCommandPrefix}help for commands","color":"gold"}`);
848
+ }
849
+ }
156
850
  }
157
-
158
- this.emit('ready', this.serverInfo);
159
- }
160
851
 
161
- if (output.includes('joined the game')) {
162
- const match = output.match(/(\w+) joined the game/);
163
- if (match) {
164
- this.handlePlayerJoin(match[1]);
852
+ if (output.includes('left the game')) {
853
+ const match = output.match(/(\w+) left the game/);
854
+ if (match) {
855
+ this.playerCount--;
856
+ this.handlePlayerLeave(match[1]);
857
+ }
165
858
  }
166
- }
167
859
 
168
- if (output.includes('left the game')) {
169
- const match = output.match(/(\w+) left the game/);
170
- if (match) {
171
- this.handlePlayerLeave(match[1]);
860
+ if (output.includes('<') && output.includes('>')) {
861
+ const chatMatch = output.match(/<(\w+)>\s+(.+)/);
862
+ if (chatMatch) {
863
+ const player = chatMatch[1];
864
+ const message = chatMatch[2];
865
+
866
+ if (message.startsWith(this.ownerCommandPrefix)) {
867
+ const command = message.substring(this.ownerCommandPrefix.length);
868
+ this.processOwnerCommand(player, command);
869
+ }
870
+ }
172
871
  }
173
- }
174
-
175
- if (output.includes('[ViaVersion]')) {
176
- this.logger.debug(`[ViaVersion] ${output.trim()}`);
177
- }
178
- });
872
+ });
179
873
 
180
- this.process.stderr.on('data', (data: Buffer) => {
181
- process.stderr.write(data.toString());
182
- });
874
+ this.process.stderr.on('data', (data: Buffer) => {
875
+ process.stderr.write(data.toString());
876
+ });
877
+ }
183
878
 
184
879
  this.process.on('exit', (code: number) => {
185
880
  this.serverInfo.status = 'stopped';
@@ -193,15 +888,122 @@ export class MinecraftServer extends EventEmitter {
193
888
  this.emit('stop', { code });
194
889
  });
195
890
 
891
+ if (this.options.statsInterval && this.options.statsInterval > 0) {
892
+ this.statsInterval = setInterval(() => this.updateStats(), this.options.statsInterval);
893
+ }
894
+
196
895
  this.monitorResources();
197
896
 
198
897
  if (this.config.backup.enabled) {
199
898
  this.setupBackups();
200
899
  }
201
900
 
901
+ setTimeout(() => {
902
+ if (this.serverInfo.status === 'starting') {
903
+ this.serverInfo.status = 'running';
904
+ this.logger.success('Server started successfully!');
905
+
906
+ if (this.options.enableViaVersion !== false) {
907
+ this.logger.info('ViaVersion is active - players from older versions can connect');
908
+ }
909
+
910
+ if (this.options.enableSkinRestorer !== false) {
911
+ this.logger.info('SkinRestorer is active - player skins will be restored');
912
+ }
913
+
914
+ if (this.worldSize > 0) {
915
+ this.logger.info(`World size: ${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB`);
916
+ }
917
+
918
+ if (this.owners.size > 0) {
919
+ this.logger.info(`Owner commands enabled with prefix: ${this.ownerCommandPrefix}`);
920
+ }
921
+
922
+ this.emit('ready', this.serverInfo);
923
+ this.startMemoryMonitor();
924
+ }
925
+ }, 10000);
926
+
202
927
  return this.serverInfo;
203
928
  }
204
929
 
930
+ private startMemoryMonitor(): void {
931
+ if (!this.options.memoryMonitor?.enabled) return;
932
+
933
+ const threshold = this.options.memoryMonitor.threshold || 90;
934
+ const interval = this.options.memoryMonitor.interval || 30000;
935
+ const action = this.options.memoryMonitor.action || 'warn';
936
+
937
+ this.memoryMonitorInterval = setInterval(async () => {
938
+ if (this.serverInfo.status !== 'running' || !this.process) return;
939
+
940
+ try {
941
+ await this.updateStats();
942
+
943
+ const memPercent = (this.serverInfo.memory.used / this.serverInfo.memory.max) * 100;
944
+
945
+ this.memoryUsageHistory.push(memPercent);
946
+ if (this.memoryUsageHistory.length > 10) {
947
+ this.memoryUsageHistory.shift();
948
+ }
949
+
950
+ if (memPercent > threshold) {
951
+ this.logger.warning(`High memory usage: ${memPercent.toFixed(1)}%`);
952
+
953
+ const isIncreasing = this.memoryUsageHistory.length > 5 &&
954
+ this.memoryUsageHistory[this.memoryUsageHistory.length - 1] >
955
+ this.memoryUsageHistory[0] * 1.2;
956
+
957
+ if (isIncreasing) {
958
+ this.logger.warning('Memory leak detected!');
959
+
960
+ switch (action) {
961
+ case 'restart':
962
+ this.logger.info('Restarting server due to memory leak...');
963
+ await this.gracefulRestart();
964
+ break;
965
+ case 'stop':
966
+ this.logger.info('Stopping server due to memory leak...');
967
+ await this.stop();
968
+ break;
969
+ case 'warn':
970
+ default:
971
+ this.logger.warning('Please restart server to free memory');
972
+ }
973
+ }
974
+ }
975
+
976
+ } catch (error) {
977
+ this.logger.error('Memory monitor error:', error);
978
+ }
979
+ }, interval);
980
+ }
981
+
982
+ private async gracefulRestart(): Promise<void> {
983
+ this.logger.info('Initiating graceful restart...');
984
+
985
+ this.sendCommand('say Server restarting in 30 seconds');
986
+ this.sendCommand('save-all');
987
+
988
+ await new Promise(resolve => setTimeout(resolve, 10000));
989
+
990
+ this.sendCommand('say Server restarting in 20 seconds');
991
+
992
+ await new Promise(resolve => setTimeout(resolve, 10000));
993
+
994
+ this.sendCommand('say Server restarting in 10 seconds');
995
+ this.sendCommand('save-all');
996
+
997
+ await new Promise(resolve => setTimeout(resolve, 5000));
998
+
999
+ this.sendCommand('say Server restarting in 5 seconds');
1000
+
1001
+ await new Promise(resolve => setTimeout(resolve, 5000));
1002
+
1003
+ await this.stop();
1004
+ await this.start();
1005
+ }
1006
+
205
1007
  public async stop(): Promise<void> {
206
1008
  if (!this.process) {
207
1009
  this.logger.warning('Server not running');
@@ -211,9 +1013,10 @@ export class MinecraftServer extends EventEmitter {
211
1013
  this.logger.info('Stopping server...');
212
1014
  this.serverInfo.status = 'stopping';
213
1015
 
1016
+ this.sendCommand('save-all');
214
1017
  this.sendCommand('stop');
215
1018
 
216
- await new Promise(resolve => setTimeout(resolve, 5000));
1019
+ await new Promise(resolve => setTimeout(resolve, 10000));
217
1020
 
218
1021
  if (this.process) {
219
1022
  this.process.kill();
@@ -224,6 +1027,14 @@ export class MinecraftServer extends EventEmitter {
224
1027
  this.backupCron.stop();
225
1028
  }
226
1029
 
1030
+ if (this.memoryMonitorInterval) {
1031
+ clearInterval(this.memoryMonitorInterval);
1032
+ }
1033
+
1034
+ if (this.statsInterval) {
1035
+ clearInterval(this.statsInterval);
1036
+ }
1037
+
227
1038
  if (this.config.platform === 'all') {
228
1039
  this.geyser.stop();
229
1040
  }
@@ -241,17 +1052,7 @@ export class MinecraftServer extends EventEmitter {
241
1052
  }
242
1053
 
243
1054
  public async getInfo(): Promise<ServerInfo> {
244
- if (this.serverInfo.status === 'running' && this.process) {
245
- try {
246
- const stats = await pidusage(this.process.pid);
247
- this.serverInfo.memory = {
248
- used: Math.round(stats.memory / 1024 / 1024),
249
- max: this.parseMemory(this.config.memory.max)
250
- };
251
- this.serverInfo.uptime = Math.floor((Date.now() - (this.startTime?.getTime() || 0)) / 1000);
252
- } catch {}
253
- }
254
-
1055
+ await this.updateStats();
255
1056
  return this.serverInfo;
256
1057
  }
257
1058
 
@@ -286,21 +1087,10 @@ export class MinecraftServer extends EventEmitter {
286
1087
  setInterval(async () => {
287
1088
  if (this.serverInfo.status === 'running' && this.process) {
288
1089
  try {
289
- const stats = await pidusage(this.process.pid);
290
- this.serverInfo.memory = {
291
- used: Math.round(stats.memory / 1024 / 1024),
292
- max: this.parseMemory(this.config.memory.max)
293
- };
294
- this.serverInfo.uptime = Math.floor((Date.now() - (this.startTime?.getTime() || 0)) / 1000);
295
-
296
- if (stats.cpu > 80) {
297
- this.logger.warning(`High CPU usage: ${stats.cpu}%`);
298
- }
299
-
300
- this.emit('resource', this.serverInfo);
1090
+ await this.updateStats();
301
1091
  } catch {}
302
1092
  }
303
- }, 5000);
1093
+ }, 30000);
304
1094
  }
305
1095
 
306
1096
  private setupBackups(): void {