Reewrite project #430
35
.github/workflows/testProject.yml
vendored
35
.github/workflows/testProject.yml
vendored
@ -1,35 +0,0 @@
|
||||
name: Test Bds Core
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Test (Node version: ${{ matrix.node_version }})"
|
||||
strategy:
|
||||
matrix:
|
||||
node_version:
|
||||
- "18"
|
||||
- "17"
|
||||
- "16"
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- name: Setup Node.js (Github Packages)
|
||||
uses: actions/setup-node@v3.4.1
|
||||
with:
|
||||
node-version: "${{ matrix.node_version }}.x"
|
||||
|
||||
- name: Install latest java
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
|
||||
- name: Install node depencies
|
||||
run: npm ci
|
||||
|
||||
- name: Test
|
||||
run: npm run test -- --show-log
|
@ -1,42 +0,0 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import Git from "./git";
|
||||
import { worldStorageRoot, serverRoot } from "./pathControl";
|
||||
import type { Platform } from "./globalType";
|
||||
export const worldGit = new Git(worldStorageRoot, {remoteUrl: process.env.BDS_GIT_WORLDBACKUP});
|
||||
|
||||
setInterval(async () => {
|
||||
if ((await worldGit.status()).length === 0) return;
|
||||
console.log("Committing world backup");
|
||||
await worldGit.addSync(".");
|
||||
await worldGit.commitSync("Automatic backup");
|
||||
await worldGit.pushSync().catch(err => console.error(err));
|
||||
return;
|
||||
}, 1000*60*60*2);
|
||||
|
||||
export async function copyWorld(serverPlatform: Platform, worldName: string, worldPath: string) {
|
||||
const copyPath = path.join(serverRoot, serverPlatform, worldPath);
|
||||
const worldPathFolder = path.join(worldGit.repoRoot, serverPlatform);
|
||||
if (await fs.lstat(worldPathFolder).then(stats => stats.isDirectory()).catch(() => false)) await fs.mkdir(path.join(worldGit.repoRoot, serverPlatform));
|
||||
if (await fs.lstat(path.join(worldPathFolder, worldName)).then(stats => stats.isDirectory() && !stats.isSymbolicLink()).catch(() => false)) {
|
||||
await fs.rmdir(path.join(worldPathFolder, worldName), {recursive: true});
|
||||
await fs.cp(copyPath, path.join(worldPathFolder, worldName), {recursive: true, force: true});
|
||||
}
|
||||
await worldGit.addSync(path.join(worldPathFolder, worldName));
|
||||
const currentDate = new Date();
|
||||
await worldGit.commitSync(`${worldName} backup - ${currentDate.getDate()}.${currentDate.getMonth()}.${currentDate.getFullYear()}`, [`${worldName} backup - ${currentDate.toLocaleDateString()}`]);
|
||||
await worldGit.pushSync().catch(err => console.error(err));
|
||||
}
|
||||
|
||||
export async function restoreWorld(serverPlatform: Platform, worldName: string, worldPath: string) {
|
||||
// check if world exists in repo
|
||||
const worldPathFolder = path.join(worldGit.repoRoot, serverPlatform);
|
||||
if (!await fs.lstat(path.join(worldPathFolder, worldName)).then(stats => stats.isDirectory() && !stats.isSymbolicLink()).catch(() => false)) throw new Error("World folder does not exist");
|
||||
// check if world is not link to worlds
|
||||
if (await fs.lstat(path.join(worldPathFolder, worldName)).then(stats => stats.isSymbolicLink()).catch(() => false)) throw new Error("World folder is a link, do not necessary restore");
|
||||
// rename world folder
|
||||
if (await fs.lstat(worldPath+"_backup").then(stats => stats.isDirectory()).catch(() => false)) await fs.rmdir(worldPath, {recursive: true});
|
||||
await fs.rename(worldPath, worldPath+"_backup");
|
||||
// copy world to world path
|
||||
await fs.cp(path.join(worldPathFolder, worldName), worldPath, {recursive: true, force: true});
|
||||
}
|
1
src/bedrock.ts
Normal file
1
src/bedrock.ts
Normal file
@ -0,0 +1 @@
|
||||
import {} from "./childPromisses";
|
@ -1,586 +0,0 @@
|
||||
/**
|
||||
* Original file url: https://github.com/chegele/BDSAddonInstaller/blob/6e9cf7334022941f8007c28470eb1e047dfe0e90/index.js
|
||||
* License: No license provided.
|
||||
* Github Repo: https://github.com/chegele/BDSAddonInstaller
|
||||
*
|
||||
* Patch by Sirherorine23 (Matheus Sampaio Queirora) <srherobrine20@gmail.com>
|
||||
*/
|
||||
import path from "node:path";
|
||||
import admZip from "adm-zip";
|
||||
import fs from "node:fs";
|
||||
import { serverRoot } from "../pathControl";
|
||||
// import stripJsonComments from "strip-json-comments";
|
||||
|
||||
const stripJsonComments = (data: string) => data.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => g ? "" : m);
|
||||
|
||||
function ensureFileSync(pathFile: string){
|
||||
if (!fs.existsSync(pathFile)){
|
||||
if (!fs.existsSync(path.parse(pathFile).dir)) fs.mkdirSync(path.parse(pathFile).dir, {recursive: true});
|
||||
fs.writeFileSync(pathFile, "");
|
||||
}
|
||||
}
|
||||
|
||||
// Below variables are updated by the constructor.
|
||||
// All paths will be converted to full paths by including serverPath at the beginning.
|
||||
let serverPath = null;
|
||||
let worldName = null;
|
||||
|
||||
const providedServerPath = path.join(serverRoot, "bedrock");
|
||||
const addonPath = path.resolve(providedServerPath, "../BDS-Addons/");
|
||||
if (!(fs.existsSync(addonPath))) fs.mkdirSync(addonPath, {recursive: true});
|
||||
|
||||
let serverPacksJsonPath = "valid_known_packs.json";
|
||||
let serverPacksJSON = null;
|
||||
let serverResourcesDir = "resource_packs/";
|
||||
let serverBehaviorsDir = "behavior_packs/";
|
||||
|
||||
let worldResourcesJsonPath = "worlds/<worldname>/world_resource_packs.json";
|
||||
let worldResourcesJSON = null;
|
||||
let worldBehaviorsJsonPath = "worlds/<worldname>/world_behavior_packs.json";
|
||||
let worldBehaviorsJSON = null;
|
||||
let worldResourcesDir = "worlds/<worldname>/resource_packs/";
|
||||
let worldBehaviorsDir = "worlds/<worldname>/behavior_packs/";
|
||||
|
||||
// Below variables updated by mapInstalledPacks function.
|
||||
// Updated to contain installed pack info {name, uuid, version, location}
|
||||
let installedServerResources = new Map();
|
||||
let installedServerBehaviors = new Map();
|
||||
let installedWorldResources = new Map();
|
||||
let installedWorldBehaviors = new Map();
|
||||
|
||||
/**
|
||||
* Prepares to install addons for the provided Bedrock Dedicated Server.
|
||||
*/
|
||||
export function addonInstaller() {
|
||||
// Update all module paths from relative to full paths.
|
||||
serverPath = providedServerPath;
|
||||
// addonPath = path.join(providedServerPath, addonPath);
|
||||
worldName = readWorldName();
|
||||
worldResourcesJsonPath = path.join(serverPath, worldResourcesJsonPath.replace("<worldname>", worldName));
|
||||
worldBehaviorsJsonPath = path.join(serverPath, worldBehaviorsJsonPath.replace("<worldname>", worldName));
|
||||
worldResourcesDir = path.join(serverPath, worldResourcesDir.replace("<worldname>", worldName));
|
||||
worldBehaviorsDir = path.join(serverPath, worldBehaviorsDir.replace("<worldname>", worldName));
|
||||
serverPacksJsonPath = path.join(serverPath, serverPacksJsonPath);
|
||||
serverResourcesDir = path.join(serverPath, serverResourcesDir);
|
||||
serverBehaviorsDir = path.join(serverPath, serverBehaviorsDir);
|
||||
|
||||
// Create JSON files if they do not exists
|
||||
ensureFileSync(serverPacksJsonPath);
|
||||
ensureFileSync(worldResourcesJsonPath);
|
||||
ensureFileSync(worldBehaviorsJsonPath);
|
||||
|
||||
// Read installed packs from JSON files & attempt to parse content.
|
||||
let serverPackContents = fs.readFileSync(serverPacksJsonPath, "utf8");
|
||||
let worldResourceContents = fs.readFileSync(worldResourcesJsonPath, "utf8");
|
||||
let worldBehaviorContents = fs.readFileSync(worldBehaviorsJsonPath, "utf8");
|
||||
// If there is an error parsing JSON assume no packs installed and use empty array.
|
||||
try { serverPacksJSON = JSON.parse(serverPackContents) } catch(err) { serverPacksJSON = [] };
|
||||
try { worldResourcesJSON = JSON.parse(worldResourceContents) } catch(err) { worldResourcesJSON = [] };
|
||||
try { worldBehaviorsJSON = JSON.parse(worldBehaviorContents) } catch(err) { worldBehaviorsJSON = [] };
|
||||
// If unexpected results from parsing JSON assume no packs installed and use empty array.
|
||||
if (!Array.isArray(serverPacksJSON)) serverPacksJSON = [];
|
||||
if (!Array.isArray(worldResourcesJSON)) worldResourcesJSON = [];
|
||||
if (!Array.isArray(worldBehaviorsJSON)) worldBehaviorsJSON = [];
|
||||
|
||||
// Map installed packs from install directories
|
||||
installedServerResources = mapInstalledPacks(serverResourcesDir);
|
||||
installedServerBehaviors = mapInstalledPacks(serverBehaviorsDir);
|
||||
installedWorldResources = mapInstalledPacks(worldResourcesDir);
|
||||
installedWorldBehaviors = mapInstalledPacks(worldBehaviorsDir);
|
||||
|
||||
/**
|
||||
* Installs the provide addon/pack to the BDS server and the active world.
|
||||
* @param {String} packPath - The full path to the mcpack or mcaddon file.
|
||||
*/
|
||||
async function installAddon(packPath: string) {
|
||||
|
||||
// Validate provided pack (pack exists & is the correct file type)
|
||||
if (!fs.existsSync(packPath)) throw new Error("Unable to install pack. The provided path does not exist. " + packPath);
|
||||
if (!packPath.endsWith(".mcpack") && !packPath.endsWith(".mcaddon")) throw new Error("Unable to install pack. The provided file is not an addon or pack. " + packPath);
|
||||
if (packPath.endsWith(".mcaddon")) {
|
||||
// If the provided pack is an addon extract packs and execute this function again for each one.
|
||||
let packs = await extractAddonPacks(packPath);
|
||||
for (const pack of packs) await this.installAddon(pack);
|
||||
return;
|
||||
}
|
||||
|
||||
// Gather pack details from the manifest.json file
|
||||
let manifest = await extractPackManifest(packPath);
|
||||
// let name = manifest.header.name.replace(/\W/g, "");
|
||||
let uuid = manifest.header.uuid;
|
||||
let version = manifest.header.version;
|
||||
if (!version) version = manifest.header.modules[0].version;
|
||||
let type: string;
|
||||
if (manifest.modules) {
|
||||
type = manifest.modules[0].type.toLowerCase();
|
||||
} else if (manifest.header.modules) {
|
||||
type = manifest.header.modules[0].type.toLowerCase();
|
||||
}else {
|
||||
throw new Error("Unable to install pack. Unknown pack manifest format.\n" + packPath);
|
||||
}
|
||||
|
||||
// console.log("BDSAddonInstaller - Installing " + name + "...");
|
||||
|
||||
// Check if already installed
|
||||
let installedWorldPack: any;
|
||||
let installedServerPack: any = null;
|
||||
if (type == "resources") {
|
||||
installedWorldPack = installedWorldResources.get(uuid);
|
||||
installedServerPack = installedServerResources.get(uuid);
|
||||
}else if (type == "data") {
|
||||
installedWorldPack = installedWorldBehaviors.get(uuid);
|
||||
installedServerPack = installedServerBehaviors.get(uuid)
|
||||
}
|
||||
|
||||
// Check if current installed packs are up to date
|
||||
if (installedWorldPack || installedServerPack) {
|
||||
let upToDate = true;
|
||||
if (installedWorldPack && installedWorldPack.version.toString() != version.toString()) upToDate = false;
|
||||
if (installedServerPack && installedServerPack.version.toString() != version.toString()) upToDate = false;
|
||||
if (upToDate) {
|
||||
// console.log(`BDSAddonInstaller - The ${name} pack is already installed and up to date.`);
|
||||
return;
|
||||
}else{
|
||||
// uninstall pack if not up to date
|
||||
// console.log("BDSAddonInstaller - Uninstalling old version of pack");
|
||||
if (installedServerPack) await uninstallServerPack(uuid, installedServerPack.location);
|
||||
if (installedWorldPack && type == "resources") await uninstallWorldResource(uuid, installedWorldPack.location);
|
||||
if (installedWorldPack && type == "data") await uninstallWorldBehavior(uuid, installedWorldPack.location);
|
||||
}
|
||||
}
|
||||
|
||||
await installPack(packPath, manifest);
|
||||
// console.log("BDSAddonInstaller - Successfully installed the " + name + " pack.");
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs all of the addons & packs found within the BDS-Addons directory.
|
||||
* NOTE: Running this function with remove packs is only recommended if facing issues.
|
||||
*/
|
||||
async function installAllAddons(removeOldPacks: boolean) {
|
||||
// If chosen, uninstall all world packs.
|
||||
if (removeOldPacks) await uninstallAllWorldPacks();
|
||||
|
||||
// Read all packs & addons from BDS-Addon directory.
|
||||
let packs = fs.readdirSync(addonPath);
|
||||
|
||||
// Get the full path of each addon/pack and install it.
|
||||
for (let pack of packs) {
|
||||
try {
|
||||
let location = path.join(addonPath, pack);
|
||||
await this.installAddon(location);
|
||||
}catch(err) {
|
||||
// console.error("BDSAddonInstaller - " + err);
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
installAddon,
|
||||
installAllAddons
|
||||
};
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
// BDSAddonInstaller - Install & Uninstall functions
|
||||
|
||||
/**
|
||||
* Installs the provided pack to the world and Bedrock Dedicated Server.
|
||||
* @param packPath - The path to the pack to be installed.
|
||||
* @param manifest - The pre-parsed manifest information for the pack.
|
||||
*/
|
||||
async function installPack(packPath: string, manifest: {[d: string]: any}) {
|
||||
// Extract manifest information
|
||||
let name = manifest.header.name.replace(/\W/g, "");
|
||||
let uuid = manifest.header.uuid;
|
||||
let version = manifest.header.version;
|
||||
if (!version) version = manifest.header.modules[0].version;
|
||||
let type: string;
|
||||
if (manifest.modules) {
|
||||
type = manifest.modules[0].type.toLowerCase();
|
||||
} else if (manifest.header.modules) {
|
||||
type = manifest.header.modules[0].type.toLowerCase();
|
||||
}else {
|
||||
throw new Error("Unable to install pack. Unknown pack manifest format.\n" + packPath);
|
||||
}
|
||||
|
||||
// Create placeholder variables for pack installation paths.
|
||||
let installServerPath: string
|
||||
let installWorldPath: string
|
||||
let WorldPacksJSON: any
|
||||
let WorldPacksPath: string
|
||||
let rawPath: string|null = null;
|
||||
|
||||
// Update variables based on the pack type.
|
||||
if (type == "data") {
|
||||
installServerPath = path.join(serverBehaviorsDir, name);
|
||||
installWorldPath = path.join(worldBehaviorsDir, name);
|
||||
WorldPacksJSON = worldBehaviorsJSON;
|
||||
WorldPacksPath = worldBehaviorsJsonPath;
|
||||
rawPath = "behavior_packs/" + name;
|
||||
}else if (type == "resources") {
|
||||
installServerPath = path.join(serverResourcesDir, name);
|
||||
installWorldPath = path.join(worldResourcesDir, name);
|
||||
WorldPacksJSON = worldResourcesJSON;
|
||||
WorldPacksPath = worldResourcesJsonPath;
|
||||
rawPath = "resource_packs/" + name;
|
||||
}else {
|
||||
throw new Error("Unknown pack type, " + type);
|
||||
}
|
||||
|
||||
// Install pack to the world.
|
||||
let worldPackInfo = {"pack_id": uuid, "version": version}
|
||||
WorldPacksJSON.unshift(worldPackInfo);
|
||||
await promiseExtract(packPath, installWorldPath);
|
||||
fs.writeFileSync(WorldPacksPath, JSON.stringify(WorldPacksJSON, undefined, 2));
|
||||
|
||||
// Install pack to the server.
|
||||
version = `${version[0]}.${version[1]}.${version[2]}`;
|
||||
let serverPackInfo = {"file_system": "RawPath", "node:path": rawPath, "uuid": uuid, "version": version};
|
||||
serverPacksJSON.splice(1, 0, serverPackInfo);
|
||||
await promiseExtract(packPath, installServerPath);
|
||||
fs.writeFileSync(serverPacksJsonPath, JSON.stringify(serverPacksJSON, undefined, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall all resource and behavior packs from the Minecraft world.
|
||||
* If the server also has the pick it will also be uninstalled.
|
||||
* NOTE: Vanilla packs can"t be safely removed from the server packs & there is no way to differentiate vanilla and added packs.
|
||||
* NOTE: This is why only packs found installed to the world will be removed from the server.
|
||||
*/
|
||||
async function uninstallAllWorldPacks() {
|
||||
// console.log("BDSAddonInstaller - Uninstalling all packs found saved to world.");
|
||||
|
||||
// Uninstall all cached world resource packs.
|
||||
for (let pack of installedWorldResources.values()) {
|
||||
await uninstallWorldResource(pack.uuid, pack.location);
|
||||
let serverPack = installedServerResources.get(pack.uuid);
|
||||
if (serverPack) await uninstallServerPack(pack.uuid, serverPack.location);
|
||||
}
|
||||
|
||||
// Uninstall all cached world behavior packs.
|
||||
for (let pack of installedWorldBehaviors.values()) {
|
||||
await uninstallWorldBehavior(pack.uuid, pack.location);
|
||||
let serverPack = installedServerBehaviors.get(pack.uuid);
|
||||
if (serverPack) await uninstallServerPack(pack.uuid, serverPack.location);
|
||||
}
|
||||
|
||||
// All packs are cached by the constructor.
|
||||
// Reload world packs after uninstall.
|
||||
installedServerResources = mapInstalledPacks(serverResourcesDir);
|
||||
installedServerBehaviors = mapInstalledPacks(serverBehaviorsDir);
|
||||
installedWorldResources = mapInstalledPacks(worldResourcesDir);
|
||||
installedWorldBehaviors = mapInstalledPacks(worldBehaviorsDir);
|
||||
}
|
||||
|
||||
// TODO: uninstallWorldResource, uninstallWorldBehavior, and uninstallServerPack share the same logic.
|
||||
// These functions can be merged into one function using an additional argument for pack type.
|
||||
|
||||
/**
|
||||
* Uninstalls the pack from the world_resource_packs.json by uuid & deletes the provided pack path.
|
||||
* @param uuid - The id of the pack to remove from the world_resource_packs.json file.
|
||||
* @param location - The path to the root directory of the installed pack to be deleted.
|
||||
* WARNING: No validation is done to confirm that the provided path is a pack.
|
||||
*/
|
||||
async function uninstallWorldResource(uuid: string, location: string) {
|
||||
// Locate the pack in the manifest data.
|
||||
let packIndex = findIndexOf(worldResourcesJSON, "pack_id", uuid);
|
||||
|
||||
// Remove the pack data and update the json file.
|
||||
if (packIndex != -1) {
|
||||
worldResourcesJSON.splice(packIndex, 1);
|
||||
fs.writeFileSync(worldResourcesJsonPath, JSON.stringify(worldResourcesJSON, undefined, 2));
|
||||
// console.log(`BDSAddonInstaller - Removed ${uuid} from world resource packs JSON.`);
|
||||
}
|
||||
|
||||
// Delete the provided pack path.
|
||||
if (fs.existsSync(location)) {
|
||||
await fs.promises.rm(location, {recursive: true});
|
||||
// console.log(`BDSAddonInstaller - Removed ${location}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstalls the pack from the world_behavior_packs.json by uuid & deletes the provided pack path.
|
||||
* @param uuid - The id of the pack to remove from the world_behavior_packs.json file.
|
||||
* @param location - The path to the root directory of the installed pack to be deleted.
|
||||
* WARNING: No validation is done to confirm that the provided path is a pack.
|
||||
*/
|
||||
async function uninstallWorldBehavior(uuid: string, location: string) {
|
||||
// Locate the pack in the manifest data.
|
||||
let packIndex = findIndexOf(worldBehaviorsJSON, "pack_id", uuid);
|
||||
|
||||
// Remove the pack data and update the json file.
|
||||
if (packIndex != -1) {
|
||||
worldBehaviorsJSON.splice(packIndex, 1);
|
||||
fs.writeFileSync(worldBehaviorsJsonPath, JSON.stringify(worldBehaviorsJSON, undefined, 2));
|
||||
// console.log(`BDSAddonInstaller - Removed ${uuid} from world behavior packs JSON.`);
|
||||
}
|
||||
|
||||
// Delete the provided pack path.
|
||||
if (fs.existsSync(location)) {
|
||||
fs.promises.rm(location);
|
||||
// console.log(`BDSAddonInstaller - Removed ${location}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstalls the pack from the valid_known_packs.json by uuid & deletes the provided pack path.
|
||||
* @param uuid - The id of the pack to remove from the valid_known_packs.json file.
|
||||
* @param location - The path to the root directory of the installed pack to be deleted.
|
||||
* WARNING: No validation is done to confirm that the provided path is a pack.
|
||||
*/
|
||||
async function uninstallServerPack (uuid: string, location: string) {
|
||||
// Locate the pack in the manifest data.
|
||||
let packIndex = findIndexOf(serverPacksJSON, "uuid", uuid);
|
||||
|
||||
// Remove the pack data and update the json file.
|
||||
if (packIndex != -1) {
|
||||
serverPacksJSON.splice(packIndex, 1);
|
||||
fs.writeFileSync(serverPacksJsonPath, JSON.stringify(serverPacksJSON, undefined, 2));
|
||||
// console.log(`BDSAddonInstaller - Removed ${uuid} from server packs JSON.`);
|
||||
}
|
||||
|
||||
// Delete the provided pack path.
|
||||
if (fs.existsSync(location)) {
|
||||
fs.promises.rm(location);
|
||||
// console.log(`BDSAddonInstaller - Removed ${location}`);
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
// BDSAddonInstaller misc functions
|
||||
|
||||
/**
|
||||
* Extracts bundled packs from the provided addon file.
|
||||
* This will only need to be ran once on an addon as it will convert the addon to multiple .mcpack files.
|
||||
* @param addonPath - The path of the addon file to extract packs from.
|
||||
*/
|
||||
async function extractAddonPacks(addonPath: string) {
|
||||
// Validate the provided path is to an addon.
|
||||
if (!fs.existsSync(addonPath)) throw new Error("Unable to extract packs from addon. Invalid file path provided: " + addonPath);
|
||||
if (!addonPath.endsWith('.mcaddon')) throw new Error('Unable to extract packs from addon. The provided file is not an addon. ' + addonPath);
|
||||
// console.log("BDSAddonInstaller - Extracting packs from " + addonPath);
|
||||
|
||||
// Extract file path and name info for saving the extracted packs.
|
||||
let addonName = path.basename(addonPath).replace(".mcaddon", "");
|
||||
let dirPath = path.dirname(addonPath);
|
||||
|
||||
// Create a temp location and extract the addon contents to it.
|
||||
let tempLocation = path.join(dirPath, "tmp/", addonName + "/");
|
||||
await promiseExtract(addonPath, tempLocation);
|
||||
let packs = fs.readdirSync(tempLocation);
|
||||
let results = [];
|
||||
|
||||
// Move addon packs from temporary location to BDS-Addon directory.
|
||||
for (let pack of packs) {
|
||||
// console.log(`BDSAddonInstaller - Extracting ${pack} from ${addonName}.`);
|
||||
|
||||
// If the mcpack is already packaged, move the file.
|
||||
if (pack.endsWith(".mcpack")) {
|
||||
let packName = addonName + "_" + pack;
|
||||
let packFile = path.join(tempLocation, pack);
|
||||
let packDestination = path.join(dirPath, packName);
|
||||
await fs.promises.rename(packFile, packDestination);
|
||||
results.push(packDestination);
|
||||
// console.log("BDSAddonInstaller - Extracted " + packDestination);
|
||||
}else {
|
||||
// The pack still needs to be zipped and then moved.
|
||||
let packName = addonName + "_" + pack + ".mcpack";
|
||||
let packFolder = path.join(tempLocation, pack);
|
||||
let packDestination = path.join(dirPath, packName);
|
||||
await promiseZip(packFolder, packDestination);
|
||||
results.push(packDestination);
|
||||
// console.log("BDSAddonInstaller - Extracted " + packDestination);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove temporary files and old addon.
|
||||
await fs.promises.rm(path.join(dirPath, "tmp/"), {recursive: true});
|
||||
await fs.promises.unlink(addonPath);
|
||||
|
||||
// Return an array of paths to the extracted packs.
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the manifest data as an object from the provided .mcpack file.
|
||||
* @param packPath - The path to the pack to extract the manifest from.
|
||||
* @returns The parsed manifest.json file.
|
||||
*/
|
||||
function extractPackManifest(packPath: string): {[key: string]: any} {
|
||||
// Validate the provided pack (path exists and file is correct type)
|
||||
if (!fs.existsSync(packPath)) throw new Error("Unable to extract manifest file. Invalid file path provided: " + packPath);
|
||||
if (!packPath.endsWith(".mcpack")) throw new Error("Unable to extract manifest file. The provided file is not a pack. " + packPath);
|
||||
// console.log("BDSAddonInstaller - Reading manifest data from " + packPath);
|
||||
|
||||
// Locate the manifest file in the zipped pack.
|
||||
let archive = new admZip(packPath);
|
||||
let manifest = archive.getEntries().filter(entry => entry.entryName.endsWith("manifest.json") || entry.entryName.endsWith("pack_manifest.json"));
|
||||
if (!manifest[0]) throw new Error("Unable to extract manifest file. It does not exist in this pack. " + packPath);
|
||||
|
||||
// Read the manifest and return the parsed JSON.
|
||||
return JSON.parse(stripJsonComments(archive.readAsText(manifest[0].entryName)));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reads the world name from a BDS server.properties file.
|
||||
* @returns The value found for level-name from server.properties.
|
||||
* NOTE: This function is Synchronous for use in the constructor without need for a callback.
|
||||
*/
|
||||
function readWorldName(): string {
|
||||
let propertyFile = path.join(serverPath, "server.properties");
|
||||
// console.log("BDSAddonInstaller - Reading world name from " + propertyFile);
|
||||
if (!fs.existsSync(propertyFile)) throw new Error("Unable to locate server properties @ " + propertyFile);
|
||||
let properties = fs.readFileSync(propertyFile);
|
||||
let levelName = properties.toString().match(/level-name=.*/);
|
||||
if (!levelName) throw new Error("Unable to retrieve level-name from server properties.");
|
||||
return levelName.toString().replace("level-name=", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects manifest information from all installed packs in provided location.
|
||||
* @param directory - The path to the directory containing extracted/installed packs.
|
||||
* @returns A collection of manifest information with the uuid as the key.
|
||||
*
|
||||
* Bug Note:
|
||||
* Some of the vanilla packs are installed multiple times using the same uuid but different versions.
|
||||
* This causes the map to only capture the last read pack with that uuid.
|
||||
* This bug should not impact the installer, as there wont be a need to install / update vanilla packs.
|
||||
*
|
||||
* NOTE: This function is Synchronous for use in the constructor without need for a callback.
|
||||
*/
|
||||
|
||||
function mapInstalledPacks(directory: string): Map<{}, any> {
|
||||
// The provided directory may not exist if the world has no packs installed.
|
||||
// Create the results Map & return empty if the directory does not exist.
|
||||
let results = new Map();
|
||||
if (!fs.existsSync(directory)) return results;
|
||||
|
||||
// Extract manifest & path information for each installed pack
|
||||
let subdirectories = fs.readdirSync(directory);
|
||||
subdirectories.forEach(subdirectory => {
|
||||
let location = path.join(directory, subdirectory);
|
||||
// console.log("BDSAddonInstaller - Reading manifest data from " + location);
|
||||
|
||||
// Locate the directory containing the pack manifest.
|
||||
let manifestLocation = findFilesSync(["manifest.json", "pack_manifest.json"], location);
|
||||
if (!manifestLocation) {
|
||||
// console.error(manifestLocation);
|
||||
// console.warn("BDSAddonInstaller - Unable to locate manifest file of installed pack.");
|
||||
// console.warn("BDSAddonInstaller - Installed location: " + location);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if pack is using a manifest.json or pack.manifest.json
|
||||
let filePath = path.join(manifestLocation, "manifest.json");
|
||||
if (!fs.existsSync(filePath)) filePath = path.join(manifestLocation, "pack_manifest.json");
|
||||
let file = fs.readFileSync(filePath, "utf8");
|
||||
|
||||
// Some vanilla packs have comments in them, this is not valid JSON and needs to be removed.
|
||||
file = stripJsonComments(file.toString());
|
||||
let manifest = JSON.parse(file);
|
||||
|
||||
// Collect and map the manifest information
|
||||
let uuid = manifest.header.uuid;
|
||||
let name = manifest.header.name;
|
||||
let version = manifest.header.version;
|
||||
if (!version) version = manifest.header.modules[0].version;
|
||||
results.set(uuid, {name, uuid, version, location});
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Misc helper functions
|
||||
|
||||
/**
|
||||
* Finds the first index of a key value pair from an array of objects.
|
||||
* @param objectArray - An array of objects to search.
|
||||
* @param key - The key to match the value against.
|
||||
* @param value - The value to find the index of.
|
||||
* @returns - The index of the key value pair or -1.
|
||||
*/
|
||||
function findIndexOf(objectArray: Array<{[d: string]: any}>, key: string, value: any): number {
|
||||
for (let index = 0; index < objectArray.length; index++) {
|
||||
if (objectArray[index][key] == value) return index;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all of the contents from a provided .zip archive.
|
||||
* @param file - The file to extract the contents from.
|
||||
* @param destination - The directory to unzip the contents into.
|
||||
*/
|
||||
function promiseExtract(file: string, destination: string) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
let archive = new admZip(file);
|
||||
archive.extractAllToAsync(destination, true, true, err => {
|
||||
if (err) return reject(err);
|
||||
resolve("");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Compresses contents of the provided folder using ADM Zip.
|
||||
* @param folder - The folder containing folder containing the files to compress.
|
||||
* @param destinationFile - The file to save the archive as.
|
||||
*/
|
||||
function promiseZip(folder: string, destinationFile: string) {
|
||||
return new Promise(async function(resolve, reject) {
|
||||
let archive = new admZip();
|
||||
let contents = await fs.promises.readdir(folder);
|
||||
for (let file of contents) {
|
||||
let filePath = path.join(folder, file);
|
||||
let stat = await fs.promises.stat(filePath);
|
||||
stat.isFile() ? archive.addLocalFile(filePath) : archive.addLocalFolder(filePath, file);
|
||||
}
|
||||
archive.writeZip(destinationFile, err => {
|
||||
if (err) return reject(err);
|
||||
resolve("");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to locate the subdirectory containing one of the provided file names.
|
||||
* @param filenames - The name of files to search for.
|
||||
* @param directory - The directory to search in.
|
||||
* @returns The path to the first folder containing one of the files or null.
|
||||
*/
|
||||
function findFilesSync(filenames: Array<string>, directory: string): string {
|
||||
|
||||
// Get the contents of the directory and see if it includes one of the files.
|
||||
const contents = fs.readdirSync(directory);
|
||||
for (let file of contents) {
|
||||
if (filenames.includes(file)) return directory;
|
||||
}
|
||||
|
||||
// If unable to find one of the files, check subdirectories.
|
||||
for (let subDir of contents) {
|
||||
let dirPath = path.join(directory, subDir);
|
||||
let stat = fs.statSync(dirPath);
|
||||
if (stat.isDirectory()) {
|
||||
let subDirectoryResult = findFilesSync(filenames, dirPath);
|
||||
if (subDirectoryResult) return subDirectoryResult;
|
||||
}
|
||||
}
|
||||
|
||||
// Unable to find the files.
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
//TODO: Add type definitions for the manifest files.
|
||||
|
||||
/**
|
||||
* @typedef {Object} PackData - Information extracted from an installed pack.
|
||||
* @property {String} name - The name found in the packs manifest.json file.
|
||||
* @property {String} uuid - The uuid found in the packs manifest.json file.
|
||||
* @property {String} version - the version found in the packs manifest.json fle.
|
||||
* @property {String} location - The full path to the root directory of the installed pack.
|
||||
* Used by the mapInstalledPacks function
|
||||
*/
|
@ -1,38 +0,0 @@
|
||||
import {promises as fsPromise, existsSync as fsExists} from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import admZip from "adm-zip";
|
||||
import { serverRoot } from "../pathControl";
|
||||
|
||||
const bedrockPath = path.join(serverRoot, "bedrock");
|
||||
/**
|
||||
* Create backup for Worlds and Settings
|
||||
*/
|
||||
export async function CreateBackup(): Promise<Buffer> {
|
||||
if (!(fsExists(bedrockPath))) throw new Error("Bedrock folder does not exist");
|
||||
const zip = new admZip();
|
||||
const FFs = (await fsPromise.readdir(bedrockPath)).filter(FF => (["allowlist.json", "permissions.json", "server.properties", "worlds"]).some(file => file === FF));
|
||||
for (const FF of FFs) {
|
||||
const FFpath = path.join(bedrockPath, FF);
|
||||
const stats = await fsPromise.stat(FFpath);
|
||||
if (stats.isSymbolicLink()) {
|
||||
const realPath = await fsPromise.realpath(FFpath);
|
||||
const realStats = await fsPromise.stat(realPath);
|
||||
if (realStats.isDirectory()) zip.addLocalFolder(realPath, FF);
|
||||
else zip.addLocalFile(realPath, FF);
|
||||
} else if (stats.isDirectory()) zip.addLocalFolder(FFpath);
|
||||
else zip.addLocalFile(FFpath);
|
||||
}
|
||||
// Return Buffer
|
||||
return zip.toBufferPromise();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore backup for Worlds and Settings
|
||||
*
|
||||
* WARNING: This will overwrite existing files and World folder files
|
||||
*/
|
||||
export async function RestoreBackup(zipBuffer: Buffer): Promise<void> {
|
||||
const zip = new admZip(zipBuffer);
|
||||
await new Promise((resolve, reject) => zip.extractAllToAsync(bedrockPath, true, true, (err) => !!err ? reject(err) : resolve("")));
|
||||
return;
|
||||
}
|
@ -1,324 +0,0 @@
|
||||
import os from "os";
|
||||
import path from "node:path";
|
||||
import fs, { promises as fsPromise } from "node:fs";
|
||||
import AdmZip from "adm-zip";
|
||||
import * as Proprieties from "../lib/Proprieties"
|
||||
import { parse as nbtParse, NBT, Metadata as nbtData, NBTFormat } from "prismarine-nbt";
|
||||
import { getBuffer } from "../lib/HttpRequests";
|
||||
import { serverRoot } from "../pathControl";
|
||||
const serverPath = path.join(serverRoot, "bedrock");
|
||||
|
||||
export type bedrockConfig = {
|
||||
/** This is the server name shown in the in-game server list. */
|
||||
serverName: string,
|
||||
/** The maximum numbers of players that should be able to play on the server. `Higher values have performance impact.` */
|
||||
maxPlayers?: number,
|
||||
/** Default gamemode to server and new Players */
|
||||
gamemode: "survival"|"creative"|"adventure"|1|2|3,
|
||||
/** Default server difficulty */
|
||||
difficulty?: "peaceful"|1|"easy"|2|"normal"|3|"hard"|4,
|
||||
/** Which permission level new players will have when they join for the first time. */
|
||||
PlayerDefaultPermissionLevel?: "visitor"|"member"|"operator",
|
||||
/** World Name to show in list friends and pause menu */
|
||||
worldName: string,
|
||||
/** The seed to be used for randomizing the world (`If left empty a seed will be chosen at random`). */
|
||||
worldSeed?: string|number,
|
||||
/** For remote servers always use true as the server will be exposed. */
|
||||
requiredXboxLive?: true|false,
|
||||
/** if enabled, allow only player in permission.json */
|
||||
allowList?: true|false,
|
||||
/** if enabled server allow commands, Command block and in survival disable achievements */
|
||||
allowCheats?: true|false,
|
||||
/** Server Ports */
|
||||
port?: {
|
||||
/** IPv4 Port, different for v6 */
|
||||
v4?: number,
|
||||
/** IPv6 */
|
||||
v6?: number,
|
||||
},
|
||||
/** The maximum allowed view distance (`Higher values have performance impact`). */
|
||||
viewDistance?: number,
|
||||
/** The world will be ticked this many chunks away from any player (`Higher values have performance impact`). */
|
||||
tickDistance?: number,
|
||||
/** After a player has idled for this many minutes they will be kicked (`If set to 0 then players can idle indefinitely`). */
|
||||
playerIdleTimeout?: number,
|
||||
/** Maximum number of threads the server will try to use (`Bds Core auto detect Threads`). */
|
||||
maxCpuThreads?: number,
|
||||
/** If the world uses any specific texture packs then this setting will force the client to use it. */
|
||||
texturepackRequired?: true|false
|
||||
};
|
||||
|
||||
export async function CreateServerConfig(config: bedrockConfig): Promise<bedrockConfig> {
|
||||
if (!!config.difficulty) {
|
||||
if (typeof config.difficulty === "number") {
|
||||
if (config.difficulty === 1) config.difficulty = "peaceful";
|
||||
else if (config.difficulty === 2) config.difficulty = "easy";
|
||||
else if (config.difficulty === 3) config.difficulty = "normal";
|
||||
else if (config.difficulty === 4) config.difficulty = "hard";
|
||||
else {
|
||||
console.log("[Bds Core] Invalid difficulty value, defaulting to normal");
|
||||
config.difficulty = "normal";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!!config.gamemode) {
|
||||
if (typeof config.gamemode === "number") {
|
||||
if (config.gamemode === 1) config.gamemode = "survival";
|
||||
else if (config.gamemode === 2) config.gamemode = "creative";
|
||||
else if (config.gamemode === 3) config.gamemode = "adventure";
|
||||
else {
|
||||
console.log("[Bds Core] Invalid gamemode value, defaulting to survival");
|
||||
config.gamemode = "survival";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!!config.viewDistance) {
|
||||
if (typeof config.viewDistance === "number") {
|
||||
if (config.viewDistance < 4) {
|
||||
console.log("[Bds Core] Invalid view distance value, defaulting to 4");
|
||||
config.viewDistance = 4;
|
||||
}
|
||||
} else {
|
||||
console.log("[Bds Core] Invalid view distance value, defaulting to 4");
|
||||
config.viewDistance = 4;
|
||||
}
|
||||
}
|
||||
if (!!config.tickDistance) {
|
||||
if (typeof config.tickDistance === "number") {
|
||||
if (config.tickDistance < 4) {
|
||||
console.log("[Bds Core] Invalid tick distance value, defaulting to 4");
|
||||
config.tickDistance = 4;
|
||||
}
|
||||
} else {
|
||||
console.log("[Bds Core] Invalid tick distance value, defaulting to 4");
|
||||
config.tickDistance = 4;
|
||||
}
|
||||
}
|
||||
if (!!config.maxPlayers) {
|
||||
if (typeof config.maxPlayers === "number") {
|
||||
if (config.maxPlayers < 2) {
|
||||
console.log("[Bds Core] Invalid max players value, defaulting to 2");
|
||||
config.maxPlayers = 2;
|
||||
}
|
||||
} else {
|
||||
console.log("[Bds Core] Invalid max players value, defaulting to 2");
|
||||
config.maxPlayers = 2;
|
||||
}
|
||||
}
|
||||
if (!!config.playerIdleTimeout||config.playerIdleTimeout !== 0) {
|
||||
if (typeof config.playerIdleTimeout === "number") {
|
||||
if (config.playerIdleTimeout < 0) {
|
||||
console.log("[Bds Core] Invalid player idle timeout value, defaulting to 0");
|
||||
config.playerIdleTimeout = 0;
|
||||
}
|
||||
} else {
|
||||
console.log("[Bds Core] Invalid player idle timeout value, defaulting to 0");
|
||||
config.playerIdleTimeout = 0;
|
||||
}
|
||||
}
|
||||
if (!!config.port) {
|
||||
if (!!config.port.v4) {
|
||||
if (typeof config.port.v4 === "number") {
|
||||
if (config.port.v4 < 1) {
|
||||
console.log("[Bds Core] Invalid v4 port value, defaulting to 19132");
|
||||
config.port.v4 = 19132;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!!config.port.v6) {
|
||||
if (typeof config.port.v6 === "number") {
|
||||
if (config.port.v6 < 1) {
|
||||
console.log("[Bds Core] Invalid v6 port value, defaulting to 19133");
|
||||
config.port.v6 = 19133;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const serverName = config.serverName || "Bedrock Server";
|
||||
const maxPlayers = config.maxPlayers || 20;
|
||||
const gamemode = config.gamemode || "survival";
|
||||
const difficulty = config.difficulty || "peaceful";
|
||||
const PlayerDefaultPermissionLevel = config.PlayerDefaultPermissionLevel || "member";
|
||||
const worldName = config.worldName || "Bedrock level";
|
||||
const worldSeed = config.worldSeed || "";
|
||||
const requiredXboxLive = config.requiredXboxLive || true;
|
||||
const allowList = config.allowList || false;
|
||||
const allowCheats = config.allowCheats || false;
|
||||
const port = {v4: (config.port||{}).v4 || 19132, v6: (config.port||{}).v6 || 19133};
|
||||
const viewDistance = config.viewDistance || 32;
|
||||
const tickDistance = config.tickDistance || 4;
|
||||
const playerIdleTimeout = config.playerIdleTimeout || 0;
|
||||
const maxCpuThreads = config.maxCpuThreads || os.cpus().length || 8;
|
||||
const texturepackRequired = config.texturepackRequired || false;
|
||||
|
||||
// Server config
|
||||
const configFileArray = [
|
||||
`server-name=${serverName}`,
|
||||
`gamemode=${gamemode}`,
|
||||
"force-gamemode=false",
|
||||
`difficulty=${difficulty}`,
|
||||
`allow-cheats=${allowCheats}`,
|
||||
`max-players=${maxPlayers}`,
|
||||
`online-mode=${requiredXboxLive}`,
|
||||
`allow-list=${allowList}`,
|
||||
`server-port=${port.v4}`,
|
||||
`server-portv6=${port.v6}`,
|
||||
`view-distance=${viewDistance}`,
|
||||
`tick-distance=${tickDistance}`,
|
||||
`player-idle-timeout=${playerIdleTimeout}`,
|
||||
`max-threads=${maxCpuThreads}`,
|
||||
`level-name=${worldName}`,
|
||||
`level-seed=${worldSeed}`,
|
||||
`default-player-permission-level=${PlayerDefaultPermissionLevel}`,
|
||||
`texturepack-required=${texturepackRequired}`,
|
||||
"emit-server-telemetry=true",
|
||||
"content-log-file-enabled=false",
|
||||
"compression-threshold=1",
|
||||
"server-authoritative-movement=server-auth",
|
||||
"player-movement-score-threshold=20",
|
||||
"player-movement-action-direction-threshold=0.85",
|
||||
"player-movement-distance-threshold=0.3",
|
||||
"player-movement-duration-threshold-in-ms=500",
|
||||
"correct-player-movement=false",
|
||||
"server-authoritative-block-breaking=false"
|
||||
];
|
||||
|
||||
// Write config file
|
||||
await fsPromise.writeFile(path.join(serverPath, "server.properties"), configFileArray.join("\n"), {encoding: "utf8"});
|
||||
|
||||
// Return writed config
|
||||
return {
|
||||
serverName,
|
||||
maxPlayers,
|
||||
gamemode,
|
||||
difficulty,
|
||||
PlayerDefaultPermissionLevel,
|
||||
worldName,
|
||||
worldSeed,
|
||||
requiredXboxLive,
|
||||
allowList,
|
||||
allowCheats,
|
||||
port,
|
||||
viewDistance,
|
||||
tickDistance,
|
||||
playerIdleTimeout,
|
||||
maxCpuThreads,
|
||||
texturepackRequired
|
||||
}
|
||||
}
|
||||
|
||||
type bedrockParsedConfig = {
|
||||
/** This is the server name shown in the in-game server list. */
|
||||
serverName: string,
|
||||
/** World Name to show in list friends and pause menu */
|
||||
worldName: string,
|
||||
/** Default gamemode to server and new Players */
|
||||
gamemode: "survival"|"creative"|"adventure",
|
||||
/** The maximum numbers of players that should be able to play on the server. `Higher values have performance impact.` */
|
||||
maxPlayers: number,
|
||||
/** Default server difficulty */
|
||||
difficulty: "peaceful"|"easy"|"normal"|"hard",
|
||||
/** The seed to be used for randomizing the world (`If left empty a seed will be chosen at random`). */
|
||||
worldSeed: string|number,
|
||||
port: {
|
||||
v4: number,
|
||||
v6: number
|
||||
},
|
||||
/** World NBT */
|
||||
nbtParsed: {parsed: NBT, type: NBTFormat, metadata: nbtData}
|
||||
};
|
||||
export async function getConfig(): Promise<bedrockParsedConfig> {
|
||||
const config: bedrockParsedConfig = {
|
||||
serverName: "Bedrock Server",
|
||||
worldName: "Bedrock level",
|
||||
gamemode: "survival",
|
||||
difficulty: "normal",
|
||||
maxPlayers: 0,
|
||||
worldSeed: "",
|
||||
port: {
|
||||
v4: 19132,
|
||||
v6: 19133
|
||||
},
|
||||
nbtParsed: undefined
|
||||
};
|
||||
if (fs.existsSync(path.join(serverPath, "server.properties"))) {
|
||||
const ProPri = Proprieties.parse(await fsPromise.readFile(path.join(serverPath, "server.properties"), {encoding: "utf8"}));
|
||||
if (ProPri["server-name"] !== undefined) config.serverName = String(ProPri["server-name"]);
|
||||
if (ProPri["level-name"] !== undefined) config.worldName = String(ProPri["level-name"]);
|
||||
if (ProPri["gamemode"] !== undefined) config.gamemode = String(ProPri["gamemode"]) as "survival"|"creative"|"adventure";
|
||||
if (ProPri["max-players"] !== undefined) config.maxPlayers = Number(ProPri["max-players"]);
|
||||
if (ProPri["difficulty"] !== undefined) config.difficulty = String(ProPri["difficulty"]) as "peaceful"|"easy"|"normal"|"hard";
|
||||
if (ProPri["server-port"] !== undefined) config.port.v4 = Number(ProPri["server-port"]);
|
||||
if (ProPri["server-portv6"] !== undefined) config.port.v6 = Number(ProPri["server-portv6"]);
|
||||
if (ProPri["level-seed"] !== undefined) config.worldSeed = String(ProPri["level-seed"]);
|
||||
// if (ProPri["allow-cheats"] !== undefined) config.allowCheats = Boolean(ProPri["allow-cheats"]);
|
||||
// if (ProPri["allow-list"] !== undefined) config.allowList = Boolean(ProPri["allow-list"]);
|
||||
// if (ProPri["texturepack-required"] !== undefined) config.texturepackRequired = Boolean(ProPri["texturepack-required"]);
|
||||
// if (ProPri["view-distance"] !== undefined) config.viewDistance = Number(ProPri["view-distance"]);
|
||||
// if (ProPri["tick-distance"] !== undefined) config.tickDistance = Number(ProPri["tick-distance"]);
|
||||
// if (ProPri["player-idle-timeout"] !== undefined) config.playerIdleTimeout = Number(ProPri["player-idle-timeout"]);
|
||||
// if (ProPri["max-threads"] !== undefined) config.maxCpuThreads = Number(ProPri["max-threads"]);
|
||||
// if (ProPri["default-player-permission-level"] !== undefined) config.PlayerDefaultPermissionLevel = String(ProPri["default-player-permission-level"]);
|
||||
// if (ProPri["emit-server-telemetry"] !== undefined) config.emitServerTelemetry = Boolean(ProPri["emit-server-telemetry"]);
|
||||
// if (ProPri["content-log-file-enabled"] !== undefined) config.contentLogFileEnabled = Boolean(ProPri["content-log-file-enabled"]);
|
||||
// if (ProPri["compression-threshold"] !== undefined) config.compressionThreshold = Number(ProPri["compression-threshold"]);
|
||||
// if (ProPri["server-authoritative-movement"] !== undefined) config.
|
||||
const worldDatePath = path.join(serverPath, "worlds", config.worldName, "level.dat");
|
||||
if (fs.existsSync(worldDatePath)) config.nbtParsed = await nbtParse(await fsPromise.readFile(worldDatePath));
|
||||
if (ProPri["level-seed"] !== undefined) config.worldSeed = String(ProPri["level-seed"]);
|
||||
else {
|
||||
if (config.nbtParsed !== undefined) {
|
||||
const seedValue = ((((((config||{}).nbtParsed||{}).parsed||{}).value||{}).RandomSeed||{}).value||"").toString()
|
||||
if (!!seedValue) config.worldSeed = seedValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (config.worldSeed === "null") delete config.worldSeed;
|
||||
return config;
|
||||
}
|
||||
|
||||
export async function Permission(): Promise<Array<{ignoresPlayerLimit: false|true, name: string, xuid?: string}>> {
|
||||
const permissionPath = path.join(serverPath, "allowlist.json");
|
||||
if (fs.existsSync(permissionPath)) {
|
||||
const permission = JSON.parse(await fsPromise.readFile(permissionPath, {encoding: "utf8"}));
|
||||
return permission;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function resourcePack(WorldName: string) {
|
||||
const mapPath = path.join(serverPath, "worlds", WorldName);
|
||||
if (!(fs.existsSync(mapPath))) throw new Error("Map not found");
|
||||
const remotePack = async () => {
|
||||
const { tree } = await getBuffer("https://api.github.com/repos/The-Bds-Maneger/BedrockAddonTextureManeger/git/trees/main?recursive=true").then(res => JSON.parse(res.toString()) as {sha: string, url: string, truncated: true|false, tree: Array<{path: string, mode: string, type: "tree"|"blob", sha: string, size: number, url: string}>});
|
||||
const pack = tree.filter(item => item.path.includes(".mcpack") && item.type === "blob");
|
||||
return await Promise.all(pack.map(BlobFile => getBuffer(BlobFile.url).then(res => JSON.parse(res.toString())).then(res => {
|
||||
const fileBuffer = Buffer.from(res.content, "base64");
|
||||
const fileName = BlobFile.path.split("/").pop().replace(/\.mcpack.*/, "");
|
||||
const zip = new AdmZip(fileBuffer);
|
||||
const manifest = JSON.parse(zip.getEntry("manifest.json").getData().toString()) as {format_version: number, header: {name: string, description: string, uuid: string, version: Array<number>, min_engine_version?: Array<number>}, modules: Array<{type: string, uuid: string, version: Array<number>}>, metadata?: {authors?: Array<string>, url?: string}};
|
||||
return {fileName, fileBuffer, manifest};
|
||||
})));
|
||||
};
|
||||
// const localPack = async () => {};
|
||||
const installPack = async (zipBuffer: Buffer) => {
|
||||
const worldResourcePacksPath = path.join(mapPath, "world_resource_packs.json");
|
||||
let worldResourcePacks: Array<{pack_id: string, version: Array<number>}> = [];
|
||||
if (fs.existsSync(worldResourcePacksPath)) worldResourcePacks = JSON.parse(await fsPromise.readFile(worldResourcePacksPath, {encoding: "utf8"}));
|
||||
const zip = new AdmZip(zipBuffer);
|
||||
const manifest = JSON.parse(zip.getEntry("manifest.json").getData().toString()) as {format_version: number, header: {name: string, description: string, uuid: string, version: Array<number>, min_engine_version?: Array<number>}, modules: Array<{type: string, uuid: string, version: Array<number>}>, metadata?: {authors?: Array<string>, url?: string}};
|
||||
const pack_id = manifest.header.uuid;
|
||||
if (worldResourcePacks.find(item => item.pack_id === pack_id)) throw new Error("Pack already installed");
|
||||
worldResourcePacks.push({pack_id, version: manifest.header.version});
|
||||
await fsPromise.writeFile(worldResourcePacksPath, JSON.stringify(worldResourcePacks, null, 2));
|
||||
return {pack_id, version: manifest.header.version.join(".")};
|
||||
};
|
||||
const removePack = async () => {};
|
||||
|
||||
return {
|
||||
remotePack,
|
||||
//localPack,
|
||||
installPack,
|
||||
removePack
|
||||
};
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import adm_zip from "adm-zip";
|
||||
import * as versionManeger from "@the-bds-maneger/server_versions";
|
||||
import * as httpRequests from "../lib/HttpRequests";
|
||||
import { runCommandAsync } from "../lib/childProcess"
|
||||
import { serverRoot } from "../pathControl";
|
||||
|
||||
export async function download(version: string|boolean) {
|
||||
const ServerPath = path.join(serverRoot, "bedrock");
|
||||
if (!(await fs.existsSync(ServerPath))) fs.mkdirSync(ServerPath, {recursive: true});
|
||||
let arch = process.arch;
|
||||
if (process.platform === "linux" && process.arch !== "x64") {
|
||||
const existQemu = await runCommandAsync("command -v qemu-x86_64-static").then(() => true).catch(() => false);
|
||||
if (existQemu) arch = "x64";
|
||||
}
|
||||
const bedrockInfo = await versionManeger.findUrlVersion("bedrock", version, arch);
|
||||
const BedrockZip = new adm_zip(await httpRequests.getBuffer(bedrockInfo.url));
|
||||
let realPathWorldBedrock = "";
|
||||
if (fs.existsSync(path.resolve(ServerPath, "worlds"))) {
|
||||
if (fs.lstatSync(path.resolve(ServerPath, "worlds")).isSymbolicLink()) {
|
||||
realPathWorldBedrock = await fs.promises.realpath(path.resolve(ServerPath, "worlds"));
|
||||
await fs.promises.unlink(path.resolve(ServerPath, "worlds"));
|
||||
}
|
||||
}
|
||||
let ServerProperties = "";
|
||||
if (fs.existsSync(path.resolve(ServerPath, "server.properties"))) {
|
||||
ServerProperties = await fs.promises.readFile(path.resolve(ServerPath, "server.properties"), "utf8");
|
||||
await fs.promises.rm(path.resolve(ServerPath, "server.properties"));
|
||||
}
|
||||
BedrockZip.extractAllTo(ServerPath, true);
|
||||
if (!!realPathWorldBedrock) await fs.promises.symlink(realPathWorldBedrock, path.resolve(ServerPath, "worlds"), "dir");
|
||||
if (!!ServerProperties) await fs.promises.writeFile(path.resolve(ServerPath, "server.properties"), ServerProperties, "utf8");
|
||||
|
||||
// Return info
|
||||
return {
|
||||
version: bedrockInfo.version,
|
||||
publishDate: bedrockInfo.datePublish,
|
||||
url: bedrockInfo.url,
|
||||
};
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
|
||||
export {download as DownloadServer} from "./download";
|
||||
export * as linkWorld from "./linkWorld";
|
||||
export * as config from "./config";
|
||||
export * as server from "./server";
|
||||
export * as backup from "./backup";
|
||||
export * as addon from "./addon";
|
@ -1,21 +0,0 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as fsOld from "node:fs";
|
||||
import * as path from "path";
|
||||
import { serverRoot, worldStorageRoot } from "../pathControl";
|
||||
|
||||
export async function linkWorld(): Promise<void> {
|
||||
const worldFolder = path.join(worldStorageRoot, "bedrock");
|
||||
const bedrockFolder = path.join(serverRoot, "bedrock");
|
||||
if (!fsOld.existsSync(bedrockFolder)) throw new Error("Server not installed")
|
||||
if (!fsOld.existsSync(worldFolder)) await fs.mkdir(worldFolder, {recursive: true});
|
||||
const bedrockServerWorld = path.join(bedrockFolder, "worlds");
|
||||
if (fsOld.existsSync(bedrockServerWorld)) {
|
||||
if ((await fs.lstat(bedrockServerWorld)).isSymbolicLink()) return;
|
||||
for (const folder of await fs.readdir(bedrockServerWorld)) {
|
||||
await fs.rename(path.join(bedrockServerWorld, folder), path.join(worldFolder, folder))
|
||||
}
|
||||
await fs.rmdir(bedrockServerWorld);
|
||||
}
|
||||
await fs.symlink(worldFolder, bedrockServerWorld, "dir");
|
||||
return;
|
||||
}
|
@ -1,196 +0,0 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import crypto from "crypto";
|
||||
import node_cron from "cron";
|
||||
import * as child_process from "../lib/childProcess";
|
||||
import { backupRoot, serverRoot } from "../pathControl";
|
||||
import { BdsSession, bdsSessionCommands, playerAction2 } from '../globalType';
|
||||
import { getConfig } from "./config";
|
||||
import { CreateBackup } from "./backup";
|
||||
import events from "../lib/customEvents";
|
||||
import portislisten from "../lib/portIsAllocated";
|
||||
import { linkWorld } from "./linkWorld";
|
||||
|
||||
const bedrockSesions: {[key: string]: BdsSession} = {};
|
||||
export function getSessions() {return bedrockSesions;}
|
||||
|
||||
const ServerPath = path.join(serverRoot, "bedrock");
|
||||
export async function startServer(): Promise<BdsSession> {
|
||||
if (!(fs.existsSync(ServerPath))) throw new Error("server dont installed");
|
||||
if (process.env.AUTO_LINK_WORLD === "true" || process.env.AUTO_LINK_WORLDS === "1") await linkWorld();
|
||||
const SessionID = crypto.randomUUID();
|
||||
const serverConfig = await getConfig();
|
||||
if (await portislisten(serverConfig.port.v4)) throw new Error("Port is already in use");
|
||||
if (await portislisten(serverConfig.port.v6)) throw new Error("Port is already in use");
|
||||
const Process: {command: string; args: Array<string>; env: {[env: string]: string};} = {command: "", args: [], env: {...process.env}};
|
||||
if (process.platform === "darwin") throw new Error("Run Docker image");
|
||||
Process.command = path.resolve(ServerPath, "bedrock_server"+(process.platform === "win32"?".exe":""));
|
||||
if (process.platform !== "win32") {
|
||||
await child_process.runAsync("chmod", ["a+x", Process.command]);
|
||||
Process.env.LD_LIBRARY_PATH = path.resolve(ServerPath, "bedrock");
|
||||
if (process.platform === "linux" && process.arch !== "x64") {
|
||||
const existQemu = await child_process.runCommandAsync("command -v qemu-x86_64-static").then(() => true).catch(() => false);
|
||||
if (existQemu) {
|
||||
console.warn("Minecraft bedrock start with emulated x64 architecture");
|
||||
Process.args.push(Process.command);
|
||||
Process.command = "qemu-x86_64-static";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start Server
|
||||
const serverEvents = new events({captureRejections: false});
|
||||
serverEvents.setMaxListeners(0);
|
||||
const ServerProcess = await child_process.execServer({runOn: "host"}, Process.command, Process.args, {env: Process.env, cwd: ServerPath});
|
||||
// Log Server redirect to callbacks events and exit
|
||||
ServerProcess.on("out", data => serverEvents.emit("log_stdout", data));
|
||||
ServerProcess.on("err", data => serverEvents.emit("log_stderr", data));
|
||||
ServerProcess.on("all", data => serverEvents.emit("log", data));
|
||||
ServerProcess.Exec.on("exit", code => {
|
||||
serverEvents.emit("closed", code);
|
||||
if (code === null) serverEvents.emit("err", new Error("Server exited with code null"));
|
||||
});
|
||||
|
||||
// on start
|
||||
serverEvents.on("log", lineData => {
|
||||
// [2022-05-19 22:35:09:315 INFO] Server started.
|
||||
if (/\[.*\]\s+Server\s+started\./.test(lineData)) serverEvents.emit("started", new Date());
|
||||
});
|
||||
|
||||
// Port
|
||||
serverEvents.on("log", data => {
|
||||
const portParse = data.match(/(IPv[46])\s+supported,\s+port:\s+(.*)/);
|
||||
if (!!portParse) serverEvents.emit("port_listen", {port: parseInt(portParse[2]), protocol: "UDP", version: portParse[1] as "IPv4"|"IPv6"});
|
||||
});
|
||||
|
||||
// Player
|
||||
serverEvents.on("log", data => {
|
||||
if (/r\s+.*\:\s+.*\,\s+xuid\:\s+.*/gi.test(data)) {
|
||||
const actionDate = new Date();
|
||||
const [action, player, xuid] = (data.match(/r\s+(.*)\:\s+(.*)\,\s+xuid\:\s+(.*)/)||[]).slice(1, 4);
|
||||
const playerAction: playerAction2 = {player: player, xuid: xuid, action: "unknown", Date: actionDate};
|
||||
if (action === "connected") playerAction.action = "connect";
|
||||
else if (action === "disconnected") playerAction.action = "disconnect";
|
||||
|
||||
// Server player event
|
||||
serverEvents.emit("player", playerAction);
|
||||
delete playerAction.action;
|
||||
if (action === "connect") serverEvents.emit("player_connect", playerAction);
|
||||
else if (action === "disconnect") serverEvents.emit("player_disconnect", playerAction);
|
||||
else serverEvents.emit("player_unknown", playerAction);
|
||||
}
|
||||
});
|
||||
|
||||
// Run Command
|
||||
const serverCommands: bdsSessionCommands = {
|
||||
/**
|
||||
* Run any commands in server.
|
||||
* @param command - Run any commands in server without parse commands
|
||||
* @returns - Server commands
|
||||
*/
|
||||
execCommand: (...command) => {
|
||||
ServerProcess.writelf(command.map(a => String(a)).join(" "));
|
||||
return serverCommands;
|
||||
},
|
||||
tpPlayer: (player: string, x: number, y: number, z: number) => {
|
||||
serverCommands.execCommand("tp", player, x, y, z);
|
||||
return serverCommands;
|
||||
},
|
||||
worldGamemode: (gamemode: "survival"|"creative"|"hardcore") => {
|
||||
serverCommands.execCommand("gamemode", gamemode);
|
||||
return serverCommands;
|
||||
},
|
||||
userGamemode: (player: string, gamemode: "survival"|"creative"|"hardcore") => {
|
||||
serverCommands.execCommand("gamemode", gamemode, player);
|
||||
return serverCommands;
|
||||
},
|
||||
stop: (): Promise<number|null> => {
|
||||
if (ServerProcess.Exec.exitCode !== null||ServerProcess.Exec.killed) return Promise.resolve(ServerProcess.Exec.exitCode);
|
||||
if (ServerProcess.Exec.killed) return Promise.resolve(ServerProcess.Exec.exitCode);
|
||||
ServerProcess.writelf("stop");
|
||||
return ServerProcess.onExit();
|
||||
}
|
||||
}
|
||||
|
||||
const backupCron = (crontime: string|Date, option?: {type: "zip", config?: {pathZip?: string}}): node_cron.CronJob => {
|
||||
// Validate Config
|
||||
if (option) {
|
||||
if (option.type === "zip") {}
|
||||
else option = {type: "zip"};
|
||||
}
|
||||
async function lockServerBackup() {
|
||||
serverCommands.execCommand("save hold");
|
||||
await new Promise(accept => setTimeout(accept, 1000));
|
||||
serverCommands.execCommand("save query");
|
||||
await new Promise(accept => setTimeout(accept, 1000));
|
||||
}
|
||||
async function unLockServerBackup() {
|
||||
serverCommands.execCommand("save resume");
|
||||
await new Promise(accept => setTimeout(accept, 1000));
|
||||
}
|
||||
if (!option) option = {type: "zip"};
|
||||
const CrontimeBackup = new node_cron.CronJob(crontime, async () => {
|
||||
if (option.type === "zip") {
|
||||
await lockServerBackup();
|
||||
if (!!option?.config?.pathZip) await CreateBackup().then(res => fs.promises.writeFile(path.resolve(backupRoot, option?.config?.pathZip), res)).catch(() => undefined);
|
||||
// else await createZipBackup(true).catch(() => undefined);
|
||||
await unLockServerBackup();
|
||||
}
|
||||
});
|
||||
CrontimeBackup.start();
|
||||
serverEvents.on("closed", () => CrontimeBackup.stop());
|
||||
return CrontimeBackup;
|
||||
}
|
||||
|
||||
// Session log
|
||||
const logFile = path.resolve(process.env.LOG_PATH||path.resolve(ServerPath, "../log"), `bedrock_${SessionID}.log`);
|
||||
if(!(fs.existsSync(path.parse(logFile).dir))) fs.mkdirSync(path.parse(logFile).dir, {recursive: true});
|
||||
const logStream = fs.createWriteStream(logFile, {flags: "w+"});
|
||||
logStream.write(`[${(new Date()).toString()}] Server started\n\n`);
|
||||
ServerProcess.Exec.stdout.pipe(logStream);
|
||||
ServerProcess.Exec.stderr.pipe(logStream);
|
||||
|
||||
// Session Object
|
||||
const Seesion: BdsSession = {
|
||||
id: SessionID,
|
||||
logFile: logFile,
|
||||
creteBackup: backupCron,
|
||||
seed: serverConfig.worldSeed,
|
||||
ports: [],
|
||||
Player: {},
|
||||
commands: serverCommands,
|
||||
server: {
|
||||
on: (act, fn) => serverEvents.on(act, fn),
|
||||
once: (act, fn) => serverEvents.once(act, fn),
|
||||
started: false,
|
||||
startDate: new Date(),
|
||||
}
|
||||
};
|
||||
|
||||
serverEvents.on("port_listen", Seesion.ports.push);
|
||||
serverEvents.on("started", date => {Seesion.server.started = true; Seesion.server.startDate = date;});
|
||||
serverEvents.on("player", playerAction => {
|
||||
// Add to object
|
||||
const playerExist = !!Seesion.Player[playerAction.player];
|
||||
if (playerExist) {
|
||||
Seesion.Player[playerAction.player].action = playerAction.action;
|
||||
Seesion.Player[playerAction.player].date = playerAction.Date;
|
||||
Seesion.Player[playerAction.player].history.push({
|
||||
action: playerAction.action,
|
||||
date: playerAction.Date
|
||||
});
|
||||
} else Seesion.Player[playerAction.player] = {
|
||||
action: playerAction.action,
|
||||
date: playerAction.Date,
|
||||
history: [{
|
||||
action: playerAction.action,
|
||||
date: playerAction.Date
|
||||
}]
|
||||
};
|
||||
});
|
||||
|
||||
// Return Session
|
||||
bedrockSesions[SessionID] = Seesion;
|
||||
serverEvents.on("closed", () => delete bedrockSesions[SessionID]);
|
||||
return Seesion;
|
||||
}
|
69
src/childPromisses.ts
Normal file
69
src/childPromisses.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import type { ObjectEncodingOptions } from "node:fs";
|
||||
import { execFile, ExecFileOptions, ChildProcess } from "node:child_process";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { promisify } from "node:util";
|
||||
export const execFileAsync = promisify(execFile);
|
||||
export {execFile};
|
||||
|
||||
export class customChild {
|
||||
private eventMiter = new EventEmitter({captureRejections: false});
|
||||
private tempLog = {};
|
||||
private child?: ChildProcess;
|
||||
|
||||
public kill(signal?: number|NodeJS.Signals) {if(this.child?.killed) return this.child?.killed;return this.child?.kill(signal);}
|
||||
|
||||
private emit(act: "error", data: Error): this;
|
||||
private emit(act: "close", data: {code: number, signal: NodeJS.Signals}): this;
|
||||
private emit(act: "stdoutRaw", data: string): this;
|
||||
private emit(act: "stderrRaw", data: string): this;
|
||||
private emit(act: "breakStdout", data: string): this;
|
||||
private emit(act: "breakStderr", data: string): this;
|
||||
private emit(act: string, ...args: any[]): this {this.eventMiter.emit(act, ...args); return this;}
|
||||
|
||||
public on(act: "error", fn: (err: Error) => void): this;
|
||||
public on(act: "close", fn: (data: {code: number, signal: NodeJS.Signals}) => void): this;
|
||||
public on(act: "stdoutRaw", fn: (data: string) => void): this;
|
||||
public on(act: "stderrRaw", fn: (data: string) => void): this;
|
||||
public on(act: "breakStdout", fn: (data: string) => void): this;
|
||||
public on(act: "breakStderr", fn: (data: string) => void): this;
|
||||
public on(act: string, fn: (...args: any[]) => void): this {this.eventMiter.on(act, fn); return this;}
|
||||
|
||||
public once(act: "stdoutRaw", fn: (data: string) => void): this;
|
||||
public once(act: "stderrRaw", fn: (data: string) => void): this;
|
||||
public once(act: "breakStdout", fn: (data: string) => void): this;
|
||||
public once(act: "breakStderr", fn: (data: string) => void): this;
|
||||
public once(act: string, fn: (...args: any[]) => void): this {this.eventMiter.once(act, fn);return this;}
|
||||
|
||||
constructor(child: ChildProcess) {
|
||||
this.child = child;
|
||||
child.on("close", (code, signal) => this.emit("close", {code, signal}));
|
||||
child.on("exit", (code, signal) => this.emit("close", {code, signal}));
|
||||
child.on("error", err => this.emit("error", err));
|
||||
// Storage tmp lines
|
||||
const parseLog = (to: "breakStdout"|"breakStderr", data: string): any => {
|
||||
if (this.tempLog[to] === undefined) this.tempLog[to] = "";
|
||||
const lines = data.split(/\r?\n/);
|
||||
if (lines.length === 1) return this.tempLog[to] += lines[0];
|
||||
for (const line of lines.slice(0, -1)) {
|
||||
if (!this.tempLog[to]) return this.eventMiter.emit(to, line);
|
||||
this.eventMiter.emit(to, this.tempLog[to]+line);
|
||||
delete this.tempLog[to];
|
||||
}
|
||||
}
|
||||
child.stdout.on("data", data => parseLog("breakStdout", data));
|
||||
child.stderr.on("data", data => parseLog("breakStderr", data));
|
||||
child.stdout.on("data", data => this.eventMiter.emit("stdoutRaw", data instanceof Buffer ? data.toString("utf8"):data));
|
||||
child.stderr.on("data", data => this.eventMiter.emit("stderrRaw", data instanceof Buffer ? data.toString("utf8"):data));
|
||||
}
|
||||
};
|
||||
|
||||
export function exec(command: string): customChild;
|
||||
export function exec(command: string, args: string[]): customChild;
|
||||
export function exec(command: string, options: ObjectEncodingOptions & ExecFileOptions): customChild;
|
||||
export function exec(command: string, args?: ObjectEncodingOptions & ExecFileOptions|string[], options?: ObjectEncodingOptions & ExecFileOptions): customChild {
|
||||
let childOptions: ObjectEncodingOptions & ExecFileOptions = {};
|
||||
let childArgs: string[] = [];
|
||||
if (args instanceof Object) childOptions = args as ObjectEncodingOptions & ExecFileOptions; else if (args instanceof Array) childArgs = args;
|
||||
if (!options) childOptions = options;
|
||||
return new customChild(execFile(command, childArgs, childOptions));
|
||||
}
|
186
src/git.ts
186
src/git.ts
@ -1,186 +0,0 @@
|
||||
import * as child_process from "node:child_process";
|
||||
import * as util from "node:util";
|
||||
// import * as fs from "node:fs/promises";
|
||||
import * as fsOld from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import admZip from "adm-zip";
|
||||
const execFile = util.promisify(child_process.execFile);
|
||||
|
||||
export type fnWithData = (err: Error|undefined, data: string) => void;
|
||||
export type fn = (err?: Error) => void;
|
||||
|
||||
export class git {
|
||||
public readonly repoRoot: string;
|
||||
|
||||
public async status() {
|
||||
const data = await execFile("git", ["status", "-s"], {cwd: this.repoRoot});
|
||||
const status: {file: string, action: "new"|"modificated"|"deleted"|"under"}[] = [];
|
||||
for (const line of data.stdout.split(/\r?\n/g)) {
|
||||
const match = line.trim().match(/^(.*)\s+(.*)$/);
|
||||
if (!match) continue;
|
||||
const [, action, filePath] = match;
|
||||
if (action.trim() === "??") status.push({file: path.resolve(this.repoRoot, filePath), action: "new"});
|
||||
else if (action.trim() === "M") status.push({file: path.resolve(this.repoRoot, filePath), action: "modificated"});
|
||||
else if (action.trim() === "D") status.push({file: path.resolve(this.repoRoot, filePath), action: "deleted"});
|
||||
else status.push({file: path.resolve(this.repoRoot, filePath), action: "under"});
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
public add(files: string|string[], callback: (error?: Error) => void) {
|
||||
const args = ["add"];
|
||||
if (typeof files === "string") args.push(files); else if (files instanceof Array) args.push(...files); else throw new Error("Files is not a string or array");
|
||||
this.status().then(async repoStatus => {
|
||||
if (repoStatus.length === 0) throw new Error("No changes");
|
||||
await execFile("git", args, {cwd: this.repoRoot});
|
||||
}).then(() => callback()).catch(err => callback(err));
|
||||
return this;
|
||||
}
|
||||
|
||||
public addSync(files: string|string[]): Promise<void> {
|
||||
return new Promise<void>((done, reject) => this.add(files, (err) => !!err ? reject(err) : done()));
|
||||
}
|
||||
|
||||
public commit(message: string, body: string[], callback: fn): this;
|
||||
public commit(message: string, callback: (error: Error) => void): this;
|
||||
public commit(message: string, body: string[]): this;
|
||||
public commit(message: string): this;
|
||||
public commit(message: string, body?: string[]|fn, callback?: fn): this {
|
||||
if (!message) throw new Error("No commit message");
|
||||
else if (message.length > 72) throw new Error("Message length is long");
|
||||
const messages = ["-m", message];
|
||||
if (typeof body === "function") {callback = body; body = undefined;}
|
||||
if (body instanceof Array) messages.forEach(message => messages.push("-m", message));
|
||||
execFile("git", ["commit", "-m", ...messages], {cwd: this.repoRoot}).then(() => callback(undefined)).catch(err => callback(err));
|
||||
return this;
|
||||
}
|
||||
|
||||
public commitSync(message: string): Promise<void>;
|
||||
public commitSync(message: string, body: string[]): Promise<void>;
|
||||
public commitSync(message: string, body?: string[]): Promise<void> {
|
||||
return new Promise<void>((done, reject) => this.commit(message, body, (err) => !!err ? reject(err) : done()));
|
||||
}
|
||||
|
||||
public push(branch: string, remote: string, force: boolean, callback: fn): this;
|
||||
public push(branch: string, remote: string, force: boolean): this;
|
||||
public push(branch: string, remote: string): this;
|
||||
public push(branch: string): this;
|
||||
public push(): this;
|
||||
public push(branch?: string, remote?: string, force?: boolean, callback?: fn): this {
|
||||
this.remote("show", async (err, data) => {
|
||||
if (err) if (callback) return callback(err); else throw err;
|
||||
if (data.length === 0) return callback(new Error("No remotes"));
|
||||
const args = ["push"];
|
||||
if (branch) args.push(branch);
|
||||
if (remote) args.push(remote);
|
||||
if (force) args.push("--force");
|
||||
await execFile("git", args, {cwd: this.repoRoot});
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public pushSync(branch: string, remote: string, force: boolean): Promise<void>;
|
||||
public pushSync(branch: string, remote: string): Promise<void>;
|
||||
public pushSync(branch: string): Promise<void>;
|
||||
public pushSync(): Promise<void>;
|
||||
public pushSync(branch?: string, remote?: string, force?: boolean): Promise<void> {
|
||||
return new Promise<void>((done, reject) => this.push(branch, remote, force, (err) => !!err ? reject(err) : done()));
|
||||
}
|
||||
|
||||
public getZip(gitPath: string = "/", callback: (zipDate: Buffer) => void) {
|
||||
if(!!gitPath) if (!fsOld.existsSync(path.join(this.repoRoot, gitPath))) throw new Error("Path does not exist");
|
||||
new Promise<void>(done => {
|
||||
const newZipFile = new admZip();
|
||||
if (!gitPath) gitPath = "/";
|
||||
newZipFile.addLocalFolder(path.normalize(path.join(this.repoRoot)), "/", (filename) => !/\.git/.test(filename));
|
||||
callback(newZipFile.toBuffer());
|
||||
done();
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public remote(action: "remove", remote: string): this;
|
||||
public remote(action: "setHead"): this;
|
||||
public remote(action: "prune"): this;
|
||||
public remote(action: "show", callback: (error: Error|undefined, data: {name: string, url: string, type?: string}[]) => void): this;
|
||||
public remote(action: "add", config: {gitUrl: string, remoteName?: string, auth?: {username: string, password: string}, user?: {name: string, email: string}}, callback: (error: Error|undefined, data: {url: string, auth?: {username: string, password?: string}}) => void): this;
|
||||
public remote(action: "show"|"remove"|"prune"|"setHead"|"add", ...args: any[]) {
|
||||
if (action === "show") {
|
||||
if (typeof args[0] !== "function") throw new Error("Callback is not a function");
|
||||
execFile("git", ["remote", "show"], {cwd: this.repoRoot}).then(remotes => {
|
||||
const result = remotes.stdout.split(/\r?\n/g).filter(x => !!x.trim()).map(x => {
|
||||
const match = x.trim().match(/^(.*)\s+(.*)\s+(\(\d+\))$/) || x.trim().match(/^(.*)\s+(.*)$/);
|
||||
if (!match) return null;
|
||||
const [, name, url, type] = match;
|
||||
return {name, url, type: type ? type.trim() : undefined} as {name: string, url: string, type?: string};
|
||||
});
|
||||
args[0](undefined, result.filter(x => x !== null));
|
||||
}).catch(error => args[0](error));
|
||||
} else if (action === "prune") {
|
||||
if (args[0]) execFile("git", ["remote", "prune", "--dry-run", args[0]], {cwd: this.repoRoot});
|
||||
else {
|
||||
execFile("git", ["remote", "show"], {cwd: this.repoRoot}).then(async ({stdout}) => {
|
||||
const remotes = stdout.split(/\r?\n/g).filter(x => !!x.trim()).map(x => x.trim());
|
||||
for (const remote of remotes) {
|
||||
await execFile("git", ["remote", "prune", "--dry-run", remote], {cwd: this.repoRoot});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (action === "setHead") execFile("git", ["remote", "set-head", "--auto"], {cwd: this.repoRoot});
|
||||
else if (action === "remove") {execFile("git", ["remote", "remove", args[0]], {cwd: this.repoRoot});}
|
||||
else if (action === "add") {
|
||||
if (typeof args[1] !== "function") throw new Error("Callback is not a function");
|
||||
if (typeof args[0] !== "object" && args[0] instanceof Object) throw new Error("Config is not an object");
|
||||
if (!args[0].gitUrl) throw new Error("No git url");
|
||||
if (args[0].user) {
|
||||
if (!args[0].user.name) throw new Error("No user name");
|
||||
if (!args[0].user.email) throw new Error("No user email");
|
||||
}
|
||||
if (args[0].auth) {
|
||||
if (!args[0].auth.username) throw new Error("No auth username");
|
||||
if (!args[0].auth.password) console.warn("Auth password/token is not set, check your config if exist credentials authentication");
|
||||
}
|
||||
const config = {
|
||||
url: args[0].gitUrl as string,
|
||||
remoteName: (!!args[0].remoteName ? args[0].name : "origin") as string,
|
||||
user: args[0].user as undefined|{name: string, email: string},
|
||||
auth: args[0].auth as undefined|{username: string, password?: string}
|
||||
};
|
||||
new Promise<void>(async (done): Promise<any> => {
|
||||
const urlParse = new URL(config.url);
|
||||
let url = urlParse.protocol + "//";
|
||||
if (config.auth) {
|
||||
url += config.auth.username;
|
||||
if (config.auth.password) url += ":" + config.auth.password;
|
||||
url += "@";
|
||||
} else if (urlParse.username) {
|
||||
url += urlParse.username;
|
||||
if (urlParse.password) url += ":" + urlParse.password;
|
||||
url += "@";
|
||||
}
|
||||
url += urlParse.hostname+urlParse.pathname+urlParse.search;
|
||||
await execFile("git", ["remote", "add", "-f", "--tags", config.remoteName, url], {cwd: this.repoRoot});
|
||||
// Done
|
||||
return done();
|
||||
});
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Init repository maneger and if not exists create a empty repository
|
||||
*/
|
||||
constructor(gitPath: string, config?: {remoteUrl?: string}) {
|
||||
this.repoRoot = path.resolve(gitPath);
|
||||
Object.defineProperty(this, "repoRoot", {value: this.repoRoot, enumerable: true, configurable: false, writable: false}); // Make it non-writable and non-configurable to prevent accidental changes
|
||||
|
||||
if (!fsOld.existsSync(this.repoRoot)) {
|
||||
fsOld.mkdirSync(this.repoRoot, {recursive: true});
|
||||
child_process.execFileSync("git", ["init", "-b", "main"], {cwd: this.repoRoot});
|
||||
} else if (!fsOld.existsSync(path.join(this.repoRoot, ".git"))) child_process.execFileSync("git", ["init", "-b", "main"], {cwd: this.repoRoot});
|
||||
|
||||
// Set url
|
||||
if (config?.remoteUrl) this.remote("add", {gitUrl: config.remoteUrl}, () => execFile("git", ["pull", "--all", "--rebase"], {cwd: this.repoRoot}));
|
||||
}
|
||||
}
|
||||
export default git;
|
@ -1,69 +0,0 @@
|
||||
import { CronJob } from "cron";
|
||||
export type Platform = "bedrock"|"java"|"pocketmine"|"spigot";
|
||||
export const PlatformArray = ["bedrock", "java", "pocketmine", "spigot"];
|
||||
|
||||
// Bds Session on declaretion function types
|
||||
export type bdsSessionCommands = {
|
||||
/** Exec any commands in server */
|
||||
execCommand: (...command: Array<string|number>) => bdsSessionCommands;
|
||||
/** Teleport player to Destination */
|
||||
tpPlayer: (player: string, x: number, y: number, z: number) => bdsSessionCommands;
|
||||
/** Change world gamemode */
|
||||
worldGamemode: (gamemode: "survival"|"creative"|"hardcore") => bdsSessionCommands;
|
||||
/** Change gamemode to specified player */
|
||||
userGamemode: (player: string, gamemode: "survival"|"creative"|"hardcore") => bdsSessionCommands;
|
||||
/** Stop Server */
|
||||
stop: () => Promise<number|null>;
|
||||
};
|
||||
export type startServerOptions = {
|
||||
/** Save only worlds/maps without server software - (Beta) */
|
||||
storageOnlyWorlds?: boolean;
|
||||
};
|
||||
export type playerAction1 = {player: string, Date: Date; xuid?: string|undefined}
|
||||
export type playerAction2 = playerAction1 & {action: "connect"|"disconnect"|"unknown"}
|
||||
|
||||
// Server events
|
||||
export type serverListen = {port: number; protocol?: "TCP"|"UDP"; version?: "IPv4"|"IPv6"|"IPv4/IPv6"};
|
||||
export type playerObject = {[player: string]: {action: "connect"|"disconnect"|"unknown"; date: Date; history: Array<{action: "connect"|"disconnect"|"unknown"; date: Date}>}};
|
||||
export interface serverOn {
|
||||
(act: "started", fn: (data: Date) => void);
|
||||
(act: "err", fn: (data: Error|number) => void);
|
||||
(act: "closed", fn: (data: number) => void);
|
||||
(act: "player_ban", fn: (data: playerAction1) => void);
|
||||
(act: "player", fn: (data: playerAction2) => void);
|
||||
(act: "player_connect", fn: (data: playerAction1) => void);
|
||||
(act: "player_disconnect", fn: (data: playerAction1) => void);
|
||||
(act: "player_unknown", fn: (data: playerAction1) => void);
|
||||
(act: "port_listen", fn: (data: {port: number; protocol?: "TCP"|"UDP"; version?: "IPv4"|"IPv6"|"IPv4/IPv6"}) => void);
|
||||
(act: "log", fn: (data: string) => void);
|
||||
(act: "log_stdout", fn: (data: string) => void);
|
||||
(act: "log_stderr", fn: (data: string) => void);
|
||||
}
|
||||
|
||||
// Type to Bds Session (All Platforms)
|
||||
export type BdsSession = {
|
||||
/** Server Session ID */
|
||||
id: string;
|
||||
logFile?: string;
|
||||
/** register cron job to create backups */
|
||||
creteBackup: (crontime: string|Date, option?: {type: "zip", pathStorage?: string}) => CronJob;
|
||||
/** Get server players historic connections */
|
||||
Player: playerObject;
|
||||
/** Get Server ports. listening. */
|
||||
ports: Array<serverListen>;
|
||||
/** if exists server map get world seed, fist map not get seed */
|
||||
seed?: string|number;
|
||||
/** Basic server functions. */
|
||||
commands: bdsSessionCommands;
|
||||
/** Server actions, example on avaible to connect or banned¹ */
|
||||
server: {
|
||||
/** Server actions */
|
||||
on: serverOn;
|
||||
/** Server actions */
|
||||
once: serverOn;
|
||||
/** Server Started date */
|
||||
startDate: Date;
|
||||
/** Server Started */
|
||||
started: boolean;
|
||||
};
|
||||
};
|
@ -1,5 +1 @@
|
||||
export * as globalType from "./globalType";
|
||||
export * as bedrock from "./bedrock/index";
|
||||
export * as pocketmine from "./pocketmine/index";
|
||||
export * as java from "./java/index";
|
||||
export * as spigot from "./spigot/index";
|
||||
export * as bedrock from "./bedrock";
|
@ -1,40 +0,0 @@
|
||||
import * as fsOld from "node:fs";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import admZip from "adm-zip";
|
||||
import { serverRoot } from "../pathControl";
|
||||
const javaPath = path.join(serverRoot, "java");
|
||||
|
||||
const filesFoldertoIgnore = ["Server.jar", "eula.txt", "libraries", "logs", "usercache.json", "versions"];
|
||||
|
||||
/**
|
||||
* Create backup for Worlds and Settings
|
||||
*/
|
||||
export async function CreateBackup(): Promise<Buffer> {
|
||||
if (!(fsOld.existsSync(javaPath))) throw new Error("Install server");
|
||||
const filesLint = (await fs.readdir(javaPath)).filter(file => !(filesFoldertoIgnore.some(folder => folder === file)));
|
||||
const zip = new admZip();
|
||||
for (const file of filesLint) {
|
||||
const filePath = path.join(javaPath, file);
|
||||
const stats = await fs.stat(filePath);
|
||||
if (stats.isSymbolicLink()) {
|
||||
const realPath = await fs.realpath(filePath);
|
||||
const realStats = await fs.stat(realPath);
|
||||
if (realStats.isDirectory()) zip.addLocalFolder(realPath, file);
|
||||
else zip.addLocalFile(realPath, file);
|
||||
} else if (stats.isDirectory()) zip.addLocalFolder(filePath);
|
||||
else zip.addLocalFile(filePath);
|
||||
}
|
||||
return zip.toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore backup for Worlds and Settings
|
||||
*
|
||||
* WARNING: This will overwrite existing files and World folder files
|
||||
*/
|
||||
export async function RestoreBackup(zipBuffer: Buffer): Promise<void> {
|
||||
const zip = new admZip(zipBuffer);
|
||||
await new Promise((resolve, reject) => zip.extractAllToAsync(javaPath, true, true, (err) => !!err ? reject(err) : resolve("")));
|
||||
return;
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import * as versionManeger from "@the-bds-maneger/server_versions";
|
||||
import * as httpRequests from "../lib/HttpRequests";
|
||||
import { serverRoot } from "../pathControl";
|
||||
|
||||
export async function download(version: string|boolean) {
|
||||
const ServerPath = path.join(serverRoot, "java");
|
||||
if (!(await fs.existsSync(ServerPath))) fs.mkdirSync(ServerPath, {recursive: true});
|
||||
if (!(await fs.existsSync(ServerPath))) fs.mkdirSync(ServerPath, {recursive: true});
|
||||
const javaInfo = await versionManeger.findUrlVersion("java", version);
|
||||
await fs.promises.writeFile(path.resolve(ServerPath, "Server.jar"), await httpRequests.getBuffer(String(javaInfo.url)));
|
||||
await fs.promises.writeFile(path.resolve(ServerPath, "eula.txt"), "eula=true");
|
||||
|
||||
// Return info
|
||||
return {
|
||||
version: javaInfo.version,
|
||||
publishDate: javaInfo.datePublish,
|
||||
url: javaInfo.url,
|
||||
};
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
export {download as DownloadServer} from "./download";
|
||||
export * as linkWorld from "./linkWorld";
|
||||
export * as server from "./server";
|
||||
export * as backup from "./backup";
|
@ -1,43 +0,0 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as fsOld from "node:fs";
|
||||
import * as path from "path";
|
||||
import { serverRoot, worldStorageRoot } from "../pathControl";
|
||||
const filesFoldertoIgnore = ["Server.jar", "eula.txt", "libraries", "logs", "usercache.json", "versions", "banned-ips.json", "banned-players.json", "ops.json", "server.properties", "whitelist.json"];
|
||||
|
||||
export async function linkWorld(): Promise<void> {
|
||||
const worldFolder = path.join(worldStorageRoot, "java");
|
||||
const javaFolder = path.join(serverRoot, "java");
|
||||
if (!fsOld.existsSync(javaFolder)) throw new Error("Server not installed")
|
||||
if (!fsOld.existsSync(worldFolder)) await fs.mkdir(worldFolder, {recursive: true});
|
||||
// From Worlds Folders
|
||||
for (const worldPath of await fs.readdir(worldFolder)) {
|
||||
const serverWorld = path.join(javaFolder, worldPath);
|
||||
const worldStorage = path.join(worldFolder, worldPath);
|
||||
if (fsOld.existsSync(serverWorld)) {
|
||||
if ((await fs.lstat(serverWorld)).isSymbolicLink()) continue;
|
||||
}
|
||||
try {
|
||||
await fs.cp(worldStorage, serverWorld, {recursive: true});
|
||||
await fs.rm(worldStorage, {recursive: true});
|
||||
await fs.symlink(worldStorage, serverWorld);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
continue
|
||||
}
|
||||
}
|
||||
// From Server folder
|
||||
for (const worldPath of (await fs.readdir(javaFolder)).filter(x => !filesFoldertoIgnore.includes(x))) {
|
||||
const serverWorld = path.join(worldFolder, worldPath);
|
||||
const worldStorage = path.join(javaFolder, worldPath);
|
||||
if ((await fs.lstat(worldStorage)).isSymbolicLink()) continue;
|
||||
try {
|
||||
await fs.cp(worldStorage, serverWorld, {recursive: true});
|
||||
await fs.rm(worldStorage, {recursive: true});
|
||||
await fs.symlink(serverWorld, worldStorage);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
continue
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
@ -1,137 +0,0 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import crypto from "crypto";
|
||||
import node_cron from "cron";
|
||||
import * as child_process from "../lib/childProcess";
|
||||
import { backupRoot, serverRoot } from "../pathControl";
|
||||
import { BdsSession, bdsSessionCommands } from '../globalType';
|
||||
import { CreateBackup } from "./backup";
|
||||
import events from "../lib/customEvents";
|
||||
import { linkWorld } from "./linkWorld";
|
||||
|
||||
const javaSesions: {[key: string]: BdsSession} = {};
|
||||
export function getSessions() {return javaSesions;}
|
||||
|
||||
const ServerPath = path.join(serverRoot, "java");
|
||||
export async function startServer(): Promise<BdsSession> {
|
||||
if (!(fs.existsSync(ServerPath))) throw new Error("Server dont instlled");
|
||||
if (process.env.AUTO_LINK_WORLD === "true" || process.env.AUTO_LINK_WORLDS === "1") await linkWorld();
|
||||
const SessionID = crypto.randomUUID();
|
||||
// Start Server
|
||||
const serverEvents = new events();
|
||||
const StartDate = new Date();
|
||||
const ServerProcess = await child_process.execServer({runOn: "host"}, "java", ["-jar", "Server.jar"], {cwd: ServerPath});
|
||||
// Log Server redirect to callbacks events and exit
|
||||
ServerProcess.on("out", data => serverEvents.emit("log_stdout", data));
|
||||
ServerProcess.on("err", data => serverEvents.emit("log_stderr", data));
|
||||
ServerProcess.on("all", data => serverEvents.emit("log", data));
|
||||
ServerProcess.Exec.on("exit", code => {
|
||||
serverEvents.emit("closed", code);
|
||||
if (code === null) serverEvents.emit("err", new Error("Server exited with code null"));
|
||||
});
|
||||
|
||||
// Detect server start
|
||||
serverEvents.on("log", lineData => {
|
||||
// [22:35:26] [Server thread/INFO]: Done (6.249s)! For help, type "help"
|
||||
if (/\[.*\].*\s+Done\s+\(.*\)\!.*/.test(lineData)) serverEvents.emit("started", new Date());
|
||||
});
|
||||
// Parse ports
|
||||
serverEvents.on("log", data => {
|
||||
const portParse = data.match(/Starting\s+Minecraft\s+server\s+on\s+(.*)\:(\d+)/);
|
||||
if (!!portParse) serverEvents.emit("port_listen", {port: parseInt(portParse[2]), protocol: "TCP", version: "IPv4/IPv6",});
|
||||
});
|
||||
|
||||
// Run Command
|
||||
const serverCommands: bdsSessionCommands = {
|
||||
/**
|
||||
* Run any commands in server.
|
||||
* @param command - Run any commands in server without parse commands
|
||||
* @returns - Server commands
|
||||
*/
|
||||
execCommand: (...command) => {
|
||||
ServerProcess.writelf(command.map(a => String(a)).join(" "));
|
||||
return serverCommands;
|
||||
},
|
||||
tpPlayer: (player: string, x: number, y: number, z: number) => {
|
||||
serverCommands.execCommand("tp", player, x, y, z);
|
||||
return serverCommands;
|
||||
},
|
||||
worldGamemode: (gamemode: "survival"|"creative"|"hardcore") => {
|
||||
serverCommands.execCommand("gamemode", gamemode);
|
||||
return serverCommands;
|
||||
},
|
||||
userGamemode: (player: string, gamemode: "survival"|"creative"|"hardcore") => {
|
||||
serverCommands.execCommand("gamemode", gamemode, player);
|
||||
return serverCommands;
|
||||
},
|
||||
stop: (): Promise<number|null> => {
|
||||
if (ServerProcess.Exec.exitCode !== null||ServerProcess.Exec.killed) return Promise.resolve(ServerProcess.Exec.exitCode);
|
||||
if (ServerProcess.Exec.killed) return Promise.resolve(ServerProcess.Exec.exitCode);
|
||||
ServerProcess.writelf("stop");
|
||||
return ServerProcess.onExit();
|
||||
}
|
||||
}
|
||||
|
||||
const backupCron = (crontime: string|Date, option?: {type: "zip", config?: {pathZip?: string}}): node_cron.CronJob => {
|
||||
// Validate Config
|
||||
if (option) {
|
||||
if (option.type === "zip") {}
|
||||
else option = {type: "zip"};
|
||||
}
|
||||
async function lockServerBackup() {
|
||||
serverCommands.execCommand("save hold");
|
||||
await new Promise(accept => setTimeout(accept, 1000));
|
||||
serverCommands.execCommand("save query");
|
||||
await new Promise(accept => setTimeout(accept, 1000));
|
||||
}
|
||||
async function unLockServerBackup() {
|
||||
serverCommands.execCommand("save resume");
|
||||
await new Promise(accept => setTimeout(accept, 1000));
|
||||
}
|
||||
if (!option) option = {type: "zip"};
|
||||
const CrontimeBackup = new node_cron.CronJob(crontime, async () => {
|
||||
if (option.type === "zip") {
|
||||
await lockServerBackup();
|
||||
if (!!option?.config?.pathZip) await CreateBackup().then(res => fs.promises.writeFile(path.resolve(backupRoot, option?.config?.pathZip), res)).catch(() => undefined);
|
||||
// else await createZipBackup(true).catch(() => undefined);
|
||||
await unLockServerBackup();
|
||||
}
|
||||
});
|
||||
CrontimeBackup.start();
|
||||
serverEvents.on("closed", () => CrontimeBackup.stop());
|
||||
return CrontimeBackup;
|
||||
}
|
||||
|
||||
// Session log
|
||||
const logFile = path.resolve(process.env.LOG_PATH||path.resolve(ServerPath, "../log"), `bedrock_${SessionID}.log`);
|
||||
if(!(fs.existsSync(path.parse(logFile).dir))) fs.mkdirSync(path.parse(logFile).dir, {recursive: true});
|
||||
const logStream = fs.createWriteStream(logFile, {flags: "w+"});
|
||||
logStream.write(`[${StartDate.toString()}] Server started\n\n`);
|
||||
ServerProcess.Exec.stdout.pipe(logStream);
|
||||
ServerProcess.Exec.stderr.pipe(logStream);
|
||||
|
||||
// Session Object
|
||||
const Seesion: BdsSession = {
|
||||
id: SessionID,
|
||||
creteBackup: backupCron,
|
||||
ports: [],
|
||||
Player: {},
|
||||
seed: undefined,
|
||||
commands: serverCommands,
|
||||
server: {
|
||||
on: (act, fn) => serverEvents.on(act, fn),
|
||||
once: (act, fn) => serverEvents.once(act, fn),
|
||||
started: false,
|
||||
startDate: StartDate
|
||||
}
|
||||
};
|
||||
|
||||
// Server Events
|
||||
serverEvents.on("port_listen", port => Seesion.ports.push(port));
|
||||
serverEvents.on("started", StartDate => {Seesion.server.started = true; Seesion.server.startDate = StartDate;});
|
||||
|
||||
// Return Session
|
||||
javaSesions[SessionID] = Seesion;
|
||||
serverEvents.on("closed", () => delete javaSesions[SessionID]);
|
||||
return Seesion;
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
import axios from "axios";
|
||||
|
||||
type githubRelease = {
|
||||
url: string;
|
||||
assets_url: string;
|
||||
upload_url: string;
|
||||
html_url: string;
|
||||
id: number;
|
||||
tarball_url: string;
|
||||
zipball_url: string;
|
||||
body: string;
|
||||
author: {
|
||||
login: string;
|
||||
id: number;
|
||||
node_id: string;
|
||||
avatar_url: string;
|
||||
gravatar_id: string;
|
||||
url: string;
|
||||
html_url: string;
|
||||
followers_url: string;
|
||||
following_url: string;
|
||||
gists_url: string;
|
||||
starred_url: string;
|
||||
subscriptions_url: string;
|
||||
organizations_url: string;
|
||||
repos_url: string;
|
||||
events_url: string;
|
||||
received_events_url: string;
|
||||
type: string;
|
||||
site_admin: boolean;
|
||||
};
|
||||
node_id: string;
|
||||
tag_name: string;
|
||||
target_commitish: string;
|
||||
name: string;
|
||||
draft: boolean;
|
||||
prerelease: boolean;
|
||||
created_at: string;
|
||||
published_at: string;
|
||||
assets: Array<{
|
||||
url: string;
|
||||
id: number;
|
||||
node_id: string;
|
||||
name: string;
|
||||
label: string;
|
||||
content_type: string;
|
||||
state: string;
|
||||
size: number;
|
||||
download_count: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
browser_download_url: string;
|
||||
uploader: {
|
||||
login: string;
|
||||
id: number;
|
||||
node_id: string;
|
||||
avatar_url: string;
|
||||
gravatar_id: string;
|
||||
url: string;
|
||||
html_url: string;
|
||||
followers_url: string;
|
||||
following_url: string;
|
||||
gists_url: string;
|
||||
starred_url: string;
|
||||
subscriptions_url: string;
|
||||
organizations_url: string;
|
||||
repos_url: string;
|
||||
events_url: string;
|
||||
received_events_url: string;
|
||||
type: string;
|
||||
site_admin: boolean;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
|
||||
export async function getBuffer(url: string, headers: {[d: string]: any} = {}): Promise<Buffer> {
|
||||
const dataReponse = await axios.get(url, {
|
||||
headers: (headers||{}),
|
||||
responseType: "arraybuffer",
|
||||
});
|
||||
return Buffer.from(dataReponse.data);
|
||||
}
|
||||
|
||||
export async function getGithubRelease(Username: string, Repo: string): Promise<Array<githubRelease>> {
|
||||
const data = await getBuffer(`https://api.github.com/repos/${Username}/${Repo}/releases`);
|
||||
return JSON.parse(data.toString("utf8"));
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
export default parse;
|
||||
|
||||
/**
|
||||
* Parse Proprieties files and return a map of properties.
|
||||
*
|
||||
* @param Proper - String with the properties or similar files
|
||||
* @returns
|
||||
*/
|
||||
export function parse(Proper: string): {[key: string]: string|number|true|false|null} {
|
||||
const ProPri = {};
|
||||
const ProperSplit = Proper.replace(/\r\n/g, "\n").replace(/\\\s+?\n/gi, "").split("\n").map(Line => Line.trim()).filter(line => /.*(\s+)?\=(\s+)?.*/.test(line) && !/^#/.test(line));
|
||||
for (const Line of ProperSplit) {
|
||||
const LineMatch = Line.match(/^([^\s\=]+)\s*\=(.*)$/);
|
||||
const key = LineMatch[1].trim(), value = LineMatch[2].trim();
|
||||
ProPri[key] = value;
|
||||
if (ProPri[key] === "") ProPri[key] = null;
|
||||
else if (ProPri[key] === "true") ProPri[key] = true;
|
||||
else if (ProPri[key] === "false") ProPri[key] = false;
|
||||
else if (/^[0-9]+\.[0-9]+/.test(ProPri[key]) && !/^[0-9]+\.[0-9]+\.[0-9]+/.test(ProPri[key])) ProPri[key] = parseFloat(ProPri[key]);
|
||||
else if (/^[0-9]+/.test(ProPri[key])) ProPri[key] = parseInt(ProPri[key]);
|
||||
}
|
||||
return ProPri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert json to properities files.
|
||||
*
|
||||
* @param ProPri - String with properties file
|
||||
* @returns
|
||||
*/
|
||||
export function stringify(ProPri: {[key: string]: any}): string {
|
||||
const Proper = [];
|
||||
for (const key in Object.keys(ProPri)) {
|
||||
if (ProPri[key] === null) Proper.push(`${key}=`);
|
||||
else if (ProPri[key] === true) Proper.push(`${key}=true`);
|
||||
else if (ProPri[key] === false) Proper.push(`${key}=false`);
|
||||
else if (typeof ProPri[key] === "number") Proper.push(`${key}=${ProPri[key]}`);
|
||||
else if (typeof ProPri[key] === "string") Proper.push(`${key}=${ProPri[key]}`);
|
||||
else if (typeof ProPri[key] === "object") Proper.push(`${key}=${JSON.stringify(ProPri[key])}`);
|
||||
else console.error(`[Proprieties.stringify] ${key} is not a valid type.`);
|
||||
}
|
||||
return Proper.join("\n");
|
||||
}
|
@ -1,114 +0,0 @@
|
||||
import child_process, { ChildProcess } from "child_process";
|
||||
import EventEmitter from "events";
|
||||
|
||||
export async function runAsync(command: string, args: Array<string|number>, options?: {env?: {[key: string]: string}, cwd?: string}): Promise<{stdout: string; stderr: string}> {
|
||||
if (!options) options = {};
|
||||
return await new Promise((resolve, reject) => {
|
||||
child_process.execFile(command, args.map(a => String(a)), {env: {...process.env, ...(options.env||{})}, cwd: options.cwd||process.cwd(), maxBuffer: Infinity}, (err, stdout, stderr) => {
|
||||
if (err) return reject(err);
|
||||
resolve({stdout, stderr});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function runCommandAsync(command: string, options?: {env?: {[key: string]: string}, cwd?: string}): Promise<{stdout: string; stderr: string}> {
|
||||
if (!options) options = {};
|
||||
return await new Promise((resolve, reject) => {
|
||||
child_process.exec(command, {env: {...process.env, ...(options.env||{})}, cwd: options.cwd||process.cwd(), maxBuffer: Infinity}, (err, stdout, stderr) => {
|
||||
if (err) return reject(err);
|
||||
resolve({stdout, stderr});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
type execOptions = {
|
||||
runOn: "docker";
|
||||
dockerVolumeName: string;
|
||||
dockerImage: string;
|
||||
dockerContainerName: string;
|
||||
}|{
|
||||
runOn: "host";
|
||||
};
|
||||
|
||||
export async function execServer(options: execOptions, command: string, args: Array<string|number>, execOption: {env?: {[key: string]: string}, cwd?: string}) {
|
||||
let Exec: ChildProcess;
|
||||
if (options.runOn === "docker") {
|
||||
const { dockerVolumeName, dockerImage, dockerContainerName } = options;
|
||||
if (!dockerVolumeName) throw new Error("Docker volume name is not defined");
|
||||
await runAsync("docker", ["volume", "create", dockerVolumeName]);
|
||||
const dockerArgs: Array<string> = ["run", "--name", dockerContainerName, "--rm", "-i", "--entrypoint=bash", "-e", "volumeMount"];
|
||||
if (!!execOption.cwd) dockerArgs.push("--workdir", execOption.cwd);
|
||||
dockerArgs.push("-v", `${dockerVolumeName}:/data`);
|
||||
if (!!execOption.env) {
|
||||
for (const key in Object.keys(execOption.env)) dockerArgs.push("-e", String(key));
|
||||
}
|
||||
dockerArgs.push(dockerImage);
|
||||
dockerArgs.push(command, ...args.map(a => String(a)));
|
||||
Exec = child_process.execFile("docker", dockerArgs, {
|
||||
env: {...process.env, ...(execOption.env||{}), volumeMount: "/data"},
|
||||
maxBuffer: Infinity
|
||||
});
|
||||
} else if (options.runOn === "host") {
|
||||
Exec = child_process.execFile(command, args.map(a => String(a)), {
|
||||
env: {...process.env, ...(execOption.env||{})},
|
||||
cwd: execOption.cwd||process.cwd(),
|
||||
maxBuffer: Infinity
|
||||
});
|
||||
} else throw new Error("Unknown runOn");
|
||||
// server exec functions
|
||||
const execEvent = new EventEmitter();
|
||||
/** log data event */
|
||||
const on = (eventName: "out"|"err"|"all", call: (data: string) => void) => execEvent.on(eventName, call);
|
||||
/** log data event */
|
||||
const once = (eventName: "out"|"err"|"all", call: (data: string) => void) => execEvent.once(eventName, call);
|
||||
/** on server exit is event activate */
|
||||
const onExit = (): Promise<number> => {
|
||||
if (Exec.killed) {
|
||||
if (Exec.exitCode === 0) return Promise.resolve(0);
|
||||
return Promise.reject(Exec.exitCode === null ? 137:Exec.exitCode);
|
||||
}
|
||||
return new Promise<number>((res, rej) => Exec.on("exit", code => {
|
||||
if (code === 0) return res(0);
|
||||
return rej(code === null ? 137 : code);
|
||||
}));
|
||||
}
|
||||
|
||||
// Storage tmp lines
|
||||
const tempLog = {out: "", err: ""};
|
||||
const parseLog = (to: "out"|"err", data: string) => {
|
||||
// Detect new line and get all line with storage line for run callback else storage line
|
||||
let lines = data.split(/\r?\n/);
|
||||
// if (lines[lines.length - 1] === "") lines.pop();
|
||||
if (lines.length === 1) tempLog[to] += lines[0];
|
||||
else {
|
||||
for (const line of lines.slice(0, -1)) {
|
||||
if (!!tempLog[to]) {
|
||||
execEvent.emit(to, tempLog[to]+line);
|
||||
execEvent.emit("all", tempLog[to]+line);
|
||||
tempLog[to] = "";
|
||||
} else {
|
||||
execEvent.emit(to, line);
|
||||
execEvent.emit("all", line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Exec.stdout.on("data", data => parseLog("out", data));
|
||||
Exec.stderr.on("data", data => parseLog("err", data));
|
||||
|
||||
// Return
|
||||
return {
|
||||
on,
|
||||
once,
|
||||
onExit,
|
||||
writelf: (data: string|number|Array<string|number>) => {
|
||||
if (typeof data === "string") Exec.stdin.write(data+"\n");
|
||||
else if (Array.isArray(data)) {
|
||||
if (data.length === 0) return;
|
||||
else if (data.length === 1) Exec.stdin.write(data[0]+"\n");
|
||||
else data.forEach(d => Exec.stdin.write(d+"\n"));
|
||||
}
|
||||
},
|
||||
Exec
|
||||
};
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
import events from "node:events";
|
||||
import { playerAction1, playerAction2 } from '../globalType';
|
||||
|
||||
export declare interface bdsServerEvent {
|
||||
emit(act: "started", data: Date): boolean;
|
||||
once(act: "started", fn: (data: Date) => void): this;
|
||||
on(act: "started", fn: (data: Date) => void): this;
|
||||
|
||||
emit(act: "err", data: Error|number): boolean;
|
||||
on(act: "err", fn: (data: Error|number) => void): this;
|
||||
once(act: "err", fn: (data: Error|number) => void): this;
|
||||
|
||||
emit(act: "closed", data: number): boolean;
|
||||
once(act: "closed", fn: (data: number) => void): this;
|
||||
on(act: "closed", fn: (data: number) => void): this;
|
||||
|
||||
emit(act: "port_listen", data: {port: number; protocol?: "TCP"|"UDP"; version?: "IPv4"|"IPv6"|"IPv4/IPv6"}): boolean;
|
||||
once(act: "port_listen", fn: (data: {port: number; protocol?: "TCP"|"UDP"; version?: "IPv4"|"IPv6"|"IPv4/IPv6"}) => void): this;
|
||||
on(act: "port_listen", fn: (data: {port: number; protocol?: "TCP"|"UDP"; version?: "IPv4"|"IPv6"|"IPv4/IPv6"}) => void): this;
|
||||
|
||||
emit(act: "log", data: string): boolean;
|
||||
once(act: "log", fn: (data: string) => void): this;
|
||||
on(act: "log", fn: (data: string) => void): this;
|
||||
|
||||
emit(act: "log_stdout", data: string): boolean;
|
||||
once(act: "log_stdout", fn: (data: string) => void): this;
|
||||
on(act: "log_stdout", fn: (data: string) => void): this;
|
||||
|
||||
emit(act: "log_stderr", data: string): boolean;
|
||||
once(act: "log_stderr", fn: (data: string) => void): this;
|
||||
on(act: "log_stderr", fn: (data: string) => void): this;
|
||||
|
||||
emit(act: "player", data: playerAction2): boolean;
|
||||
once(act: "player", fn: (data: playerAction2) => void): this;
|
||||
on(act: "player", fn: (data: playerAction2) => void): this;
|
||||
|
||||
emit(act: "player_ban", data: playerAction1): boolean;
|
||||
once(act: "player_ban", fn: (data: playerAction1) => void): this;
|
||||
on(act: "player_ban", fn: (data: playerAction1) => void): this;
|
||||
|
||||
emit(act: "player_connect", data: playerAction1): boolean;
|
||||
once(act: "player_connect", fn: (data: playerAction1) => void): this;
|
||||
on(act: "player_connect", fn: (data: playerAction1) => void): this;
|
||||
|
||||
emit(act: "player_disconnect", data: playerAction1): boolean;
|
||||
once(act: "player_disconnect", fn: (data: playerAction1) => void): this;
|
||||
on(act: "player_disconnect", fn: (data: playerAction1) => void): this;
|
||||
|
||||
emit(act: "player_unknown", data: playerAction1): boolean;
|
||||
once(act: "player_unknown", fn: (data: playerAction1) => void): this;
|
||||
on(act: "player_unknown", fn: (data: playerAction1) => void): this;
|
||||
}
|
||||
|
||||
export class bdsServerEvent extends events {}
|
||||
export default bdsServerEvent;
|
@ -1,33 +0,0 @@
|
||||
import { promises as fsPromise } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export default async function Readdir(pathRead: string, filter?: Array<RegExp>) {
|
||||
if (!filter) filter = [/.*/];
|
||||
const fixedPath = path.resolve(pathRead);
|
||||
const files: Array<{
|
||||
path: string,
|
||||
name: string
|
||||
}> = [];
|
||||
for (const file of await fsPromise.readdir(fixedPath)) {
|
||||
const FullFilePath = path.join(fixedPath, file);
|
||||
const stats = await fsPromise.stat(FullFilePath);
|
||||
if (stats.isDirectory()) files.push(...(await Readdir(FullFilePath, filter)));
|
||||
else if (stats.isSymbolicLink()) {
|
||||
const realPath = await fsPromise.realpath(FullFilePath);
|
||||
const statsSys = await fsPromise.stat(realPath);
|
||||
if (statsSys.isDirectory()) files.push(...(await Readdir(realPath, filter)));
|
||||
else {
|
||||
if (filter.length === 0||filter.some(x => x.test(realPath))) files.push({
|
||||
path: FullFilePath,
|
||||
name: path.basename(FullFilePath)
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (filter.length === 0||filter.some(x => x.test(FullFilePath))) files.push({
|
||||
path: FullFilePath,
|
||||
name: path.basename(FullFilePath)
|
||||
});
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
export default function parse(ignoreFile: string, filter?: Array<string>) {
|
||||
ignoreFile = ignoreFile.replace(/\r\n/g, "\n").replace(/#.*\n?/gim, "").replace(/^\n/g, "").replace(/\*\*/g, "(.+)").replace(/\*/g, "([^\\/]+)");
|
||||
const allow = (([...ignoreFile.matchAll(/!.*\n?/g)])||[]).filter(x => !!x);
|
||||
ignoreFile = ignoreFile.replace(/!.*\n?/gim, "").replace(/^\n/g, "");
|
||||
const ignore = ignoreFile.split(/\n/g);
|
||||
const objIngore = {
|
||||
allow: allow.length > 0 ? new RegExp("^((" + allow.join(")|(") + "))") : new RegExp("$^"),
|
||||
ignore: ignore.length > 0 ? new RegExp("^((" + ignore.join(")|(") + "))") : new RegExp("$^"),
|
||||
};
|
||||
if (!filter) return objIngore;
|
||||
else return filter.filter(x => objIngore.allow.test(x) && !objIngore.ignore.test(x));
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import * as net from "node:net";
|
||||
|
||||
export default async function portIsAllocated(port: number): Promise<boolean> {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
const tester = net.createServer()
|
||||
tester.once("error", () => () => resolve(true));
|
||||
tester.once("listening", () => {
|
||||
tester.once("close", () => resolve(false));
|
||||
tester.close();
|
||||
});
|
||||
tester.listen(port);
|
||||
});
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
|
||||
const bdsCorePathHome = path.join(os.homedir(), "bds_core");
|
||||
export const serverRoot = (!!process.env.SERVER_PATH) ? path.resolve(process.env.SERVER_PATH) : path.join(bdsCorePathHome, "servers");
|
||||
export const backupRoot = (!!process.env.BACKUP_PATH) ? path.resolve(process.env.BACKUP_PATH) : path.join(bdsCorePathHome, "backups");
|
||||
export const worldStorageRoot = (!!process.env.WORLD_STORAGE) ? path.resolve(process.env.WORLD_STORAGE) : path.join(bdsCorePathHome, "worlds");
|
@ -1,39 +0,0 @@
|
||||
import * as httpRequest from "../lib/HttpRequests";
|
||||
|
||||
export async function getPlugins(): Promise<Array<{
|
||||
id: number,
|
||||
name: string,
|
||||
version: string,
|
||||
html_url: string,
|
||||
tagline: string,
|
||||
artifact_url: string,
|
||||
downloads: number,
|
||||
score: number,
|
||||
repo_id: number,
|
||||
repo_name: string,
|
||||
project_id: number,
|
||||
project_name: string,
|
||||
build_id: number,
|
||||
build_number: number,
|
||||
build_commit: string,
|
||||
description_url: string,
|
||||
icon_url: string,
|
||||
changelog_url: string,
|
||||
license: string,
|
||||
license_url: null,
|
||||
is_obsolete: false,
|
||||
is_pre_release: false,
|
||||
is_outdated: false,
|
||||
is_official: false,
|
||||
submission_date: number,
|
||||
state: number,
|
||||
last_state_change_date: number,
|
||||
categories: Array<{ major: true|false, category_name: string }>,
|
||||
keywords: Array<string>,
|
||||
api: Array<{from: string}>,
|
||||
deps: Array<any>,
|
||||
producers: {Collaborator: Array<string>},
|
||||
state_name: string
|
||||
}>> {
|
||||
return await httpRequest.getBuffer("https://poggit.pmmp.io/plugins.json").then(async res => JSON.parse(await res.toString("utf8")))
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
import * as fsOld from "node:fs";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import admZip from "adm-zip";
|
||||
import { serverRoot } from '../pathControl';
|
||||
const javaPath = path.join(serverRoot, "pocketmine");
|
||||
|
||||
const filesFoldertoIgnore = ["PocketMine.phar", "bin", "server.log"];
|
||||
/**
|
||||
* Create backup for Worlds and Settings
|
||||
*/
|
||||
export async function CreateBackup(): Promise<Buffer> {
|
||||
if (!(fsOld.existsSync(javaPath))) throw new Error("Install server");
|
||||
const filesLint = (await fs.readdir(javaPath)).filter(file => !(filesFoldertoIgnore.some(folder => folder === file)));
|
||||
const zip = new admZip();
|
||||
for (const file of filesLint) {
|
||||
const filePath = path.join(javaPath, file);
|
||||
const stats = await fs.stat(filePath);
|
||||
if (stats.isSymbolicLink()) {
|
||||
const realPath = await fs.realpath(filePath);
|
||||
const realStats = await fs.stat(realPath);
|
||||
if (realStats.isDirectory()) zip.addLocalFolder(realPath, file);
|
||||
else zip.addLocalFile(realPath, file);
|
||||
} else if (stats.isDirectory()) zip.addLocalFolder(filePath);
|
||||
else zip.addLocalFile(filePath);
|
||||
}
|
||||
return zip.toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore backup for Worlds and Settings
|
||||
*
|
||||
* WARNING: This will overwrite existing files and World folder files
|
||||
*/
|
||||
export async function RestoreBackup(zipBuffer: Buffer): Promise<void> {
|
||||
const zip = new admZip(zipBuffer);
|
||||
await new Promise((resolve, reject) => zip.extractAllToAsync(javaPath, true, true, (err) => !!err ? reject(err) : resolve("")));
|
||||
return;
|
||||
}
|
@ -1,420 +0,0 @@
|
||||
import path from "node:path";
|
||||
import { promises as fsPromise } from "node:fs";
|
||||
import { serverRoot } from "../pathControl";
|
||||
const serverPath = path.join(serverRoot, "pocketmine");
|
||||
/*
|
||||
#Properties Config file
|
||||
#Wed Apr 20 23:32:32 UTC 2022
|
||||
language=eng
|
||||
motd=PocketMine-MP Server
|
||||
server-name=PocketMine-MP Server
|
||||
server-port=19132
|
||||
server-portv6=19133
|
||||
gamemode=survival
|
||||
max-players=20
|
||||
view-distance=16
|
||||
white-list=on
|
||||
enable-query=on
|
||||
enable-ipv6=on
|
||||
force-gamemode=off
|
||||
hardcore=off
|
||||
pvp=on
|
||||
difficulty=2
|
||||
generator-settings=
|
||||
level-name=world
|
||||
level-seed=
|
||||
level-type=DEFAULT
|
||||
auto-save=on
|
||||
xbox-auth=on
|
||||
*/
|
||||
|
||||
export type pocketmineConfig = {
|
||||
language: "chs"|"deu"|"ell"|"eng"|"fra"|"hrv"|"jpn"|"kor"|"lav"|"nld",
|
||||
motd: string,
|
||||
port: {
|
||||
v4: number,
|
||||
v6: number
|
||||
},
|
||||
whiteList: boolean,
|
||||
maxPlayers: number,
|
||||
gamemode: "survival"|"creative"|"hardcore",
|
||||
forceGamemode: boolean,
|
||||
pvp: boolean,
|
||||
difficulty: "peaceful"|"easy"|"normal"|"hard",
|
||||
worldName: string,
|
||||
worldSeed: string,
|
||||
worldType: "default"|"flat",
|
||||
xboxAuth: boolean
|
||||
}
|
||||
|
||||
export async function CreateServerConfig(config: pocketmineConfig) {
|
||||
const lang = config.language||"eng";
|
||||
const serverMotd = config.motd||"PocketMine-MP Server";
|
||||
const serverPortv4 = config.port.v4||19132;
|
||||
const serverPortv6 = config.port.v6||19133;
|
||||
const gamemode = config.gamemode||"survival";
|
||||
const maxPlayers = config.maxPlayers||20;
|
||||
const viewDistance = 16;
|
||||
const whiteList = (config.whiteList||false)?"on":"off";
|
||||
const enableQuery = (false)?"on":"off";
|
||||
const enableIPv6 = (true)? "on":"off";
|
||||
const forceGamemode = (true)?"on":"off";
|
||||
const hardcore = (gamemode === "hardcore")?"on":"off";
|
||||
const pvp = (config.pvp||true)?"on":"off";
|
||||
const difficulty = config.difficulty||"normal";
|
||||
const generatorSettings = "";
|
||||
const levelName = config.worldName||"world";
|
||||
const levelSeed = config.worldSeed||"";
|
||||
const levelType = config.worldType||"default";
|
||||
const autoSave = (true)?"on":"off";
|
||||
const xboxAuth = (config.xboxAuth||false)?"on":"off";
|
||||
const configPath = path.join(serverPath, "server.properties");
|
||||
const configContent = [
|
||||
`language=${lang}`,
|
||||
`motd=${serverMotd}`,
|
||||
`server-port=${serverPortv4}`,
|
||||
`server-portv6=${serverPortv6}`,
|
||||
`gamemode=${gamemode}`,
|
||||
`max-players=${maxPlayers}`,
|
||||
`view-distance=${viewDistance}`,
|
||||
`white-list=${whiteList}`,
|
||||
`enable-query=${enableQuery}`,
|
||||
`enable-ipv6=${enableIPv6}`,
|
||||
`force-gamemode=${forceGamemode}`,
|
||||
`hardcore=${hardcore}`,
|
||||
`pvp=${pvp}`,
|
||||
`difficulty=${difficulty}`,
|
||||
`generator-settings=${generatorSettings}`,
|
||||
`level-name=${levelName}`,
|
||||
`level-seed=${levelSeed}`,
|
||||
`level-type=${levelType}`,
|
||||
`auto-save=${autoSave}`,
|
||||
`xbox-auth=${xboxAuth}`
|
||||
];
|
||||
await fsPromise.writeFile(configPath, configContent.join("\n"));
|
||||
return {lang, serverMotd, serverPortv4, serverPortv6, gamemode, maxPlayers, viewDistance, whiteList, enableQuery, enableIPv6, forceGamemode, hardcore, pvp, difficulty, generatorSettings, levelName, levelSeed, levelType, autoSave, xboxAuth};
|
||||
}
|
||||
|
||||
// new config in to pocketmine.yml
|
||||
// Example
|
||||
// TODO: yaml lang parse with js-yaml
|
||||
/*
|
||||
# Main configuration file for PocketMine-MP
|
||||
# These settings are the ones that cannot be included in server.properties
|
||||
# Some of these settings are safe, others can break your server if modified incorrectly
|
||||
# New settings/defaults won't appear automatically in this file when upgrading.
|
||||
|
||||
settings:
|
||||
#Whether to send all strings translated to server locale or let the device handle them
|
||||
force-language: false
|
||||
shutdown-message: "Server closed"
|
||||
#Allow listing plugins via Query
|
||||
query-plugins: true
|
||||
#Enable plugin and core profiling by default
|
||||
enable-profiling: false
|
||||
#Will only add results when tick measurement is below or equal to given value (default 20)
|
||||
profile-report-trigger: 20
|
||||
#Number of AsyncTask workers.
|
||||
#Used for plugin asynchronous tasks, world generation, compression and web communication.
|
||||
#Set this approximately to your number of cores.
|
||||
#If set to auto, it'll try to detect the number of cores (or use 2)
|
||||
async-workers: auto
|
||||
#Whether to allow running development builds. Dev builds might crash, break your plugins, corrupt your world and more.
|
||||
#It is recommended to avoid using development builds where possible.
|
||||
enable-dev-builds: false
|
||||
|
||||
memory:
|
||||
#Global soft memory limit in megabytes. Set to 0 to disable
|
||||
#This will trigger low-memory-triggers and fire an event to free memory when the usage goes over this
|
||||
global-limit: 0
|
||||
|
||||
#Main thread soft memory limit in megabytes. Set to 0 to disable
|
||||
#This will trigger low-memory-triggers and fire an event to free memory when the usage goes over this
|
||||
main-limit: 0
|
||||
|
||||
#Main thread hard memory limit in megabytes. Set to 0 to disable
|
||||
#This will stop the server when the limit is surpassed
|
||||
main-hard-limit: 1024
|
||||
|
||||
#AsyncWorker threads' hard memory limit in megabytes. Set to 0 to disable
|
||||
#This will crash the task currently executing on the worker if the task exceeds the limit
|
||||
#NOTE: THIS LIMIT APPLIES PER WORKER, NOT TO THE WHOLE PROCESS.
|
||||
async-worker-hard-limit: 256
|
||||
|
||||
#Period in ticks to check memory (default 1 second)
|
||||
check-rate: 20
|
||||
|
||||
#Continue firing low-memory-triggers and event while on low memory
|
||||
continuous-trigger: true
|
||||
|
||||
#Only if memory.continuous-trigger is enabled. Specifies the rate in memory.check-rate steps (default 30 seconds)
|
||||
continuous-trigger-rate: 30
|
||||
|
||||
garbage-collection:
|
||||
#Period in ticks to fire the garbage collector manually (default 30 minutes), set to 0 to disable
|
||||
#This only affects the main thread. Other threads should fire their own collections
|
||||
period: 36000
|
||||
|
||||
#Fire asynchronous tasks to collect garbage from workers
|
||||
collect-async-worker: true
|
||||
|
||||
#Trigger on low memory
|
||||
low-memory-trigger: true
|
||||
|
||||
#Settings controlling memory dump handling.
|
||||
memory-dump:
|
||||
#Dump memory from async workers as well as the main thread. If you have issues with segfaults when dumping memory, disable this setting.
|
||||
dump-async-worker: true
|
||||
|
||||
max-chunks:
|
||||
#Cap maximum render distance per player when low memory is triggered. Set to 0 to disable cap.
|
||||
chunk-radius: 4
|
||||
|
||||
#Do chunk garbage collection on trigger
|
||||
trigger-chunk-collect: true
|
||||
|
||||
world-caches:
|
||||
#Disallow adding to world chunk-packet caches when memory is low
|
||||
disable-chunk-cache: true
|
||||
#Clear world caches when memory is low
|
||||
low-memory-trigger: true
|
||||
|
||||
|
||||
network:
|
||||
#Threshold for batching packets, in bytes. Only these packets will be compressed
|
||||
#Set to 0 to compress everything, -1 to disable.
|
||||
batch-threshold: 256
|
||||
#Compression level used when sending batched packets. Higher = more CPU, less bandwidth usage
|
||||
compression-level: 6
|
||||
#Use AsyncTasks for compression. Adds half/one tick delay, less CPU load on main thread
|
||||
async-compression: false
|
||||
#Experimental. Use UPnP to automatically port forward
|
||||
upnp-forwarding: false
|
||||
#Maximum size in bytes of packets sent over the network (default 1492 bytes). Packets larger than this will be
|
||||
#fragmented or split into smaller parts. Clients can request MTU sizes up to but not more than this number.
|
||||
max-mtu-size: 1492
|
||||
#Enable encryption of Minecraft network traffic. This has an impact on performance, but prevents hackers from stealing sessions and pretending to be other players.
|
||||
#DO NOT DISABLE THIS unless you understand the risks involved.
|
||||
enable-encryption: true
|
||||
|
||||
debug:
|
||||
#If > 1, it will show debug messages in the console
|
||||
level: 1
|
||||
|
||||
player:
|
||||
#Choose whether to enable player data saving.
|
||||
save-player-data: true
|
||||
#If true, checks that joining players' Xbox user ID (XUID) match what was previously recorded.
|
||||
#This also prevents non-XBL players using XBL players' usernames to steal their data on servers with xbox-auth=off.
|
||||
verify-xuid: true
|
||||
|
||||
level-settings:
|
||||
#The default format that worlds will use when created
|
||||
default-format: leveldb
|
||||
|
||||
chunk-sending:
|
||||
#To change server normal render distance, change view-distance in server.properties.
|
||||
#Amount of chunks sent to players per tick
|
||||
per-tick: 4
|
||||
#Radius of chunks that need to be sent before spawning the player
|
||||
spawn-radius: 4
|
||||
|
||||
chunk-ticking:
|
||||
#Max amount of chunks processed each tick
|
||||
per-tick: 40
|
||||
#Radius of chunks around a player to tick
|
||||
tick-radius: 3
|
||||
#Number of blocks inside ticking areas' subchunks that get ticked every tick. Higher values will accelerate events
|
||||
#like tree and plant growth, but at a higher performance cost.
|
||||
blocks-per-subchunk-per-tick: 3
|
||||
#IDs of blocks not to perform random ticking on.
|
||||
disable-block-ticking:
|
||||
#- grass
|
||||
#- ice
|
||||
#- fire
|
||||
|
||||
chunk-generation:
|
||||
#Max. amount of chunks in the waiting queue to be populated
|
||||
population-queue-size: 32
|
||||
|
||||
ticks-per:
|
||||
autosave: 6000
|
||||
|
||||
auto-report:
|
||||
#Send crash reports for processing
|
||||
enabled: true
|
||||
send-code: true
|
||||
send-settings: true
|
||||
send-phpinfo: false
|
||||
use-https: true
|
||||
host: crash.pmmp.io
|
||||
|
||||
anonymous-statistics:
|
||||
#Sends anonymous statistics for data aggregation, plugin usage tracking
|
||||
enabled: false #TODO: re-enable this when we have a new stats host
|
||||
host: stats.pocketmine.net
|
||||
|
||||
auto-updater:
|
||||
enabled: true
|
||||
on-update:
|
||||
warn-console: true
|
||||
#Can be development, alpha, beta or stable.
|
||||
preferred-channel: stable
|
||||
#If using a development version, it will suggest changing the channel
|
||||
suggest-channels: true
|
||||
host: update.pmmp.io
|
||||
|
||||
timings:
|
||||
#Choose the host to use for viewing your timings results.
|
||||
host: timings.pmmp.io
|
||||
|
||||
console:
|
||||
#Choose whether to enable server stats reporting on the console title.
|
||||
#NOTE: The title ticker will be disabled regardless if console colours are not enabled.
|
||||
title-tick: true
|
||||
|
||||
aliases:
|
||||
##This section allows you to add, remove or remap command aliases.
|
||||
##A single alias can call one or more other commands (or aliases).
|
||||
##Aliases defined here will override any command aliases declared by plugins or PocketMine-MP itself.
|
||||
|
||||
##To remove an alias, set it to [], like so (note that prefixed aliases like "pocketmine:stop" will remain and can't
|
||||
##be removed):
|
||||
#stop: []
|
||||
|
||||
##Commands are not removed, only their aliases. You can still refer to a command using its full (prefixed)
|
||||
##name, even if all its aliases are overwritten. The full name is usually something like "pocketmine:commandname" or
|
||||
##"pluginname:commandname".
|
||||
#abort: [pocketmine:stop]
|
||||
|
||||
##To add an alias, list the command(s) that it calls:
|
||||
#showtheversion: [version]
|
||||
#savestop: [save-all, stop]
|
||||
|
||||
##To invoke another command with arguments, use $1 to pass the first argument, $2 for the second etc:
|
||||
#giveadmin: [op $1] ## `giveadmin alex` -> `op alex`
|
||||
#kill: [suicide, say "I tried to kill $1"] ## `kill alex` -> `suicide` + `say "I tried to kill alex"`
|
||||
#giverandom: [give $1 $2, say "Someone has just received a $2!"] ## `giverandom alex diamond` -> `give alex diamond` + `say "Someone has just received a diamond!"`
|
||||
|
||||
##To change an existing command alias and make it do something else:
|
||||
#tp: [suicide]
|
||||
|
||||
worlds:
|
||||
#These settings will override the generator set in server.properties and allows loading multiple worlds
|
||||
#Example:
|
||||
#world:
|
||||
# seed: 404
|
||||
# generator: FLAT
|
||||
# preset: 2;bedrock,59xstone,3xdirt,grass;1
|
||||
|
||||
plugins:
|
||||
#Setting this to true will cause the legacy structure to be used where plugin data is placed inside the --plugins dir.
|
||||
#False will place plugin data under plugin_data under --data.
|
||||
#This option exists for backwards compatibility with existing installations.
|
||||
legacy-data-dir: false
|
||||
*/
|
||||
// TODO: in json
|
||||
/*
|
||||
{
|
||||
"settings": {
|
||||
"force-language": false,
|
||||
"shutdown-message": "Server closed",
|
||||
"query-plugins": true,
|
||||
"enable-profiling": false,
|
||||
"profile-report-trigger": 20,
|
||||
"async-workers": "auto",
|
||||
"enable-dev-builds": false
|
||||
},
|
||||
"memory": {
|
||||
"global-limit": 0,
|
||||
"main-limit": 0,
|
||||
"main-hard-limit": 1024,
|
||||
"async-worker-hard-limit": 256,
|
||||
"check-rate": 20,
|
||||
"continuous-trigger": true,
|
||||
"continuous-trigger-rate": 30,
|
||||
"garbage-collection": {
|
||||
"period": 36000,
|
||||
"collect-async-worker": true,
|
||||
"low-memory-trigger": true
|
||||
},
|
||||
"memory-dump": {
|
||||
"dump-async-worker": true
|
||||
},
|
||||
"max-chunks": {
|
||||
"chunk-radius": 4,
|
||||
"trigger-chunk-collect": true
|
||||
},
|
||||
"world-caches": {
|
||||
"disable-chunk-cache": true,
|
||||
"low-memory-trigger": true
|
||||
}
|
||||
},
|
||||
"network": {
|
||||
"batch-threshold": 256,
|
||||
"compression-level": 6,
|
||||
"async-compression": false,
|
||||
"upnp-forwarding": false,
|
||||
"max-mtu-size": 1492,
|
||||
"enable-encryption": true
|
||||
},
|
||||
"debug": {
|
||||
"level": 1
|
||||
},
|
||||
"player": {
|
||||
"save-player-data": true,
|
||||
"verify-xuid": true
|
||||
},
|
||||
"level-settings": {
|
||||
"default-format": "leveldb"
|
||||
},
|
||||
"chunk-sending": {
|
||||
"per-tick": 4,
|
||||
"spawn-radius": 4
|
||||
},
|
||||
"chunk-ticking": {
|
||||
"per-tick": 40,
|
||||
"tick-radius": 3,
|
||||
"blocks-per-subchunk-per-tick": 3,
|
||||
"disable-block-ticking": null
|
||||
},
|
||||
"chunk-generation": {
|
||||
"population-queue-size": 32
|
||||
},
|
||||
"ticks-per": {
|
||||
"autosave": 6000
|
||||
},
|
||||
"auto-report": {
|
||||
"enabled": true,
|
||||
"send-code": true,
|
||||
"send-settings": true,
|
||||
"send-phpinfo": false,
|
||||
"use-https": true,
|
||||
"host": "crash.pmmp.io"
|
||||
},
|
||||
"anonymous-statistics": {
|
||||
"enabled": false,
|
||||
"host": "stats.pocketmine.net"
|
||||
},
|
||||
"auto-updater": {
|
||||
"enabled": true,
|
||||
"on-update": {
|
||||
"warn-console": true
|
||||
},
|
||||
"preferred-channel": "stable",
|
||||
"suggest-channels": true,
|
||||
"host": "update.pmmp.io"
|
||||
},
|
||||
"timings": {
|
||||
"host": "timings.pmmp.io"
|
||||
},
|
||||
"console": {
|
||||
"title-tick": true
|
||||
},
|
||||
"aliases": null,
|
||||
"worlds": null,
|
||||
"plugins": {
|
||||
"legacy-data-dir": false
|
||||
}
|
||||
}
|
||||
*/
|
@ -1,90 +0,0 @@
|
||||
import * as path from "node:path";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as child_process from "node:child_process";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import adm_zip from "adm-zip";
|
||||
import tar from "tar";
|
||||
import Readdirrec from "../lib/listRecursive";
|
||||
import * as versionManeger from "@the-bds-maneger/server_versions";
|
||||
import * as httpRequests from "../lib/HttpRequests";
|
||||
import { serverRoot } from "../pathControl";
|
||||
|
||||
export async function buildLocal(serverPath: string) {
|
||||
if (process.platform === "win32") throw new Error("Current only to unix support");
|
||||
const randomFolder = path.join(os.tmpdir(), "bdscore_php_"+randomBytes(8).toString("hex"));
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
child_process.execFile("git", ["clone", "--depth", "1", "https://github.com/pmmp/php-build-scripts.git", randomFolder], (err) => {
|
||||
if (!!err) return reject(err);
|
||||
const cpuCores = os.cpus().length * 4||2;
|
||||
const compiler = child_process.execFile("./compile.sh", ["-j"+cpuCores], {cwd: randomFolder}, err2 => {
|
||||
if (!!err2) return reject(err2);
|
||||
resolve();
|
||||
});
|
||||
compiler.stdout.on("data", data => process.stdout.write(data));
|
||||
compiler.stderr.on("data", data => process.stdout.write(data));
|
||||
});
|
||||
});
|
||||
await Readdirrec(path.join(randomFolder, "bin/php7")).then(files => files.map(file => {
|
||||
console.log("Move '%s' to PHP Folder", file.path);
|
||||
return fs.promises.cp(file.path, path.join(file.path.replace(path.join(randomFolder, "bin/php7"), serverPath)));
|
||||
})).then(res => Promise.all(res));
|
||||
}
|
||||
|
||||
async function InstallPrebuildPHP(serverPath: string) {
|
||||
const nameTest = (name: string) => (process.platform === "win32" ? /\.zip/:/\.tar\.gz/).test(name) && RegExp(process.platform).test(name) && RegExp(process.arch).test(name);
|
||||
const Release = (await httpRequests.getGithubRelease("The-Bds-Maneger", "PocketMinePHPAutoBinBuilds")).map(release => {
|
||||
release.assets = release.assets.filter(asset => nameTest(asset.name));
|
||||
return release;
|
||||
}).filter(res => res.assets.length >= 1);
|
||||
if (Release.length === 0) throw new Error("No file found for this Platform and Arch");
|
||||
const urlBin = Release[0].assets[0].browser_download_url;
|
||||
if (!urlBin) throw new Error("No file found for this Platform and Arch");
|
||||
if (/\.tar\.gz/.test(urlBin)) {
|
||||
const tmpFileTar = path.join(os.tmpdir(), Buffer.from(Math.random().toString()).toString("hex")+"bdscore_php.tar.gz");
|
||||
await fs.promises.writeFile(tmpFileTar, await httpRequests.getBuffer(urlBin));
|
||||
if (fs.existsSync(path.join(serverPath, "bin"))) {
|
||||
await fs.promises.rm(path.join(serverPath, "bin"), {recursive: true});
|
||||
await fs.promises.mkdir(path.join(serverPath, "bin"));
|
||||
} else await fs.promises.mkdir(path.join(serverPath, "bin"));
|
||||
await tar.x({
|
||||
file: tmpFileTar,
|
||||
C: path.join(serverPath, "bin"),
|
||||
keep: true,
|
||||
p: true,
|
||||
noChmod: false
|
||||
});
|
||||
await fs.promises.rm(tmpFileTar, {force: true});
|
||||
} else {
|
||||
const PHPZip = new adm_zip(await httpRequests.getBuffer(urlBin));
|
||||
if (fs.existsSync(path.resolve(serverPath, "bin"))) await fs.promises.rm(path.resolve(serverPath, "bin"), {recursive: true});
|
||||
await new Promise((res,rej) => PHPZip.extractAllToAsync(serverPath, false, true, err => err?rej(err):res("")));
|
||||
}
|
||||
if (process.platform === "linux"||process.platform === "android"||process.platform === "darwin") {
|
||||
const ztsFind = await Readdirrec(path.resolve(serverPath, "bin"), [/.*debug-zts.*/]);
|
||||
if (ztsFind.length === 0) return urlBin;
|
||||
const phpIniPath = (await Readdirrec(path.resolve(serverPath, "bin"), [/php\.ini$/]))[0].path;
|
||||
let phpIni = await fs.promises.readFile(phpIniPath, "utf8");
|
||||
if (phpIni.includes("extension_dir")) {
|
||||
await fs.promises.writeFile(phpIniPath, phpIni.replace(/extension_dir=.*/g, ""));
|
||||
}
|
||||
phpIni = phpIni+`\nextension_dir=${ztsFind[0].path}`
|
||||
await fs.promises.writeFile(phpIniPath, phpIni);
|
||||
}
|
||||
return urlBin;
|
||||
}
|
||||
|
||||
export async function download(version: string|boolean) {
|
||||
const ServerPath = path.join(serverRoot, "pocketmine");
|
||||
if (!(await fs.existsSync(ServerPath))) fs.mkdirSync(ServerPath, {recursive: true});
|
||||
const pocketmineInfo = await versionManeger.findUrlVersion("pocketmine", version);
|
||||
await fs.promises.writeFile(path.resolve(ServerPath, "PocketMine.phar"), await httpRequests.getBuffer(String(pocketmineInfo.url)));
|
||||
await buildLocal(ServerPath).catch(err => {console.log("Error on build in system, error:\n%o\nDownloading pre build files", err); return InstallPrebuildPHP(ServerPath);});
|
||||
|
||||
// Return info
|
||||
return {
|
||||
version: pocketmineInfo.version,
|
||||
publishDate: pocketmineInfo.datePublish,
|
||||
url: pocketmineInfo.url,
|
||||
};
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
export { download as DownloadServer} from "./download";
|
||||
export * as linkWorld from "./linkWorld";
|
||||
export * as addons from "./addons";
|
||||
export * as config from "./config";
|
||||
export * as server from "./server";
|
||||
export * as backup from "./backup";
|
@ -1,21 +0,0 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as fsOld from "node:fs";
|
||||
import * as path from "path";
|
||||
import { serverRoot, worldStorageRoot } from "../pathControl";
|
||||
|
||||
export async function linkWorld(): Promise<void> {
|
||||
const worldFolder = path.join(worldStorageRoot, "pocketmine");
|
||||
const pocketmineFolder = path.join(serverRoot, "pocketmine");
|
||||
if (!fsOld.existsSync(pocketmineFolder)) throw new Error("Server not installed")
|
||||
if (!fsOld.existsSync(worldFolder)) await fs.mkdir(worldFolder, {recursive: true});
|
||||
const pocketmineServerWorld = path.join(pocketmineFolder, "worlds");
|
||||
if (fsOld.existsSync(pocketmineServerWorld)) {
|
||||
if ((await fs.lstat(pocketmineServerWorld)).isSymbolicLink()) return;
|
||||
for (const folder of await fs.readdir(pocketmineServerWorld)) {
|
||||
await fs.rename(path.join(pocketmineServerWorld, folder), path.join(worldFolder, folder))
|
||||
}
|
||||
await fs.rmdir(pocketmineServerWorld);
|
||||
}
|
||||
await fs.symlink(worldFolder, pocketmineServerWorld, "dir");
|
||||
return;
|
||||
}
|
@ -1,192 +0,0 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import crypto from "crypto";
|
||||
import node_cron from "cron";
|
||||
import * as child_process from "../lib/childProcess";
|
||||
import { backupRoot, serverRoot } from "../pathControl";
|
||||
import { BdsSession, bdsSessionCommands, serverListen, playerAction2 } from '../globalType';
|
||||
import { CreateBackup } from "./backup";
|
||||
import events from "../lib/customEvents";
|
||||
import { linkWorld } from "./linkWorld";
|
||||
|
||||
const pocketmineSesions: {[key: string]: BdsSession} = {};
|
||||
export function getSessions() {return pocketmineSesions;}
|
||||
|
||||
const ServerPath = path.join(serverRoot, "pocketmine");
|
||||
export async function startServer(): Promise<BdsSession> {
|
||||
if (!(fs.existsSync(ServerPath))) throw new Error("Server dont installed");
|
||||
if (process.env.AUTO_LINK_WORLD === "true" || process.env.AUTO_LINK_WORLDS === "1") await linkWorld();
|
||||
const SessionID = crypto.randomUUID();
|
||||
const Process: {command: string; args: Array<string>} = {command: "", args: []};
|
||||
if (process.platform === "win32") Process.command = path.resolve(ServerPath, "bin/php/php.exe");
|
||||
else {
|
||||
Process.command = path.resolve(ServerPath, "bin/bin/php");
|
||||
await child_process.runAsync("chmod", ["a+x", Process.command]);
|
||||
}
|
||||
Process.args.push(path.join(ServerPath, "PocketMine.phar"), "--no-wizard", "--enable-ansi");
|
||||
|
||||
// Start Server
|
||||
const serverEvents = new events();
|
||||
const StartDate = new Date();
|
||||
const ServerProcess = await child_process.execServer({runOn: "host"}, Process.command, Process.args, {cwd: ServerPath});
|
||||
const { onExit, on: execOn } = ServerProcess;
|
||||
// Log Server redirect to callbacks events and exit
|
||||
execOn("out", data => serverEvents.emit("log_stdout", data));
|
||||
execOn("err", data => serverEvents.emit("log_stderr", data));
|
||||
execOn("all", data => serverEvents.emit("log", data));
|
||||
onExit().catch(err => {serverEvents.emit("err", err);return null}).then(code => serverEvents.emit("closed", code));
|
||||
|
||||
// On server started
|
||||
serverEvents.on("log", lineData => {
|
||||
// [22:52:05.580] [Server thread/INFO]: Done (0.583s)! For help, type "help" or "?"
|
||||
if (/\[.*\].*\s+Done\s+\(.*\)\!.*/.test(lineData)) serverEvents.emit("started", new Date());
|
||||
});
|
||||
|
||||
// Port listen
|
||||
serverEvents.on("log", data => {
|
||||
// [16:49:31.284] [Server thread/INFO]: Minecraft network interface running on [::]:19133
|
||||
// [16:49:31.273] [Server thread/INFO]: Minecraft network interface running on 0.0.0.0:19132
|
||||
if (/\[.*\]:\s+Minecraft\s+network\s+interface\s+running\s+on\s+.*/gi.test(data)) {
|
||||
const matchString = data.match(/\[.*\]:\s+Minecraft\s+network\s+interface\s+running\s+on\s+(.*)/);
|
||||
if (!!matchString) {
|
||||
const portParse = matchString[1];
|
||||
const portObject: serverListen = {port: 0, version: "IPv4", protocol: "UDP"};
|
||||
const isIpv6 = /\[.*\]:/.test(portParse);
|
||||
if (!isIpv6) portObject.port = parseInt(portParse.split(":")[1]);
|
||||
else {
|
||||
portObject.port = parseInt(portParse.replace(/\[.*\]:/, "").trim())
|
||||
portObject.version = "IPv6";
|
||||
}
|
||||
serverEvents.emit("port_listen", portObject);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Player Actions
|
||||
serverEvents.on("log", data => {
|
||||
const actionDate = new Date();
|
||||
if (/\[.*\]:\s+(.*)\s+(.*)\s+the\s+game/gi.test(data)) {
|
||||
const [action, player] = (data.match(/[.*]:\s+(.*)\s+(.*)\s+the\s+game/gi)||[]).slice(1, 3);
|
||||
const playerAction: playerAction2 = {player: player, action: "unknown", Date: actionDate};
|
||||
if (action === "joined") playerAction.action = "connect";
|
||||
else if (action === "left") playerAction.action = "disconnect";
|
||||
|
||||
// Server player event
|
||||
serverEvents.emit("player", playerAction);
|
||||
delete playerAction.action;
|
||||
if (action === "connect") serverEvents.emit("player_connect", playerAction);
|
||||
else if (action === "disconnect") serverEvents.emit("player_disconnect", playerAction);
|
||||
else serverEvents.emit("player_unknown", playerAction);
|
||||
}
|
||||
});
|
||||
|
||||
// Run Command
|
||||
const serverCommands: bdsSessionCommands = {
|
||||
/**
|
||||
* Run any commands in server.
|
||||
* @param command - Run any commands in server without parse commands
|
||||
* @returns - Server commands
|
||||
*/
|
||||
execCommand: (...command) => {
|
||||
ServerProcess.writelf(command.map(a => String(a)).join(" "));
|
||||
return serverCommands;
|
||||
},
|
||||
tpPlayer: (player: string, x: number, y: number, z: number) => {
|
||||
serverCommands.execCommand("tp", player, x, y, z);
|
||||
return serverCommands;
|
||||
},
|
||||
worldGamemode: (gamemode: "survival"|"creative"|"hardcore") => {
|
||||
serverCommands.execCommand("gamemode", gamemode);
|
||||
return serverCommands;
|
||||
},
|
||||
userGamemode: (player: string, gamemode: "survival"|"creative"|"hardcore") => {
|
||||
serverCommands.execCommand("gamemode", gamemode, player);
|
||||
return serverCommands;
|
||||
},
|
||||
stop: (): Promise<number|null> => {
|
||||
if (ServerProcess.Exec.exitCode !== null||ServerProcess.Exec.killed) return Promise.resolve(ServerProcess.Exec.exitCode);
|
||||
if (ServerProcess.Exec.killed) return Promise.resolve(ServerProcess.Exec.exitCode);
|
||||
ServerProcess.writelf("stop");
|
||||
return ServerProcess.onExit();
|
||||
}
|
||||
}
|
||||
|
||||
const backupCron = (crontime: string|Date, option?: {type: "zip", config?: {pathZip?: string}}): node_cron.CronJob => {
|
||||
// Validate Config
|
||||
if (option) {
|
||||
if (option.type === "zip") {}
|
||||
else option = {type: "zip"};
|
||||
}
|
||||
async function lockServerBackup() {
|
||||
serverCommands.execCommand("save hold");
|
||||
await new Promise(accept => setTimeout(accept, 1000));
|
||||
serverCommands.execCommand("save query");
|
||||
await new Promise(accept => setTimeout(accept, 1000));
|
||||
}
|
||||
async function unLockServerBackup() {
|
||||
serverCommands.execCommand("save resume");
|
||||
await new Promise(accept => setTimeout(accept, 1000));
|
||||
}
|
||||
if (!option) option = {type: "zip"};
|
||||
const CrontimeBackup = new node_cron.CronJob(crontime, async () => {
|
||||
if (option.type === "zip") {
|
||||
await lockServerBackup();
|
||||
if (!!option?.config?.pathZip) await CreateBackup().then(res=> fs.promises.writeFile(path.resolve(backupRoot, option?.config?.pathZip), res)).catch(() => undefined);
|
||||
// else await createZipBackup(true).catch(() => undefined);
|
||||
await unLockServerBackup();
|
||||
}
|
||||
});
|
||||
CrontimeBackup.start();
|
||||
serverEvents.on("closed", () => CrontimeBackup.stop());
|
||||
return CrontimeBackup;
|
||||
}
|
||||
|
||||
// Session log
|
||||
const logFile = path.resolve(process.env.LOG_PATH||path.resolve(ServerPath, "../log"), `bedrock_${SessionID}.log`);
|
||||
if(!(fs.existsSync(path.parse(logFile).dir))) fs.mkdirSync(path.parse(logFile).dir, {recursive: true});
|
||||
const logStream = fs.createWriteStream(logFile, {flags: "w+"});
|
||||
logStream.write(`[${StartDate.toString()}] Server started\n\n`);
|
||||
ServerProcess.Exec.stdout.pipe(logStream);
|
||||
ServerProcess.Exec.stderr.pipe(logStream);
|
||||
|
||||
// Session Object
|
||||
const Seesion: BdsSession = {
|
||||
id: SessionID,
|
||||
creteBackup: backupCron,
|
||||
ports: [],
|
||||
Player: {},
|
||||
seed: undefined,
|
||||
commands: serverCommands,
|
||||
server: {
|
||||
on: (act, fn) => serverEvents.on(act, fn),
|
||||
once: (act, fn) => serverEvents.once(act, fn),
|
||||
startDate: StartDate,
|
||||
started: false
|
||||
}
|
||||
};
|
||||
|
||||
serverEvents.on("started", StartDate => {Seesion.server.startDate = StartDate; Seesion.server.started = true;});
|
||||
serverEvents.on("port_listen", portObject => Seesion.ports.push(portObject));
|
||||
serverEvents.on("player", playerAction => {
|
||||
if (!Seesion.Player[playerAction.player]) Seesion.Player[playerAction.player] = {
|
||||
action: playerAction.action,
|
||||
date: playerAction.Date,
|
||||
history: [{
|
||||
action: playerAction.action,
|
||||
date: playerAction.Date
|
||||
}]
|
||||
}; else {
|
||||
Seesion.Player[playerAction.player].action = playerAction.action;
|
||||
Seesion.Player[playerAction.player].date = playerAction.Date;
|
||||
Seesion.Player[playerAction.player].history.push({
|
||||
action: playerAction.action,
|
||||
date: playerAction.Date
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Return Session
|
||||
pocketmineSesions[SessionID] = Seesion;
|
||||
serverEvents.on("closed", () => delete pocketmineSesions[SessionID]);
|
||||
return Seesion;
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
import * as fsOld from "node:fs";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import admZip from "adm-zip";
|
||||
import { serverRoot } from '../pathControl';
|
||||
const javaPath = path.join(serverRoot, "spigot");
|
||||
|
||||
const filesFoldertoIgnore = ["Server.jar", "bundler", "eula.txt", "help.yml", "logs", "usercache.json"];
|
||||
|
||||
/**
|
||||
* Create backup for Worlds and Settings
|
||||
*/
|
||||
export async function CreateBackup(): Promise<Buffer> {
|
||||
if (!(fsOld.existsSync(javaPath))) throw new Error("Install server");
|
||||
const filesLint = (await fs.readdir(javaPath)).filter(file => !(filesFoldertoIgnore.some(folder => folder === file)));
|
||||
const zip = new admZip();
|
||||
for (const file of filesLint) {
|
||||
const filePath = path.join(javaPath, file);
|
||||
const stats = await fs.stat(filePath);
|
||||
if (stats.isSymbolicLink()) {
|
||||
const realPath = await fs.realpath(filePath);
|
||||
const realStats = await fs.stat(realPath);
|
||||
if (realStats.isDirectory()) zip.addLocalFolder(realPath, file);
|
||||
else zip.addLocalFile(realPath, file);
|
||||
} else if (stats.isDirectory()) zip.addLocalFolder(filePath);
|
||||
else zip.addLocalFile(filePath);
|
||||
}
|
||||
return zip.toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore backup for Worlds and Settings
|
||||
*
|
||||
* WARNING: This will overwrite existing files and World folder files
|
||||
*/
|
||||
export async function RestoreBackup(zipBuffer: Buffer): Promise<void> {
|
||||
const zip = new admZip(zipBuffer);
|
||||
await new Promise((resolve, reject) => zip.extractAllToAsync(javaPath, true, true, (err) => !!err ? reject(err) : resolve("")));
|
||||
return;
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import * as versionManeger from "@the-bds-maneger/server_versions";
|
||||
import * as httpRequests from "../lib/HttpRequests";
|
||||
import { serverRoot } from "../pathControl";
|
||||
|
||||
export async function download(version: string|boolean) {
|
||||
const ServerPath = path.join(serverRoot, "spigot");
|
||||
if (!(await fs.existsSync(ServerPath))) fs.mkdirSync(ServerPath, {recursive: true});
|
||||
if (!(await fs.existsSync(ServerPath))) fs.mkdirSync(ServerPath, {recursive: true});
|
||||
const spigotSearch = await versionManeger.findUrlVersion("spigot", version);
|
||||
await fs.promises.writeFile(path.resolve(ServerPath, "Server.jar"), await httpRequests.getBuffer(String(spigotSearch.url)));
|
||||
await fs.promises.writeFile(path.resolve(ServerPath, "eula.txt"), "eula=true");
|
||||
|
||||
// Return info
|
||||
return {
|
||||
version: spigotSearch.version,
|
||||
publishDate: spigotSearch.datePublish,
|
||||
url: spigotSearch.url,
|
||||
};
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export { download as DownloadServer } from "./download";
|
||||
export * as server from "./server";
|
||||
export * as backup from "./backup";
|
@ -1,43 +0,0 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as fsOld from "node:fs";
|
||||
import * as path from "path";
|
||||
import { serverRoot, worldStorageRoot } from "../pathControl";
|
||||
const filesFoldertoIgnore = ["Server.jar", "eula.txt", "libraries", "logs", "usercache.json", "versions", "banned-ips.json", "banned-players.json", "ops.json", "server.properties", "whitelist.json"];
|
||||
|
||||
export async function linkWorld(): Promise<void> {
|
||||
const worldFolder = path.join(worldStorageRoot, "java");
|
||||
const javaFolder = path.join(serverRoot, "java");
|
||||
if (!fsOld.existsSync(javaFolder)) throw new Error("Server not installed")
|
||||
if (!fsOld.existsSync(worldFolder)) await fs.mkdir(worldFolder, {recursive: true});
|
||||
// From Worlds Folders
|
||||
for (const worldPath of await fs.readdir(worldFolder)) {
|
||||
const serverWorld = path.join(javaFolder, worldPath);
|
||||
const worldStorage = path.join(worldFolder, worldPath);
|
||||
if (fsOld.existsSync(serverWorld)) {
|
||||
if ((await fs.lstat(serverWorld)).isSymbolicLink()) continue;
|
||||
}
|
||||
try {
|
||||
await fs.cp(worldStorage, serverWorld, {recursive: true});
|
||||
await fs.rm(worldStorage, {recursive: true});
|
||||
await fs.symlink(worldStorage, serverWorld);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
continue
|
||||
}
|
||||
}
|
||||
// From Server folder
|
||||
for (const worldPath of (await fs.readdir(javaFolder)).filter(x => !filesFoldertoIgnore.includes(x))) {
|
||||
const serverWorld = path.join(worldFolder, worldPath);
|
||||
const worldStorage = path.join(javaFolder, worldPath);
|
||||
if ((await fs.lstat(worldStorage)).isSymbolicLink()) continue;
|
||||
try {
|
||||
await fs.cp(worldStorage, serverWorld, {recursive: true});
|
||||
await fs.rm(worldStorage, {recursive: true});
|
||||
await fs.symlink(serverWorld, worldStorage);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
continue
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
@ -1,135 +0,0 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import crypto from "node:crypto";
|
||||
import node_cron from "cron";
|
||||
import * as child_process from "../lib/childProcess";
|
||||
import { backupRoot, serverRoot } from "../pathControl";
|
||||
import { BdsSession, bdsSessionCommands } from '../globalType';
|
||||
import { CreateBackup } from "./backup";
|
||||
import events from "../lib/customEvents";
|
||||
import { linkWorld } from "./linkWorld";
|
||||
|
||||
const javaSesions: {[key: string]: BdsSession} = {};
|
||||
export function getSessions() {return javaSesions;}
|
||||
|
||||
const ServerPath = path.join(serverRoot, "spigot");
|
||||
export async function startServer(): Promise<BdsSession> {
|
||||
if (!(fs.existsSync((ServerPath)))) throw new Error("server dont installed");
|
||||
if (process.env.AUTO_LINK_WORLD === "true" || process.env.AUTO_LINK_WORLDS === "1") await linkWorld();
|
||||
const SessionID = crypto.randomUUID();
|
||||
// Start Server
|
||||
const serverEvents = new events();
|
||||
const StartDate = new Date();
|
||||
const ServerProcess = await child_process.execServer({runOn: "host"}, "java", ["-jar", "Server.jar"], {cwd: ServerPath});
|
||||
const { onExit, on: execOn } = ServerProcess;
|
||||
// Log Server redirect to callbacks events and exit
|
||||
execOn("out", data => serverEvents.emit("log_stdout", data));
|
||||
execOn("err", data => serverEvents.emit("log_stderr", data));
|
||||
execOn("all", data => serverEvents.emit("log", data));
|
||||
onExit().catch(err => {serverEvents.emit("err", err);return null}).then(code => serverEvents.emit("closed", code));
|
||||
|
||||
// Server Start
|
||||
serverEvents.on("log", lineData => {
|
||||
// [22:35:26] [Server thread/INFO]: Done (6.249s)! For help, type "help"
|
||||
if (/\[.*\].*\s+Done\s+\(.*\)\!.*/.test(lineData)) serverEvents.emit("started", new Date());
|
||||
});
|
||||
|
||||
// Parse ports
|
||||
serverEvents.on("log", data => {
|
||||
const portParse = data.match(/Starting\s+Minecraft\s+server\s+on\s+(.*)\:(\d+)/);
|
||||
if (!!portParse) serverEvents.emit("port_listen", {port: parseInt(portParse[2]), version: "IPv4/IPv6", protocol: "TCP"});
|
||||
});
|
||||
|
||||
// Run Command
|
||||
const serverCommands: bdsSessionCommands = {
|
||||
/**
|
||||
* Run any commands in server.
|
||||
* @param command - Run any commands in server without parse commands
|
||||
* @returns - Server commands
|
||||
*/
|
||||
execCommand: (...command) => {
|
||||
ServerProcess.writelf(command.map(a => String(a)).join(" "));
|
||||
return serverCommands;
|
||||
},
|
||||
tpPlayer: (player: string, x: number, y: number, z: number) => {
|
||||
serverCommands.execCommand("tp", player, x, y, z);
|
||||
return serverCommands;
|
||||
},
|
||||
worldGamemode: (gamemode: "survival"|"creative"|"hardcore") => {
|
||||
serverCommands.execCommand("gamemode", gamemode);
|
||||
return serverCommands;
|
||||
},
|
||||
userGamemode: (player: string, gamemode: "survival"|"creative"|"hardcore") => {
|
||||
serverCommands.execCommand("gamemode", gamemode, player);
|
||||
return serverCommands;
|
||||
},
|
||||
stop: (): Promise<number|null> => {
|
||||
if (ServerProcess.Exec.exitCode !== null||ServerProcess.Exec.killed) return Promise.resolve(ServerProcess.Exec.exitCode);
|
||||
if (ServerProcess.Exec.killed) return Promise.resolve(ServerProcess.Exec.exitCode);
|
||||
ServerProcess.writelf("stop");
|
||||
return ServerProcess.onExit();
|
||||
}
|
||||
}
|
||||
|
||||
const backupCron = (crontime: string|Date, option?: {type: "zip", config?: {pathZip?: string}}): node_cron.CronJob => {
|
||||
// Validate Config
|
||||
if (option) {
|
||||
if (option.type === "zip") {}
|
||||
else option = {type: "zip"};
|
||||
}
|
||||
async function lockServerBackup() {
|
||||
serverCommands.execCommand("save hold");
|
||||
await new Promise(accept => setTimeout(accept, 1000));
|
||||
serverCommands.execCommand("save query");
|
||||
await new Promise(accept => setTimeout(accept, 1000));
|
||||
}
|
||||
async function unLockServerBackup() {
|
||||
serverCommands.execCommand("save resume");
|
||||
await new Promise(accept => setTimeout(accept, 1000));
|
||||
}
|
||||
if (!option) option = {type: "zip"};
|
||||
const CrontimeBackup = new node_cron.CronJob(crontime, async () => {
|
||||
if (option.type === "zip") {
|
||||
await lockServerBackup();
|
||||
if (!!option?.config?.pathZip) await CreateBackup().then(res => fs.promises.writeFile(path.resolve(backupRoot, option?.config?.pathZip), res)).catch(() => undefined);
|
||||
// else await createZipBackup(true).catch(() => undefined);
|
||||
await unLockServerBackup();
|
||||
}
|
||||
});
|
||||
CrontimeBackup.start();
|
||||
serverEvents.on("closed", () => CrontimeBackup.stop());
|
||||
return CrontimeBackup;
|
||||
}
|
||||
|
||||
// Session log
|
||||
const logFile = path.resolve(process.env.LOG_PATH||path.resolve(ServerPath, "../log"), `bedrock_${SessionID}.log`);
|
||||
if(!(fs.existsSync(path.parse(logFile).dir))) fs.mkdirSync(path.parse(logFile).dir, {recursive: true});
|
||||
const logStream = fs.createWriteStream(logFile, {flags: "w+"});
|
||||
logStream.write(`[${StartDate.toString()}] Server started\n\n`);
|
||||
ServerProcess.Exec.stdout.pipe(logStream);
|
||||
ServerProcess.Exec.stderr.pipe(logStream);
|
||||
|
||||
// Session Object
|
||||
const Seesion: BdsSession = {
|
||||
id: SessionID,
|
||||
creteBackup: backupCron,
|
||||
ports: [],
|
||||
Player: {},
|
||||
seed: undefined,
|
||||
commands: serverCommands,
|
||||
server: {
|
||||
on: (act, fn) => serverEvents.on(act, fn),
|
||||
once: (act, fn) => serverEvents.once(act, fn),
|
||||
started: false,
|
||||
startDate: StartDate
|
||||
}
|
||||
};
|
||||
|
||||
serverEvents.on("started", StartDate => {Seesion.server.startDate = StartDate; Seesion.server.started = true;});
|
||||
serverEvents.on("port_listen", portObject => Seesion.ports.push(portObject));
|
||||
|
||||
// Return Session
|
||||
javaSesions[SessionID] = Seesion;
|
||||
serverEvents.on("closed", () => delete javaSesions[SessionID]);
|
||||
return Seesion;
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as fsOld from "node:fs";
|
||||
import * as path from "node:path";
|
||||
|
||||
async function readDirAndFilter(dir: string, test: Array<RegExp> = [/.*/]) {
|
||||
if (!(fsOld.existsSync(dir))) throw new Error(`${dir} does not exist`);
|
||||
const files = await fs.readdir(dir);
|
||||
const parseFiles: Array<string> = []
|
||||
await Promise.all(files.map(async (fd) => {
|
||||
const stat = await fs.stat(path.join(dir, fd));
|
||||
if (stat.isDirectory()) return readDirAndFilter(path.join(dir, fd), test).then(res => parseFiles.push(...res)).catch(err => console.error(err));
|
||||
else if (stat.isFile()) {
|
||||
const match = test.some(reg => reg.test(fd));
|
||||
if (match) parseFiles.push(path.join(dir, fd));
|
||||
}
|
||||
}));
|
||||
return parseFiles;
|
||||
}
|
||||
|
||||
async function runTest() {
|
||||
const mainFind = path.join(process.cwd(), "src");
|
||||
const testsFiles = await readDirAndFilter(mainFind, [/.*\.test\.ts$/]);
|
||||
for (const file of testsFiles) {
|
||||
console.log("************** Start Script: %s **************", file);
|
||||
const testScript = await import(file) as {[key: string]: () => Promise<void>};
|
||||
if (!!testScript.default) {
|
||||
console.log("************** Start Test: %s **************", file);
|
||||
await testScript.default();
|
||||
}
|
||||
for (const key in testScript) {
|
||||
if (key === "default") continue;
|
||||
console.log("************** Start Test: %s **************", key);
|
||||
await testScript[key]();
|
||||
}
|
||||
console.log("************** End Script: %s **************", file);
|
||||
}
|
||||
}
|
||||
|
||||
runTest().then(() => {
|
||||
console.log("Test passed");
|
||||
process.exitCode = 0;
|
||||
}).catch((err: Error) => {
|
||||
console.error("Test failed");
|
||||
console.error(err);
|
||||
process.exitCode = 1;
|
||||
}).then(() => {
|
||||
console.log("Exit with code: %d", process.exitCode);
|
||||
return process.exit();
|
||||
});
|
Reference in New Issue
Block a user