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