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