@dimzxzzx07/mc-headless 2.2.0 → 2.2.2
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.
- package/README.md +48 -31
- package/dist/core/JavaChecker.d.ts +8 -2
- package/dist/core/JavaChecker.d.ts.map +1 -1
- package/dist/core/JavaChecker.js +219 -104
- package/dist/core/JavaChecker.js.map +1 -1
- package/dist/core/MinecraftServer.d.ts +11 -1
- package/dist/core/MinecraftServer.d.ts.map +1 -1
- package/dist/core/MinecraftServer.js +337 -158
- package/dist/core/MinecraftServer.js.map +1 -1
- package/package.json +1 -1
- package/src/core/JavaChecker.ts +224 -108
- package/src/core/MinecraftServer.ts +402 -201
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
2
|
import { spawn } from 'child_process';
|
|
3
3
|
import * as cron from 'node-cron';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as fs from 'fs-extra';
|
|
4
6
|
import { MinecraftConfig, ServerInfo, Player } from '../types';
|
|
5
7
|
import { ConfigHandler } from './ConfigHandler';
|
|
6
8
|
import { JavaChecker, JavaInfo } from './JavaChecker';
|
|
@@ -15,8 +17,6 @@ import { ServerEngine } from '../engines/ServerEngine';
|
|
|
15
17
|
import { GeyserBridge } from '../platforms/GeyserBridge';
|
|
16
18
|
import { ViaVersionManager } from '../platforms/ViaVersion';
|
|
17
19
|
import { SkinRestorerManager } from '../platforms/SkinRestorer';
|
|
18
|
-
import * as path from 'path';
|
|
19
|
-
import * as fs from 'fs-extra';
|
|
20
20
|
|
|
21
21
|
export interface MinecraftServerOptions extends Partial<MinecraftConfig> {
|
|
22
22
|
enableViaVersion?: boolean;
|
|
@@ -46,6 +46,8 @@ export interface MinecraftServerOptions extends Partial<MinecraftConfig> {
|
|
|
46
46
|
};
|
|
47
47
|
silentMode?: boolean;
|
|
48
48
|
statsInterval?: number;
|
|
49
|
+
cleanLogsInterval?: number;
|
|
50
|
+
optimizeForPterodactyl?: boolean;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
export class MinecraftServer extends EventEmitter {
|
|
@@ -60,6 +62,7 @@ export class MinecraftServer extends EventEmitter {
|
|
|
60
62
|
private serverInfo: ServerInfo;
|
|
61
63
|
private players: Map<string, Player> = new Map();
|
|
62
64
|
private backupCron: cron.ScheduledTask | null = null;
|
|
65
|
+
private cleanLogsCron: cron.ScheduledTask | null = null;
|
|
63
66
|
private startTime: Date | null = null;
|
|
64
67
|
private memoryMonitorInterval: NodeJS.Timeout | null = null;
|
|
65
68
|
private statsInterval: NodeJS.Timeout | null = null;
|
|
@@ -75,15 +78,18 @@ export class MinecraftServer extends EventEmitter {
|
|
|
75
78
|
private cpuUsage: number = 0;
|
|
76
79
|
private cgroupMemory: number = 0;
|
|
77
80
|
private cgroupCpu: number = 0;
|
|
81
|
+
private isPterodactyl: boolean = false;
|
|
78
82
|
|
|
79
83
|
constructor(userConfig: MinecraftServerOptions = {}) {
|
|
80
84
|
super();
|
|
81
85
|
this.logger = Logger.getInstance();
|
|
82
86
|
this.logger.banner();
|
|
83
87
|
|
|
88
|
+
this.detectEnvironment();
|
|
89
|
+
|
|
84
90
|
this.options = {
|
|
85
91
|
javaVersion: 'auto',
|
|
86
|
-
usePortableJava:
|
|
92
|
+
usePortableJava: !this.isPterodactyl,
|
|
87
93
|
memoryMonitor: {
|
|
88
94
|
enabled: true,
|
|
89
95
|
threshold: 90,
|
|
@@ -103,6 +109,8 @@ export class MinecraftServer extends EventEmitter {
|
|
|
103
109
|
},
|
|
104
110
|
silentMode: true,
|
|
105
111
|
statsInterval: 30000,
|
|
112
|
+
cleanLogsInterval: 3 * 60 * 60 * 1000,
|
|
113
|
+
optimizeForPterodactyl: true,
|
|
106
114
|
...userConfig
|
|
107
115
|
};
|
|
108
116
|
|
|
@@ -117,6 +125,10 @@ export class MinecraftServer extends EventEmitter {
|
|
|
117
125
|
const handler = new ConfigHandler(userConfig);
|
|
118
126
|
this.config = handler.getConfig();
|
|
119
127
|
|
|
128
|
+
if (this.isPterodactyl && this.options.optimizeForPterodactyl) {
|
|
129
|
+
this.optimizeForPterodactylPanel();
|
|
130
|
+
}
|
|
131
|
+
|
|
120
132
|
this.engine = this.createEngine();
|
|
121
133
|
this.geyser = new GeyserBridge();
|
|
122
134
|
this.viaVersion = new ViaVersionManager();
|
|
@@ -139,6 +151,181 @@ export class MinecraftServer extends EventEmitter {
|
|
|
139
151
|
};
|
|
140
152
|
|
|
141
153
|
this.detectCgroupLimits();
|
|
154
|
+
this.cleanPaperCache();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private detectEnvironment(): void {
|
|
158
|
+
this.isPterodactyl = !!(
|
|
159
|
+
process.env.PTERODACTYL ||
|
|
160
|
+
process.env.SERVER_PORT ||
|
|
161
|
+
process.env.SERVER_MEMORY_MAX ||
|
|
162
|
+
fs.existsSync('/home/container')
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
if (this.isPterodactyl) {
|
|
166
|
+
this.logger.info('Pterodactyl environment detected');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private optimizeForPterodactylPanel(): void {
|
|
171
|
+
const pterodactylPort = process.env.SERVER_PORT;
|
|
172
|
+
if (pterodactylPort) {
|
|
173
|
+
this.config.network.port = parseInt(pterodactylPort);
|
|
174
|
+
this.logger.info(`Using Pterodactyl port: ${pterodactylPort}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const pterodactylMemory = process.env.SERVER_MEMORY_MAX;
|
|
178
|
+
if (pterodactylMemory) {
|
|
179
|
+
this.config.memory.max = pterodactylMemory;
|
|
180
|
+
this.logger.info(`Using Pterodactyl memory: ${pterodactylMemory}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const pterodactylInitMemory = process.env.SERVER_MEMORY_INIT;
|
|
184
|
+
if (pterodactylInitMemory) {
|
|
185
|
+
this.config.memory.init = pterodactylInitMemory;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.config.world.viewDistance = 6;
|
|
189
|
+
this.config.world.simulationDistance = 4;
|
|
190
|
+
|
|
191
|
+
this.logger.info('Applied Pterodactyl optimizations');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private cleanPaperCache(): void {
|
|
195
|
+
const cachePaths = [
|
|
196
|
+
path.join(process.cwd(), 'plugins', '.paper-remapped'),
|
|
197
|
+
path.join(process.cwd(), 'plugins', '.paper-remapped', 'geyser.jar'),
|
|
198
|
+
path.join(process.cwd(), 'plugins', '.paper-remapped', 'ViaVersion.jar'),
|
|
199
|
+
path.join(process.cwd(), 'cache', 'paper-remapped')
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
cachePaths.forEach(cachePath => {
|
|
203
|
+
if (fs.existsSync(cachePath)) {
|
|
204
|
+
this.logger.info(`Cleaning corrupt cache: ${cachePath}`);
|
|
205
|
+
try {
|
|
206
|
+
fs.rmSync(cachePath, { recursive: true, force: true });
|
|
207
|
+
this.logger.debug(`Cache cleaned: ${cachePath}`);
|
|
208
|
+
} catch (err: any) {
|
|
209
|
+
this.logger.warning(`Failed to clean cache: ${err.message}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private cleanOldLogs(): void {
|
|
216
|
+
const logsDir = path.join(process.cwd(), 'logs');
|
|
217
|
+
if (!fs.existsSync(logsDir)) return;
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const files = fs.readdirSync(logsDir);
|
|
221
|
+
const now = Date.now();
|
|
222
|
+
const threeHours = 3 * 60 * 60 * 1000;
|
|
223
|
+
|
|
224
|
+
files.forEach(file => {
|
|
225
|
+
const filePath = path.join(logsDir, file);
|
|
226
|
+
const stats = fs.statSync(filePath);
|
|
227
|
+
if (now - stats.mtimeMs > threeHours) {
|
|
228
|
+
fs.unlinkSync(filePath);
|
|
229
|
+
this.logger.debug(`Cleaned old log: ${file}`);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
this.logger.info('Old logs cleaned');
|
|
233
|
+
} catch (err: any) {
|
|
234
|
+
this.logger.warning(`Failed to clean logs: ${err.message}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private generateOptimizedConfigs(): void {
|
|
239
|
+
const serverDir = process.cwd();
|
|
240
|
+
|
|
241
|
+
const paperGlobalYml = path.join(serverDir, 'paper-global.yml');
|
|
242
|
+
if (!fs.existsSync(paperGlobalYml)) {
|
|
243
|
+
const paperGlobalConfig = `# Paper Global Configuration
|
|
244
|
+
# Optimized by MC-Headless
|
|
245
|
+
|
|
246
|
+
chunk-loading:
|
|
247
|
+
autoconfig-send-distance: false
|
|
248
|
+
enable-frustum-priority: false
|
|
249
|
+
global-max-chunk-load-rate: 100
|
|
250
|
+
global-max-chunk-send-rate: 50
|
|
251
|
+
|
|
252
|
+
entities:
|
|
253
|
+
despawn-ranges:
|
|
254
|
+
monster:
|
|
255
|
+
soft: 28
|
|
256
|
+
hard: 96
|
|
257
|
+
animal:
|
|
258
|
+
soft: 16
|
|
259
|
+
hard: 32
|
|
260
|
+
spawn-limits:
|
|
261
|
+
ambient: 5
|
|
262
|
+
axolotls: 5
|
|
263
|
+
creature: 10
|
|
264
|
+
monster: 15
|
|
265
|
+
water_ambient: 5
|
|
266
|
+
water_creature: 5
|
|
267
|
+
|
|
268
|
+
misc:
|
|
269
|
+
max-entity-speed: 100
|
|
270
|
+
redstone-implementation: ALTERNATIVE_CURRENT
|
|
271
|
+
|
|
272
|
+
player-auto-save: 6000
|
|
273
|
+
player-auto-save-rate: 1200
|
|
274
|
+
no-tick-view-distance: 12
|
|
275
|
+
`;
|
|
276
|
+
fs.writeFileSync(paperGlobalYml, paperGlobalConfig);
|
|
277
|
+
this.logger.debug('Generated paper-global.yml');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const paperWorldDefaultsYml = path.join(serverDir, 'paper-world-defaults.yml');
|
|
281
|
+
if (!fs.existsSync(paperWorldDefaultsYml)) {
|
|
282
|
+
const paperWorldConfig = `# Paper World Defaults
|
|
283
|
+
# Optimized by MC-Headless
|
|
284
|
+
|
|
285
|
+
view-distance: 6
|
|
286
|
+
no-tick-view-distance: 12
|
|
287
|
+
|
|
288
|
+
despawn-ranges:
|
|
289
|
+
monster:
|
|
290
|
+
soft: 28
|
|
291
|
+
hard: 96
|
|
292
|
+
animal:
|
|
293
|
+
soft: 16
|
|
294
|
+
hard: 32
|
|
295
|
+
ambient:
|
|
296
|
+
soft: 16
|
|
297
|
+
hard: 32
|
|
298
|
+
`;
|
|
299
|
+
fs.writeFileSync(paperWorldDefaultsYml, paperWorldConfig);
|
|
300
|
+
this.logger.debug('Generated paper-world-defaults.yml');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const spigotYml = path.join(serverDir, 'spigot.yml');
|
|
304
|
+
if (!fs.existsSync(spigotYml)) {
|
|
305
|
+
const spigotConfig = `# Spigot Configuration
|
|
306
|
+
# Optimized by MC-Headless
|
|
307
|
+
|
|
308
|
+
entity-activation-range:
|
|
309
|
+
animals: 16
|
|
310
|
+
monsters: 24
|
|
311
|
+
misc: 8
|
|
312
|
+
water: 8
|
|
313
|
+
|
|
314
|
+
mob-spawn-range: 4
|
|
315
|
+
|
|
316
|
+
world-settings:
|
|
317
|
+
default:
|
|
318
|
+
view-distance: 6
|
|
319
|
+
mob-spawn-range: 4
|
|
320
|
+
entity-activation-range:
|
|
321
|
+
animals: 16
|
|
322
|
+
monsters: 24
|
|
323
|
+
misc: 8
|
|
324
|
+
water: 8
|
|
325
|
+
`;
|
|
326
|
+
fs.writeFileSync(spigotYml, spigotConfig);
|
|
327
|
+
this.logger.debug('Generated spigot.yml');
|
|
328
|
+
}
|
|
142
329
|
}
|
|
143
330
|
|
|
144
331
|
private detectCgroupLimits(): void {
|
|
@@ -187,11 +374,16 @@ export class MinecraftServer extends EventEmitter {
|
|
|
187
374
|
const memMax = this.parseMemory(this.config.memory.max);
|
|
188
375
|
const javaVersion = this.options.javaVersion || 'auto';
|
|
189
376
|
|
|
377
|
+
const baseArgs = [
|
|
378
|
+
`-Xms${this.config.memory.init}`,
|
|
379
|
+
`-Xmx${this.config.memory.max}`,
|
|
380
|
+
'-XX:+UseG1GC'
|
|
381
|
+
];
|
|
382
|
+
|
|
190
383
|
let gcArgs: string[] = [];
|
|
191
384
|
|
|
192
385
|
if (memMax >= 16384) {
|
|
193
386
|
gcArgs = [
|
|
194
|
-
'-XX:+UseG1GC',
|
|
195
387
|
'-XX:+ParallelRefProcEnabled',
|
|
196
388
|
'-XX:MaxGCPauseMillis=100',
|
|
197
389
|
'-XX:+UnlockExperimentalVMOptions',
|
|
@@ -212,7 +404,6 @@ export class MinecraftServer extends EventEmitter {
|
|
|
212
404
|
];
|
|
213
405
|
} else if (memMax >= 8192) {
|
|
214
406
|
gcArgs = [
|
|
215
|
-
'-XX:+UseG1GC',
|
|
216
407
|
'-XX:+ParallelRefProcEnabled',
|
|
217
408
|
'-XX:MaxGCPauseMillis=150',
|
|
218
409
|
'-XX:+UnlockExperimentalVMOptions',
|
|
@@ -233,7 +424,6 @@ export class MinecraftServer extends EventEmitter {
|
|
|
233
424
|
];
|
|
234
425
|
} else {
|
|
235
426
|
gcArgs = this.config.memory.useAikarsFlags ? [
|
|
236
|
-
'-XX:+UseG1GC',
|
|
237
427
|
'-XX:+ParallelRefProcEnabled',
|
|
238
428
|
'-XX:MaxGCPauseMillis=200',
|
|
239
429
|
'-XX:+UnlockExperimentalVMOptions',
|
|
@@ -256,11 +446,6 @@ export class MinecraftServer extends EventEmitter {
|
|
|
256
446
|
gcArgs.push('--enable-preview');
|
|
257
447
|
}
|
|
258
448
|
|
|
259
|
-
const baseArgs = [
|
|
260
|
-
`-Xms${this.config.memory.init}`,
|
|
261
|
-
`-Xmx${this.config.memory.max}`
|
|
262
|
-
];
|
|
263
|
-
|
|
264
449
|
return [...baseArgs, ...gcArgs];
|
|
265
450
|
}
|
|
266
451
|
|
|
@@ -268,7 +453,6 @@ export class MinecraftServer extends EventEmitter {
|
|
|
268
453
|
const env: NodeJS.ProcessEnv = { ...process.env };
|
|
269
454
|
|
|
270
455
|
env.MALLOC_ARENA_MAX = '2';
|
|
271
|
-
|
|
272
456
|
env._JAVA_OPTIONS = `-Xmx${this.config.memory.max}`;
|
|
273
457
|
|
|
274
458
|
if (this.javaInfo && this.javaInfo.type === 'portable') {
|
|
@@ -288,6 +472,207 @@ export class MinecraftServer extends EventEmitter {
|
|
|
288
472
|
return env;
|
|
289
473
|
}
|
|
290
474
|
|
|
475
|
+
public async start(): Promise<ServerInfo> {
|
|
476
|
+
this.logger.info(`Starting ${this.config.type} server v${this.config.version}...`);
|
|
477
|
+
|
|
478
|
+
this.cleanPaperCache();
|
|
479
|
+
this.generateOptimizedConfigs();
|
|
480
|
+
|
|
481
|
+
if (this.options.cleanLogsInterval && this.options.cleanLogsInterval > 0) {
|
|
482
|
+
this.cleanLogsCron = cron.schedule('0 */3 * * *', () => this.cleanOldLogs());
|
|
483
|
+
this.logger.info('Log cleanup scheduled every 3 hours');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (this.owners.size > 0) {
|
|
487
|
+
this.logger.info(`Owners configured: ${Array.from(this.owners).join(', ')}`);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
this.javaInfo = await JavaChecker.ensureJava(
|
|
491
|
+
this.config.version,
|
|
492
|
+
this.options.usePortableJava || false
|
|
493
|
+
);
|
|
494
|
+
this.javaCommand = this.javaInfo.path;
|
|
495
|
+
this.logger.success(`Using Java ${this.javaInfo.version} (${this.javaInfo.type}) at ${this.javaInfo.path}`);
|
|
496
|
+
|
|
497
|
+
const systemInfo = SystemDetector.getSystemInfo();
|
|
498
|
+
this.logger.debug('System info:', systemInfo);
|
|
499
|
+
|
|
500
|
+
const serverDir = process.cwd();
|
|
501
|
+
await FileUtils.ensureServerStructure(this.config);
|
|
502
|
+
|
|
503
|
+
this.worldSize = await this.calculateWorldSize();
|
|
504
|
+
if (this.worldSize > 10 * 1024 * 1024 * 1024) {
|
|
505
|
+
this.logger.warning(`Large world detected (${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB). Consider increasing memory allocation.`);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const jarPath = await this.engine.download(this.config, serverDir);
|
|
509
|
+
|
|
510
|
+
if (this.config.type === 'forge') {
|
|
511
|
+
await this.engine.prepare(this.config, serverDir, jarPath);
|
|
512
|
+
} else {
|
|
513
|
+
await this.engine.prepare(this.config, serverDir);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (this.config.platform === 'all') {
|
|
517
|
+
await FileUtils.ensureDir(this.config.folders.plugins);
|
|
518
|
+
await this.geyser.setup(this.config);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (this.options.enableViaVersion !== false) {
|
|
522
|
+
this.logger.info('Enabling ViaVersion for client version compatibility...');
|
|
523
|
+
await FileUtils.ensureDir(this.config.folders.plugins);
|
|
524
|
+
await this.viaVersion.setup(this.config);
|
|
525
|
+
await this.viaVersion.configureViaVersion(this.config);
|
|
526
|
+
|
|
527
|
+
if (this.options.enableViaBackwards !== false) {
|
|
528
|
+
this.logger.info('ViaBackwards will be installed');
|
|
529
|
+
}
|
|
530
|
+
if (this.options.enableViaRewind !== false) {
|
|
531
|
+
this.logger.info('ViaRewind will be installed');
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (this.options.enableSkinRestorer !== false) {
|
|
536
|
+
this.logger.info('Enabling SkinRestorer for player skins...');
|
|
537
|
+
await FileUtils.ensureDir(this.config.folders.plugins);
|
|
538
|
+
await this.skinRestorer.setup(this.config);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const javaArgs = this.buildJavaArgs();
|
|
542
|
+
const serverJar = this.engine.getServerJar(jarPath);
|
|
543
|
+
const serverArgs = this.engine.getServerArgs();
|
|
544
|
+
|
|
545
|
+
const fullArgs = [
|
|
546
|
+
...javaArgs,
|
|
547
|
+
'-jar',
|
|
548
|
+
serverJar,
|
|
549
|
+
...serverArgs
|
|
550
|
+
];
|
|
551
|
+
|
|
552
|
+
if (this.options.networkOptimization?.tcpFastOpen) {
|
|
553
|
+
fullArgs.unshift('-Djava.net.preferIPv4Stack=true');
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (this.options.networkOptimization?.bungeeMode) {
|
|
557
|
+
fullArgs.unshift('-Dnet.kyori.adventure.text.serializer.legacy.AMPMSupport=true');
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const env = this.buildEnvironment();
|
|
561
|
+
|
|
562
|
+
this.logger.info(`Launching: ${this.javaCommand} ${fullArgs.join(' ')}`);
|
|
563
|
+
|
|
564
|
+
this.process = spawn(this.javaCommand, fullArgs, {
|
|
565
|
+
cwd: serverDir,
|
|
566
|
+
env: env,
|
|
567
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
this.serverInfo.pid = this.process.pid!;
|
|
571
|
+
this.serverInfo.status = 'starting';
|
|
572
|
+
this.startTime = new Date();
|
|
573
|
+
|
|
574
|
+
if (this.options.silentMode) {
|
|
575
|
+
this.process.stdout.pipe(process.stdout);
|
|
576
|
+
this.process.stderr.pipe(process.stderr);
|
|
577
|
+
} else {
|
|
578
|
+
this.setupLogHandlers();
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
this.process.on('exit', (code: number) => {
|
|
582
|
+
this.serverInfo.status = 'stopped';
|
|
583
|
+
this.logger.warning(`Server stopped with code ${code}`);
|
|
584
|
+
|
|
585
|
+
if (this.config.autoRestart && code !== 0) {
|
|
586
|
+
this.logger.info('Auto-restarting...');
|
|
587
|
+
this.start();
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
this.emit('stop', { code });
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
if (this.options.statsInterval && this.options.statsInterval > 0) {
|
|
594
|
+
this.statsInterval = setInterval(() => this.updateStats(), this.options.statsInterval);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
this.monitorResources();
|
|
598
|
+
|
|
599
|
+
if (this.config.backup.enabled) {
|
|
600
|
+
this.setupBackups();
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
setTimeout(() => {
|
|
604
|
+
if (this.serverInfo.status === 'starting') {
|
|
605
|
+
this.serverInfo.status = 'running';
|
|
606
|
+
this.logger.success('Server started successfully!');
|
|
607
|
+
|
|
608
|
+
if (this.options.enableViaVersion !== false) {
|
|
609
|
+
this.logger.info('ViaVersion is active - players from older versions can connect');
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (this.options.enableSkinRestorer !== false) {
|
|
613
|
+
this.logger.info('SkinRestorer is active - player skins will be restored');
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (this.worldSize > 0) {
|
|
617
|
+
this.logger.info(`World size: ${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB`);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (this.owners.size > 0) {
|
|
621
|
+
this.logger.info(`Owner commands enabled with prefix: ${this.ownerCommandPrefix}`);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
this.emit('ready', this.serverInfo);
|
|
625
|
+
this.startMemoryMonitor();
|
|
626
|
+
}
|
|
627
|
+
}, 10000);
|
|
628
|
+
|
|
629
|
+
return this.serverInfo;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
private setupLogHandlers(): void {
|
|
633
|
+
this.process.stdout.on('data', (data: Buffer) => {
|
|
634
|
+
const output = data.toString();
|
|
635
|
+
process.stdout.write(output);
|
|
636
|
+
|
|
637
|
+
if (output.includes('joined the game')) {
|
|
638
|
+
const match = output.match(/(\w+) joined the game/);
|
|
639
|
+
if (match) {
|
|
640
|
+
this.playerCount++;
|
|
641
|
+
this.handlePlayerJoin(match[1]);
|
|
642
|
+
|
|
643
|
+
if (this.owners.has(match[1].toLowerCase())) {
|
|
644
|
+
this.sendCommand(`tellraw ${match[1]} {"text":"Welcome Owner! Use ${this.ownerCommandPrefix}help for commands","color":"gold"}`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (output.includes('left the game')) {
|
|
650
|
+
const match = output.match(/(\w+) left the game/);
|
|
651
|
+
if (match) {
|
|
652
|
+
this.playerCount--;
|
|
653
|
+
this.handlePlayerLeave(match[1]);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (output.includes('<') && output.includes('>')) {
|
|
658
|
+
const chatMatch = output.match(/<(\w+)>\s+(.+)/);
|
|
659
|
+
if (chatMatch) {
|
|
660
|
+
const player = chatMatch[1];
|
|
661
|
+
const message = chatMatch[2];
|
|
662
|
+
|
|
663
|
+
if (message.startsWith(this.ownerCommandPrefix)) {
|
|
664
|
+
const command = message.substring(this.ownerCommandPrefix.length);
|
|
665
|
+
this.processOwnerCommand(player, command);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
this.process.stderr.on('data', (data: Buffer) => {
|
|
672
|
+
process.stderr.write(data.toString());
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
|
|
291
676
|
private processOwnerCommand(player: string, command: string): void {
|
|
292
677
|
if (!this.options.ownerCommands?.enabled) return;
|
|
293
678
|
if (!this.owners.has(player.toLowerCase())) return;
|
|
@@ -588,195 +973,6 @@ export class MinecraftServer extends EventEmitter {
|
|
|
588
973
|
}
|
|
589
974
|
}
|
|
590
975
|
|
|
591
|
-
public async start(): Promise<ServerInfo> {
|
|
592
|
-
this.logger.info(`Starting ${this.config.type} server v${this.config.version}...`);
|
|
593
|
-
|
|
594
|
-
if (this.owners.size > 0) {
|
|
595
|
-
this.logger.info(`Owners configured: ${Array.from(this.owners).join(', ')}`);
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
this.javaInfo = await JavaChecker.ensureJava(
|
|
599
|
-
this.config.version,
|
|
600
|
-
this.options.usePortableJava || false
|
|
601
|
-
);
|
|
602
|
-
this.javaCommand = this.javaInfo.path;
|
|
603
|
-
this.logger.success(`Using Java ${this.javaInfo.version} (${this.javaInfo.type}) at ${this.javaInfo.path}`);
|
|
604
|
-
|
|
605
|
-
const systemInfo = SystemDetector.getSystemInfo();
|
|
606
|
-
this.logger.debug('System info:', systemInfo);
|
|
607
|
-
|
|
608
|
-
const serverDir = process.cwd();
|
|
609
|
-
await FileUtils.ensureServerStructure(this.config);
|
|
610
|
-
|
|
611
|
-
this.worldSize = await this.calculateWorldSize();
|
|
612
|
-
if (this.worldSize > 10 * 1024 * 1024 * 1024) {
|
|
613
|
-
this.logger.warning(`Large world detected (${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB). Consider increasing memory allocation.`);
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
const jarPath = await this.engine.download(this.config, serverDir);
|
|
617
|
-
|
|
618
|
-
if (this.config.type === 'forge') {
|
|
619
|
-
await this.engine.prepare(this.config, serverDir, jarPath);
|
|
620
|
-
} else {
|
|
621
|
-
await this.engine.prepare(this.config, serverDir);
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
if (this.config.platform === 'all') {
|
|
625
|
-
await FileUtils.ensureDir(this.config.folders.plugins);
|
|
626
|
-
await this.geyser.setup(this.config);
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
if (this.options.enableViaVersion !== false) {
|
|
630
|
-
this.logger.info('Enabling ViaVersion for client version compatibility...');
|
|
631
|
-
await FileUtils.ensureDir(this.config.folders.plugins);
|
|
632
|
-
await this.viaVersion.setup(this.config);
|
|
633
|
-
await this.viaVersion.configureViaVersion(this.config);
|
|
634
|
-
|
|
635
|
-
if (this.options.enableViaBackwards !== false) {
|
|
636
|
-
this.logger.info('ViaBackwards will be installed');
|
|
637
|
-
}
|
|
638
|
-
if (this.options.enableViaRewind !== false) {
|
|
639
|
-
this.logger.info('ViaRewind will be installed');
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
if (this.options.enableSkinRestorer !== false) {
|
|
644
|
-
this.logger.info('Enabling SkinRestorer for player skins...');
|
|
645
|
-
await FileUtils.ensureDir(this.config.folders.plugins);
|
|
646
|
-
await this.skinRestorer.setup(this.config);
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
const javaArgs = this.buildJavaArgs();
|
|
650
|
-
const serverJar = this.engine.getServerJar(jarPath);
|
|
651
|
-
const serverArgs = this.engine.getServerArgs();
|
|
652
|
-
|
|
653
|
-
const fullArgs = [
|
|
654
|
-
...javaArgs,
|
|
655
|
-
'-jar',
|
|
656
|
-
serverJar,
|
|
657
|
-
...serverArgs
|
|
658
|
-
];
|
|
659
|
-
|
|
660
|
-
if (this.options.networkOptimization?.tcpFastOpen) {
|
|
661
|
-
fullArgs.unshift('-Djava.net.preferIPv4Stack=true');
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
if (this.options.networkOptimization?.bungeeMode) {
|
|
665
|
-
fullArgs.unshift('-Dnet.kyori.adventure.text.serializer.legacy.AMPMSupport=true');
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
const env = this.buildEnvironment();
|
|
669
|
-
|
|
670
|
-
this.logger.info(`Launching: ${this.javaCommand} ${fullArgs.join(' ')}`);
|
|
671
|
-
|
|
672
|
-
this.process = spawn(this.javaCommand, fullArgs, {
|
|
673
|
-
cwd: serverDir,
|
|
674
|
-
env: env,
|
|
675
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
this.serverInfo.pid = this.process.pid!;
|
|
679
|
-
this.serverInfo.status = 'starting';
|
|
680
|
-
this.startTime = new Date();
|
|
681
|
-
|
|
682
|
-
if (this.options.silentMode) {
|
|
683
|
-
this.process.stdout.pipe(process.stdout);
|
|
684
|
-
this.process.stderr.pipe(process.stderr);
|
|
685
|
-
} else {
|
|
686
|
-
this.process.stdout.on('data', (data: Buffer) => {
|
|
687
|
-
const output = data.toString();
|
|
688
|
-
process.stdout.write(output);
|
|
689
|
-
|
|
690
|
-
if (output.includes('joined the game')) {
|
|
691
|
-
const match = output.match(/(\w+) joined the game/);
|
|
692
|
-
if (match) {
|
|
693
|
-
this.playerCount++;
|
|
694
|
-
this.handlePlayerJoin(match[1]);
|
|
695
|
-
|
|
696
|
-
if (this.owners.has(match[1].toLowerCase())) {
|
|
697
|
-
this.sendCommand(`tellraw ${match[1]} {"text":"Welcome Owner! Use ${this.ownerCommandPrefix}help for commands","color":"gold"}`);
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
if (output.includes('left the game')) {
|
|
703
|
-
const match = output.match(/(\w+) left the game/);
|
|
704
|
-
if (match) {
|
|
705
|
-
this.playerCount--;
|
|
706
|
-
this.handlePlayerLeave(match[1]);
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
if (output.includes('<') && output.includes('>')) {
|
|
711
|
-
const chatMatch = output.match(/<(\w+)>\s+(.+)/);
|
|
712
|
-
if (chatMatch) {
|
|
713
|
-
const player = chatMatch[1];
|
|
714
|
-
const message = chatMatch[2];
|
|
715
|
-
|
|
716
|
-
if (message.startsWith(this.ownerCommandPrefix)) {
|
|
717
|
-
const command = message.substring(this.ownerCommandPrefix.length);
|
|
718
|
-
this.processOwnerCommand(player, command);
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
});
|
|
723
|
-
|
|
724
|
-
this.process.stderr.on('data', (data: Buffer) => {
|
|
725
|
-
process.stderr.write(data.toString());
|
|
726
|
-
});
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
this.process.on('exit', (code: number) => {
|
|
730
|
-
this.serverInfo.status = 'stopped';
|
|
731
|
-
this.logger.warning(`Server stopped with code ${code}`);
|
|
732
|
-
|
|
733
|
-
if (this.config.autoRestart && code !== 0) {
|
|
734
|
-
this.logger.info('Auto-restarting...');
|
|
735
|
-
this.start();
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
this.emit('stop', { code });
|
|
739
|
-
});
|
|
740
|
-
|
|
741
|
-
if (this.options.statsInterval && this.options.statsInterval > 0) {
|
|
742
|
-
this.statsInterval = setInterval(() => this.updateStats(), this.options.statsInterval);
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
this.monitorResources();
|
|
746
|
-
|
|
747
|
-
if (this.config.backup.enabled) {
|
|
748
|
-
this.setupBackups();
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
setTimeout(() => {
|
|
752
|
-
if (this.serverInfo.status === 'starting') {
|
|
753
|
-
this.serverInfo.status = 'running';
|
|
754
|
-
this.logger.success('Server started successfully!');
|
|
755
|
-
|
|
756
|
-
if (this.options.enableViaVersion !== false) {
|
|
757
|
-
this.logger.info('ViaVersion is active - players from older versions can connect');
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
if (this.options.enableSkinRestorer !== false) {
|
|
761
|
-
this.logger.info('SkinRestorer is active - player skins will be restored');
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
if (this.worldSize > 0) {
|
|
765
|
-
this.logger.info(`World size: ${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB`);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
if (this.owners.size > 0) {
|
|
769
|
-
this.logger.info(`Owner commands enabled with prefix: ${this.ownerCommandPrefix}`);
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
this.emit('ready', this.serverInfo);
|
|
773
|
-
this.startMemoryMonitor();
|
|
774
|
-
}
|
|
775
|
-
}, 10000);
|
|
776
|
-
|
|
777
|
-
return this.serverInfo;
|
|
778
|
-
}
|
|
779
|
-
|
|
780
976
|
private startMemoryMonitor(): void {
|
|
781
977
|
if (!this.options.memoryMonitor?.enabled) return;
|
|
782
978
|
|
|
@@ -877,6 +1073,10 @@ export class MinecraftServer extends EventEmitter {
|
|
|
877
1073
|
this.backupCron.stop();
|
|
878
1074
|
}
|
|
879
1075
|
|
|
1076
|
+
if (this.cleanLogsCron) {
|
|
1077
|
+
this.cleanLogsCron.stop();
|
|
1078
|
+
}
|
|
1079
|
+
|
|
880
1080
|
if (this.memoryMonitorInterval) {
|
|
881
1081
|
clearInterval(this.memoryMonitorInterval);
|
|
882
1082
|
}
|
|
@@ -889,6 +1089,7 @@ export class MinecraftServer extends EventEmitter {
|
|
|
889
1089
|
this.geyser.stop();
|
|
890
1090
|
}
|
|
891
1091
|
|
|
1092
|
+
this.cleanPaperCache();
|
|
892
1093
|
this.logger.success('Server stopped');
|
|
893
1094
|
}
|
|
894
1095
|
|