V6 initial #507
@@ -0,0 +1,9 @@
|
||||
import * as Bedrock from "./platform/Bedrock.js";
|
||||
|
||||
export default {
|
||||
Bedrock: Bedrock.default,
|
||||
};
|
||||
|
||||
export {
|
||||
Bedrock,
|
||||
}
|
@@ -1,13 +1,205 @@
|
||||
import { serverManeger, pathOptions } from "../serverManeger.js";
|
||||
import { createServerManeger, platformPathID, pathOptions, serverConfig } from "../serverManeger.js";
|
||||
import { promises as fs, createWriteStream } from "node:fs";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
import coreUtils, { childPromisses } from "@sirherobrine23/coreutils";
|
||||
import path from "node:path";
|
||||
import tar from "tar";
|
||||
import AdmZip from "adm-zip";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
export default async function Bedrock(options?: pathOptions & {variant?: string}) {
|
||||
options = {...options};
|
||||
export type bedrockRootOption = pathOptions & {
|
||||
variant?: "oficial"|"Pocketmine-PMMP"|"Powernukkit"|"Cloudbust"
|
||||
};
|
||||
|
||||
return serverManeger({
|
||||
const hostArchEmulate = [
|
||||
"qemu-x86_64-static",
|
||||
"qemu-x86_64",
|
||||
"box64"
|
||||
];
|
||||
|
||||
type bedrockVersionJSON = {
|
||||
version: string,
|
||||
date: Date,
|
||||
release?: "stable"|"preview",
|
||||
url: {
|
||||
[platform in NodeJS.Platform]?: {
|
||||
[arch in NodeJS.Architecture]?: string
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function getPHPBin(options?: bedrockRootOption) {
|
||||
options = {variant: "oficial", ...options};
|
||||
const serverPath = await platformPathID("bedrock", options);
|
||||
const binFolder = path.join(serverPath.serverPath, "bin");
|
||||
const files = await coreUtils.extendFs.readdir({folderPath: binFolder});
|
||||
const file = files.filter((v) => v.endsWith("php.exe")||v.endsWith("php")).at(0);
|
||||
if (!file) throw new Error("PHP Bin not found");
|
||||
return file;
|
||||
}
|
||||
|
||||
const oracleBucket = await coreUtils.oracleBucket("sa-saopaulo-1", "bdsFiles", "grwodtg32n4d", "0IKM-5KFpAF8PuWoVe86QFsF4sipU2rXfojpaOMEdf4QgFQLcLlDWgMSPHWmjf5W");
|
||||
export async function installServer(version: string, options?: bedrockRootOption) {
|
||||
options = {variant: "oficial", ...options};
|
||||
const serverPath = await platformPathID("bedrock", options);
|
||||
if (options?.variant === "Pocketmine-PMMP") {
|
||||
const phpBin = (await oracleBucket.fileList()).filter(({name}) => name.startsWith("/php_bin/")).filter(({name}) => name.includes(process.platform) && name.includes(process.arch)).at(0);
|
||||
if (!phpBin) throw new Error("PHP Bin not found");
|
||||
const binFolder = path.join(serverPath.serverPath, "bin");
|
||||
if (await coreUtils.extendFs.exists(binFolder)) await fs.rm(binFolder, {recursive: true});
|
||||
await fs.mkdir(binFolder);
|
||||
await pipeline(await oracleBucket.getFileStream(phpBin.name), createWriteStream(path.join(binFolder, "phpTmp")));
|
||||
|
||||
if (phpBin.name.endsWith(".tar.gz")) {
|
||||
await tar.extract({
|
||||
file: path.join(binFolder, "phpTmp"),
|
||||
cwd: binFolder
|
||||
});
|
||||
} else if (phpBin.name.endsWith(".zip")) {
|
||||
await promisify((new AdmZip(path.join(binFolder, "phpTmp"))).extractAllToAsync)(binFolder, true, true);
|
||||
}
|
||||
await fs.rm(path.join(binFolder, "phpTmp"));
|
||||
const phpBinPath = await getPHPBin(options);
|
||||
await coreUtils.childPromisses.execFile(phpBinPath, ["--version"]);
|
||||
|
||||
const rel = await coreUtils.httpRequestGithub.getRelease({owner: "pmmp", repository: "PocketMine-MP", all: true});
|
||||
const relData = version.trim().toLowerCase() === "latest" ? rel.at(0) : rel.find((v) => v.tag_name === version.trim());
|
||||
if (!relData) throw new Error("Version not found");
|
||||
const phpAsset = relData.assets.find((a) => a.name.endsWith(".phar"))?.browser_download_url;
|
||||
if (!phpAsset) throw new Error("PHP asset not found");
|
||||
await coreUtils.httpRequestLarge.saveFile({url: phpAsset, filePath: path.join(serverPath.serverPath, "PocketMine-MP.phar")});
|
||||
|
||||
return {
|
||||
version: relData.tag_name,
|
||||
releaseDate: new Date(relData.published_at),
|
||||
release: (relData.prerelease ? "preview" : "stable") as "preview"|"stable",
|
||||
url: phpAsset,
|
||||
phpBin: phpBin.name,
|
||||
};
|
||||
} else if (options?.variant === "Powernukkit") {
|
||||
const versions = await coreUtils.httpRequest.getJSON<{version: string, mcpeVersion: string, date: string, url: string, variantType: "snapshot"|"stable"}[]>("https://mcpeversion-static.sirherobrine23.org/powernukkit/all.json");
|
||||
const versionData = version.trim().toLowerCase() === "latest" ? versions.at(-1) : versions.find((v) => v.version === version.trim() || v.mcpeVersion === version.trim());
|
||||
if (!versionData) throw new Error("Version not found");
|
||||
const url = versionData.url;
|
||||
if (!url) throw new Error("Platform not supported");
|
||||
await coreUtils.httpRequestLarge.saveFile({url, filePath: path.join(serverPath.serverPath, "server.jar")});
|
||||
return {
|
||||
version: versionData.version,
|
||||
mcpeVersion: versionData.mcpeVersion,
|
||||
variantType: versionData.variantType,
|
||||
releaseDate: new Date(versionData.date),
|
||||
url,
|
||||
};
|
||||
} else if (options?.variant === "Cloudbust") {
|
||||
throw new Error("Not implemented");
|
||||
} else {
|
||||
const versions = await coreUtils.httpRequest.getJSON<bedrockVersionJSON[]>("https://the-bds-maneger.github.io/BedrockFetch/all.json");
|
||||
const versionData = version.trim().toLowerCase() === "latest" ? versions.at(-1) : versions.find((v) => v.version === version.trim());
|
||||
if (!versionData) throw new Error("Version not found");
|
||||
let currentPlatform = process.platform;
|
||||
if (currentPlatform === "android") currentPlatform = "linux";
|
||||
const url = versionData.url[currentPlatform]?.[process.arch];
|
||||
if (!url) throw new Error("Platform not supported");
|
||||
await coreUtils.httpRequestLarge.extractZip({url, folderTarget: serverPath.serverPath});
|
||||
return {
|
||||
version: versionData.version,
|
||||
releaseDate: new Date(versionData.date),
|
||||
release: versionData.release ?? "stable",
|
||||
url: url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default startServer;
|
||||
export async function startServer(options?: bedrockRootOption) {
|
||||
// Bad fix options
|
||||
options = {variant: "oficial", ...options};
|
||||
const serverPath = await platformPathID("bedrock", options);
|
||||
// Server Object
|
||||
const serverExec: serverConfig = {
|
||||
exec: {
|
||||
exec: "java",
|
||||
args: []
|
||||
cwd: serverPath.serverPath,
|
||||
},
|
||||
actions: {}
|
||||
})
|
||||
};
|
||||
|
||||
if (options?.variant === "Pocketmine-PMMP") {
|
||||
serverExec.exec.exec = await getPHPBin();
|
||||
serverExec.exec.args = ["PocketMine-MP.phar", "--no-wizard"];
|
||||
serverExec.actions = {
|
||||
stopServer(child_process) {
|
||||
child_process.stdin.write("stop\n");
|
||||
},
|
||||
onStart(lineData, fnRegister) {
|
||||
if (!(lineData.includes("INFO") && lineData.includes("Done") && lineData.includes("help"))) return;
|
||||
const doneStart = new Date();
|
||||
fnRegister({
|
||||
serverAvaible: doneStart,
|
||||
bootUp: runStart.getTime() - doneStart.getTime()
|
||||
});
|
||||
},
|
||||
};
|
||||
} else if (options?.variant === "Powernukkit") {
|
||||
} else if (options?.variant === "Cloudbust") {
|
||||
} else {
|
||||
if (process.platform === "win32") serverExec.exec.exec = "bedrock_server.exe";
|
||||
else if (process.platform === "darwin") throw new Error("MacOS is not supported, run in Docker or Virtual Machine");
|
||||
else {
|
||||
serverExec.exec.exec = path.join(serverPath.serverPath, "bedrock_server");
|
||||
serverExec.exec.env = {
|
||||
LD_LIBRARY_PATH: serverPath.serverPath
|
||||
};
|
||||
}
|
||||
if ((["android", "linux"]).includes(process.platform) && process.arch !== "x64") {
|
||||
const exec = serverExec.exec.exec;
|
||||
serverExec.exec.exec = undefined;
|
||||
for (const command of hostArchEmulate) {
|
||||
if (await childPromisses.commandExists(command, true)) {
|
||||
serverExec.exec.args = [exec];
|
||||
serverExec.exec.exec = command;
|
||||
break;
|
||||
}
|
||||
if (!serverExec.exec.exec) throw new Error("No emulator found for this platform");
|
||||
}
|
||||
}
|
||||
|
||||
const startTest = /\[.*\]\s+Server\s+started\./;
|
||||
// Server actions
|
||||
serverExec.actions = {
|
||||
stopServer(child_process) {
|
||||
child_process.stdin.write("stop\n");
|
||||
},
|
||||
onStart(lineData, fnRegister) {if (startTest.test(lineData)) fnRegister({serverAvaible: new Date()});},
|
||||
playerActions(lineData, fnRegister) {
|
||||
const playerActionsV1 = /\[.*\]\s+Player\s+((dis|)connected):\s+(.*),\s+xuid:\s+([0-9]+)/;
|
||||
const newPlayerActions = /\[.*INFO\]\s+Player\s+(Spawned|connected|disconnected):\s+([\s\S\w]+)\s+(xuid:\s+([0-9]+))?/;
|
||||
const connectTime = new Date();
|
||||
if (!(newPlayerActions.test(lineData)||playerActionsV1.test(lineData))) return;
|
||||
let playerName: string, action: string, xuid: string;
|
||||
if (newPlayerActions.test(lineData)) {
|
||||
const [, actionV2,, playerNameV2,, xuidV2] = lineData.match(newPlayerActions);
|
||||
playerName = playerNameV2;
|
||||
action = actionV2;
|
||||
xuid = xuidV2;
|
||||
} else {
|
||||
const [, actionV1,, playerNameV1, xuidV1] = lineData.match(newPlayerActions);
|
||||
playerName = playerNameV1;
|
||||
action = actionV1;
|
||||
xuid = xuidV1;
|
||||
}
|
||||
fnRegister({
|
||||
player: playerName,
|
||||
action: action === "Spawned" ? "spawned" : action === "connected" ? "join" : "leave",
|
||||
actionDate: connectTime,
|
||||
sessionID: serverPath.id,
|
||||
more: {
|
||||
xuid
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const runStart = new Date();
|
||||
return createServerManeger(serverExec);
|
||||
}
|
@@ -5,6 +5,7 @@ import { extendFs } from "@sirherobrine23/coreutils";
|
||||
import crypto from "node:crypto";
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
import EventEmitter from "node:events";
|
||||
|
||||
export type pathOptions = {
|
||||
id?: "default"|string,
|
||||
@@ -63,36 +64,129 @@ export async function platformPathID(platform: "bedrock"|"java", options?: pathO
|
||||
};
|
||||
}
|
||||
|
||||
export type playerAction = ({action: "join"|"spawned"|"leave"}|{
|
||||
action: "kick"|"ban",
|
||||
reason?: string,
|
||||
by?: string
|
||||
}) & {
|
||||
player: string,
|
||||
actionDate: Date,
|
||||
sessionID: string
|
||||
more?: any,
|
||||
latestAction?: playerAction
|
||||
}
|
||||
|
||||
export type serverConfig = {
|
||||
exec: {
|
||||
exec: string,
|
||||
exec?: string,
|
||||
args?: string[],
|
||||
cwd?: string,
|
||||
env?: NodeJS.ProcessEnv & {[key: string]: string},
|
||||
},
|
||||
actions: {
|
||||
actions?: {
|
||||
stopServer?: (child_process: ChildProcess) => void,
|
||||
onStart?: (lineData: string, fnRegister: (data: {serverAvaible: Date, bootUp?: number}) => void) => void,
|
||||
playerActions?: (lineData: string, fnRegister: (data: {player: string, action: "join" | "leave"}) => void) => void,
|
||||
onStart?: (lineData: string, fnRegister: (data?: {serverAvaible?: Date, bootUp?: number}) => void) => void,
|
||||
playerActions?: (lineData: string, fnRegister: (data: playerAction) => void) => void,
|
||||
}
|
||||
};
|
||||
|
||||
export async function serverManeger(serverOptions: serverConfig) {
|
||||
declare class serverManeger extends EventEmitter {
|
||||
on(event: "error", fn: (lineLog: any) => void): this;
|
||||
once(event: "error", fn: (lineLog: any) => void): this;
|
||||
emit(event: "error", data: any): boolean;
|
||||
|
||||
on(event: "log", fn: (lineLog: string) => void): this;
|
||||
once(event: "log", fn: (lineLog: string) => void): this;
|
||||
emit(event: "log", data: string): boolean;
|
||||
|
||||
on(event: "rawLog", fn: (raw: any) => void): this;
|
||||
once(event: "rawLog", fn: (raw: any) => void): this;
|
||||
emit(event: "rawLog", data: any): boolean;
|
||||
|
||||
// Player actions
|
||||
on(event: "playerAction", fn: (playerAction: playerAction) => void): this;
|
||||
once(event: "playerAction", fn: (playerAction: playerAction) => void): this;
|
||||
emit(event: "playerAction", data: playerAction): boolean;
|
||||
|
||||
// Server started
|
||||
on(event: "serverStarted", fn: (data: {serverAvaible: Date, bootUp: number}) => void): this;
|
||||
once(event: "serverStarted", fn: (data: {serverAvaible: Date, bootUp: number}) => void): this;
|
||||
emit(event: "serverStarted", data: {serverAvaible: Date, bootUp: number}): boolean;
|
||||
}
|
||||
|
||||
export async function createServerManeger(serverOptions: serverConfig): Promise<serverManeger> {
|
||||
const serverExec = child_process.execFile(serverOptions.exec.exec, serverOptions.exec.args ?? [], {
|
||||
cwd: serverOptions.exec.cwd,
|
||||
windowsHide: true,
|
||||
maxBuffer: Infinity,
|
||||
env: {
|
||||
...process.env,
|
||||
...serverOptions.exec.env
|
||||
},
|
||||
windowsHide: true,
|
||||
maxBuffer: Infinity
|
||||
});
|
||||
const stdoutReadline = readline({input: serverExec.stdout});
|
||||
const stderrReadline = readline({input: serverExec.stderr});
|
||||
const playerActions: playerAction[] = [];
|
||||
const internalEvent = new class serverManeger extends EventEmitter {
|
||||
stopServer() {
|
||||
const stopServer = serverOptions.actions?.stopServer ?? ((child_process) => child_process.kill("SIGKILL"));
|
||||
stopServer(serverExec);
|
||||
}
|
||||
|
||||
return {
|
||||
serverExec,
|
||||
stdoutReadline,
|
||||
stderrReadline
|
||||
getPlayers() {
|
||||
return playerActions ?? [];
|
||||
}
|
||||
};
|
||||
serverExec.on("error", internalEvent.emit.bind(internalEvent, "error"));
|
||||
const stdoutReadline = readline({input: serverExec.stdout});
|
||||
stdoutReadline.on("line", (line) => internalEvent.emit("log", line));
|
||||
stdoutReadline.on("error", internalEvent.emit.bind(internalEvent, "error"));
|
||||
serverExec.stdout.on("data", (data) => internalEvent.emit("rawLog", data));
|
||||
|
||||
const stderrReadline = readline({input: serverExec.stderr});
|
||||
stderrReadline.on("line", (line) => internalEvent.emit("log", line));
|
||||
stderrReadline.on("error", internalEvent.emit.bind(internalEvent, "error"));
|
||||
serverExec.stderr.on("data", (data) => internalEvent.emit("rawLog", data));
|
||||
|
||||
// Server start
|
||||
if (serverOptions.actions?.onStart) {
|
||||
const serverStartFN = serverOptions.actions.onStart;
|
||||
let lock = false;
|
||||
const started = new Date();
|
||||
function register(data?: {serverAvaible?: Date, bootUp?: number}) {
|
||||
if (lock) return;
|
||||
const eventData = {
|
||||
serverAvaible: data?.serverAvaible ?? new Date(),
|
||||
bootUp: data?.bootUp ?? new Date().getTime() - started.getTime()
|
||||
};
|
||||
internalEvent.emit("serverStarted", eventData);
|
||||
lock = true;
|
||||
stderrReadline.removeListener("line", register);
|
||||
stdoutReadline.removeListener("line", register);
|
||||
// emit and remove new listener for serverStarted
|
||||
internalEvent.removeAllListeners("serverStarted");
|
||||
internalEvent.prependListener("serverStarted", () => {
|
||||
internalEvent.emit("serverStarted", eventData);
|
||||
internalEvent.removeAllListeners("serverStarted");
|
||||
});
|
||||
}
|
||||
stdoutReadline.on("line", (line) => serverStartFN(line, register));
|
||||
stderrReadline.on("line", (line) => serverStartFN(line, register));
|
||||
}
|
||||
|
||||
// Player actions
|
||||
if (serverOptions.actions?.playerActions) {
|
||||
const playerFn = serverOptions.actions.playerActions;
|
||||
const registerData = (data: playerAction) => {
|
||||
const player = playerActions.find((player) => player.player === data.player);
|
||||
if (!player) playerActions.push(data);
|
||||
else {
|
||||
data.latestAction = player;
|
||||
playerActions[playerActions.indexOf(player)] = data;
|
||||
}
|
||||
internalEvent.emit("playerAction", data);
|
||||
}
|
||||
stdoutReadline.on("line", (line) => playerFn(line, registerData));
|
||||
stderrReadline.on("line", (line) => playerFn(line, registerData));
|
||||
}
|
||||
|
||||
return internalEvent;
|
||||
}
|
Reference in New Issue
Block a user