Reewrite project #430

Merged
Sirherobrine23 merged 13 commits from reewriteProject into main 2022-08-31 10:03:14 +00:00
40 changed files with 71 additions and 3257 deletions
Showing only changes of commit f8ab8914f2 - Show all commits

View File

@ -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

View File

@ -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
View File

@ -0,0 +1 @@
import {} from "./childPromisses";

View File

@ -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
*/

View File

@ -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;
}

View File

@ -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
};
}

View File

@ -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,
};
}

View File

@ -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";

View File

@ -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;
}

View File

@ -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
View 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));
}

View File

@ -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;

View File

@ -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;
};
};

View File

@ -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";

View File

@ -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;
}

View File

@ -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,
};
}

View File

@ -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";

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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"));
}

View File

@ -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");
}

View File

@ -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
};
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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));
}

View File

@ -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);
});
}

View File

@ -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");

View File

@ -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")))
}

View File

@ -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;
}

View File

@ -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
}
}
*/

View File

@ -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,
};
}

View File

@ -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";

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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,
};
}

View File

@ -1,3 +0,0 @@
export { download as DownloadServer } from "./download";
export * as server from "./server";
export * as backup from "./backup";

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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();
});