Reewrite project #430

Merged
Sirherobrine23 merged 13 commits from reewriteProject into main 2022-08-31 10:03:14 +00:00
56 changed files with 5866 additions and 9056 deletions

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

19
.vscode/launch.json vendored
View File

@ -2,15 +2,20 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Bds Test",
"type": "node", "type": "node",
"request": "launch", "name": "Mocha Tests",
"runtimeExecutable": "node", "program": "${workspaceFolder}/node_modules/mocha/bin/mocha.js",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
"args": ["testProject.ts"],
"cwd": "${workspaceRoot}",
"internalConsoleOptions": "openOnSessionStart", "internalConsoleOptions": "openOnSessionStart",
"skipFiles": ["<node_internals>/**", "node_modules/**"] "request": "launch",
"skipFiles": [
"<node_internals>/**"
],
"args": [
"-r", "ts-node/register",
"--colors",
"${workspaceFolder}/tests/**/*.ts"
],
} }
] ]
} }

View File

@ -1,128 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@ -1,14 +0,0 @@
# Contributing to Core Core for server management and events
Read Code of Conduct fist!
[Bds Manager Core code of conduct](CODE_OF_CONDUCT.md)
## TL;DR
[Add Platform](https://github.com/The-Bds-Maneger/Bds-Maneger-Core/wiki/Add-new-Platform-to-Core)
Bds Manager Core accepts contributions provided that:
* Be a fork of the repository.
* have the latest repository modifications applied to the repository in the `main` branch.
* to add a platform to the project, maintaining scripts.

146
LICENSE
View File

@ -1,5 +1,5 @@
GNU AFFERO GENERAL PUBLIC LICENSE GNU GENERAL PUBLIC LICENSE
Version 3, 19 November 2007 Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies Everyone is permitted to copy and distribute verbatim copies
@ -7,15 +7,17 @@
Preamble Preamble
The GNU Affero General Public License is a free, copyleft license for The GNU General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure software and other kinds of works.
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast, to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free share and change all versions of a program--to make sure it remains free
software for all its users. software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you price. Our General Public Licenses are designed to make sure that you
@ -24,34 +26,44 @@ them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things. free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights To protect your rights, we need to prevent others from denying you
with two steps: (1) assert copyright on the software, and (2) offer these rights or asking you to surrender the rights. Therefore, you have
you this License which gives you legal permission to copy, distribute certain responsibilities if you distribute copies of the software, or if
and/or modify the software. you modify it: responsibilities to respect the freedom of others.
A secondary benefit of defending all users' freedom is that For example, if you distribute copies of such a program, whether
improvements made in alternate versions of the program, if they gratis or for a fee, you must pass on to the recipients the same
receive widespread use, become available for other developers to freedoms that you received. You must make sure that they, too, receive
incorporate. Many developers of free software are heartened and or can get the source code. And you must show them these terms so they
encouraged by the resulting cooperation. However, in the case of know their rights.
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to Developers that use the GNU GPL protect your rights with two steps:
ensure that, in such cases, the modified source code becomes available (1) assert copyright on the software, and (2) offer you this License
to the community. It requires the operator of a network server to giving you legal permission to copy, distribute and/or modify it.
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and For the developers' and authors' protection, the GPL clearly explains
published by Affero, was designed to accomplish similar goals. This is that there is no warranty for this free software. For both users' and
a different license, not a version of the Affero GPL, but Affero has authors' sake, the GPL requires that modified versions be marked as
released a new version of the Affero GPL which permits relicensing under changed, so that their problems will not be attributed erroneously to
this license. authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and The precise terms and conditions for copying, distribution and
modification follow. modification follow.
@ -60,7 +72,7 @@ modification follow.
0. Definitions. 0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License. "This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of "Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks. works, such as semiconductor masks.
@ -537,45 +549,35 @@ to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program. License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License. 13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work, License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version but the special requirements of the GNU Affero General Public License,
3 of the GNU General Public License. section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License. 14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions the GNU General Public License from time to time. Such new versions will
will be similar in spirit to the present version, but may differ in detail to be similar in spirit to the present version, but may differ in detail to
address new problems or concerns. address new problems or concerns.
Each version is given a distinguishing version number. If the Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published GNU General Public License, you may choose any version ever published
by the Free Software Foundation. by the Free Software Foundation.
If the Program specifies that a proxy can decide which future If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you public statement of acceptance of a version permanently authorizes you
to choose that version for the Program. to choose that version for the Program.
@ -630,32 +632,44 @@ state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found. the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.> <one line to give the program's name and a brief idea of what it does.>
Copyright (C) 2021 <name of author> Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published it under the terms of the GNU General Public License as published by
by the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
(at your option) any later version. (at your option) any later version.
This program is distributed in the hope that it will be useful, This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details. GNU General Public License for more details.
You should have received a copy of the GNU Affero General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail. Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer If the program does terminal interaction, make it output a short
network, you should also make sure that it provides a way for users to notice like this when it starts in an interactive mode:
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive <program> Copyright (C) <year> <name of author>
of the code. There are many ways you could offer source, and different This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
solutions will be better for different programs; see section 13 for the This is free software, and you are welcome to redistribute it
specific requirements. under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school, You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary. if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>. <https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

2749
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,51 +1,27 @@
{ {
"name": "@the-bds-maneger/core", "name": "@the-bds-maneger/core",
"version": "3.1.0", "version": "4.0.0",
"description": "A very simple way to manage Minecraft servers", "description": "A very simple way to manage Minecraft servers",
"author": "Sirherobrine23", "author": "Sirherobrine23",
"license": "AGPL-3.0-or-later", "license": "GPL-3.0",
"homepage": "https://sirherobrine23.org/Bds_Maneger_Project", "homepage": "https://sirherobrine23.org/BdsProject",
"type": "commonjs",
"types": "./dist/index.d.ts",
"main": "./dist/index.js",
"private": false, "private": false,
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
}, },
"maintainers": [
{
"name": "Matheus Sampaio Queiroga",
"email": "srherobrine20@gmail.com",
"url": "https://sirherobrine23.org"
}
],
"types": "./dist/dts/index.d.ts",
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.mjs",
"exports": {
"require": "./dist/cjs/index.js",
"import": "./dist/esm/index.mjs"
},
"scripts": { "scripts": {
"dev": "nodemon", "dev": "nodemon",
"build": "run-s build:*", "build": "tsc",
"test": "ts-node testProject.ts", "test": "mocha -r ts-node/register 'tests/**/*.ts'"
"build:cjs": "tsc --outDir dist/cjs --module commonjs",
"build:esm": "tsc --outDir dist/esm --module es6 && node -e 'const fs = require(\"fs\");const path = require(\"path\");const read = (pathRe) => {for (const fileFolde of fs.readdirSync(pathRe)) {const filePath = path.join(pathRe, fileFolde);if (fs.statSync(filePath).isDirectory()) read(filePath);else {console.log(filePath, \"-->\", filePath.replace(/\\.js$/, \".mjs\"));fs.renameSync(filePath, filePath.replace(/\\.js$/, \".mjs\"));}}};read(\"dist/esm\");'"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/The-Bds-Maneger/Bds-Maneger-Core.git" "url": "git+https://github.com/The-Bds-Maneger/Bds-Maneger-Core.git"
}, },
"keywords": [ "keywords": [],
"typescript",
"bds",
"bds_maneger",
"bds-maneger",
"bds_project",
"minecraft",
"bedrock",
"java",
"pocketmine",
"spigot"
],
"bugs": { "bugs": {
"url": "https://github.com/The-Bds-Maneger/Bds-Maneger-Core/issues/new", "url": "https://github.com/The-Bds-Maneger/Bds-Maneger-Core/issues/new",
"email": "support_bds@sirherobrine23.org" "email": "support_bds@sirherobrine23.org"
@ -53,40 +29,22 @@
"engines": { "engines": {
"node": ">=16.0.0" "node": ">=16.0.0"
}, },
"os": [
"linux",
"darwin",
"win32",
"android"
],
"dependencies": { "dependencies": {
"@the-bds-maneger/server_versions": "^2.3.1", "@the-bds-maneger/server_versions": "^3.0.1",
"adm-zip": "^0.5.9", "adm-zip": "^0.5.9",
"axios": "^0.27.2", "axios": "^0.27.2",
"cron": "^2.1.0", "cron": "^2.1.0",
"fs-extra": "^10.1.0",
"prismarine-nbt": "^2.2.1", "prismarine-nbt": "^2.2.1",
"tar": "^6.1.11" "tar": "^6.1.11"
}, },
"devDependencies": { "devDependencies": {
"@types/adm-zip": "^0.5.0", "@types/adm-zip": "^0.5.0",
"@types/cron": "^2.0.0", "@types/cron": "^2.0.0",
"@types/fs-extra": "^9.0.13", "@types/mocha": "^9.1.1",
"@types/node": "^18.7.8", "@types/node": "^18.7.14",
"@types/tar": "^6.1.2", "@types/tar": "^6.1.2",
"nodemon": "^2.0.19", "mocha": "^10.0.0",
"npm-run-all": "^4.1.5",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^4.7.4" "typescript": "^4.8.2"
},
"nodemonConfig": {
"delay": 2500,
"exec": "npm run test",
"ext": "json,ts",
"watch": [
"src/**/*",
"package.json",
"package-lock.json"
]
} }
} }

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

98
src/bedrock.ts Normal file
View File

@ -0,0 +1,98 @@
import * as path from "node:path";
import * as fsOld from "node:fs";
import * as fs from "node:fs/promises";
import { promisify } from "node:util";
import { getBedrockZip } from "@the-bds-maneger/server_versions";
import admZip from "adm-zip";
import { exec, execAsync } from "./childPromisses";
import { serverRoot } from "./pathControl";
import { actions, actionConfig } from "./globalPlatfroms";
export const serverPath = path.join(serverRoot, "Bedrock");
export { bedrockServerWorld, bedrockWorld, linkBedrock } from "./linkWorlds/bedrock_pocketmine";
// RegExp
export const saveFf = /^(worlds|server\.properties|config|((permissions|allowlist|valid_known_packs)\.json)|(development_.*_packs))$/;
export const portListen = /\[.*\]\s+(IPv[46])\s+supported,\s+port:\s+([0-9]+)/;
export const started = /\[.*\]\s+Server\s+started\./;
// [2022-08-30 20:50:53:821 INFO] Player connected: Sirherobrine, xuid: 111111111111111
// [2022-08-30 20:56:55:231 INFO] Player disconnected: Sirherobrine, xuid: 111111111111111
export const player = /\[.*\]\s+Player\s+((dis|)connected):\s+(.*),\s+xuid:\s+([0-9]+)/;
// [2022-08-30 20:56:55:601 INFO] Running AutoCompaction...
export async function installServer(version: string|boolean) {
let arch = process.arch;
if (process.platform === "linux" && process.arch !== "x64") {
if (await execAsync("command -v qemu-x86_64-static").then(() => true).catch(() => false)||await execAsync("command -v box64").then(() => true).catch(() => false)) arch = "x64";
}
const zip = new admZip(await getBedrockZip(version, arch));
if (!fsOld.existsSync(serverPath)) await fs.mkdir(serverPath, {recursive: true});
// Remover files
for (const file of await fs.readdir(serverPath).then(files => files.filter(file => !saveFf.test(file)))) await fs.rm(path.join(serverPath, file), {recursive: true, force: true});
const serverConfig = (await fs.readFile(path.join(serverPath, "server.properties"), "utf8").catch(() => "")).trim();
await promisify(zip.extractAllToAsync)(serverPath, true, true);
if (serverConfig) await fs.writeFile(path.join(serverPath, "server.properties"), serverConfig);
}
const serverConfig: actionConfig[] = [
{
name: "portListening",
callback(data, done) {
const match = data.match(portListen);
if (!match) return;
const [, protocol, port] = match;
done({port: parseInt(port), type: "UDP", host: "127.0.0.1", protocol: protocol?.trim() === "IPv4" ? "IPv4" : protocol?.trim() === "IPv6" ? "IPv6" : "Unknown"});
}
},
{
name: "serverStarted",
callback(data, done) {
const resulter = data.match(started);
if (resulter) done(new Date());
},
},
{
name: "playerConnect",
callback(data, done) {
const match = data.match(player);
if (!match) return;
const [, action,, playerName, xuid] = match;
if (action === "connect") done({connectTime: new Date(), playerName: playerName, xuid});
}
},
{
name: "playerDisconnect",
callback(data, done) {
const match = data.match(player);
if (!match) return;
const [, action,, playerName, xuid] = match;
if (action === "disconnect") done({connectTime: new Date(), playerName: playerName, xuid});
}
},
{
name: "playerUnknown",
callback(data, done) {
const match = data.match(player);
if (!match) return;
const [, action,, playerName, xuid] = match;
if (!(action === "disconnect" || action === "connect")) done({connectTime: new Date(), playerName: playerName, xuid});
}
},
{
name: "serverStop",
run: (child) => child.writeStdin("stop")
}
];
export async function startServer() {
if (!fsOld.existsSync(serverPath)) throw new Error("Install server fist");
const args: string[] = [];
let command = path.join(serverPath, "bedrock_server");
if (process.platform === "linux" && process.arch !== "x64") {
args.push(command);
if (await execAsync("command -v qemu-x86_64-static").then(() => true).catch(() => false)) command = "qemu-x86_64-static";
else if (await execAsync("command -v box64").then(() => true).catch(() => false)) command = "box64";
else throw new Error("Cannot emulate x64 architecture. Check the documentents in \"https://github.com/The-Bds-Maneger/Bds-Maneger-Core/wiki/Server-Platforms#minecraft-bedrock-server-alpha\"");
}
return new actions(exec(command, args, {cwd: serverPath, maxBuffer: Infinity, env: {LD_LIBRARY_PATH: process.platform === "win32"?undefined:serverPath}}), serverConfig);
}

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

106
src/childPromisses.ts Normal file
View File

@ -0,0 +1,106 @@
import type { ObjectEncodingOptions } from "node:fs";
import { execFile, exec as nodeExec, ExecFileOptions, ChildProcess } from "node:child_process";
import { EventEmitter } from "node:events";
import { promisify } from "node:util";
export {execFile};
export const execAsync = promisify(nodeExec);
// export const execFileAsync = promisify(execFile);
export type execOptions = ObjectEncodingOptions & ExecFileOptions & {stdio?: "ignore"|"inherit"};
export function execFileAsync(command: string): Promise<{stdout: string, stderr: string}>;
export function execFileAsync(command: string, args: string[]): Promise<{stdout: string, stderr: string}>;
export function execFileAsync(command: string, options: execOptions): Promise<{stdout: string, stderr: string}>;
export function execFileAsync(command: string, args: string[], options: execOptions): Promise<{stdout: string, stderr: string}>;
export function execFileAsync(command: string, args?: execOptions|string[], options?: execOptions) {
let childOptions: execOptions = {};
let childArgs: string[] = [];
if (args instanceof Array) childArgs = args; else if (args instanceof Object) childOptions = args as execOptions;
if (options) childOptions = options;
if (childOptions?.env) childOptions.env = {...process.env, ...childOptions.env};
return new Promise<{stdout: string, stderr: string}>((resolve, rejectExec) => {
const child = execFile(command, childArgs, childOptions, (err, out, err2) => {if (err) return rejectExec(err);resolve({stdout: out, stderr: err2});});
if (options?.stdio === "inherit") {
child.stdout.on("data", data => process.stdout.write(data));
child.stderr.on("data", data => process.stderr.write(data));
}
});
}
export class customChild {
private eventMiter = new EventEmitter({captureRejections: false});
public child?: ChildProcess;
public kill(signal?: number|NodeJS.Signals) {if(this.child?.killed) return this.child?.killed;return this.child?.kill(signal);}
public writeStdin(command: string, args?: string[]) {
let toWrite = command;
if (args?.length > 0) toWrite += (" "+args.join(" "));
toWrite+="\n";
this.child.stdin.write(toWrite);
}
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;}
private tempLog = {};
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));
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));
// 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];
const a = lines.pop();
if (a !== "") lines.push(a);
for (const line of lines) {
if (!this.tempLog[to]) {
// console.log(this.tempLog, lines);
this.eventMiter.emit(to, line);
continue;
}
console.log(this.tempLog, lines);
this.tempLog[to]+=line;
this.eventMiter.emit(to, this.tempLog[to]);
this.tempLog[to] = "";
}
}
child.stdout.on("data", data => parseLog("breakStdout", data));
child.stderr.on("data", data => parseLog("breakStderr", 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: 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 Array) childArgs = args; else if (args instanceof Object) childOptions = args as execOptions;
if (options) childOptions = options;
if (childOptions?.env) childOptions.env = {...process.env, ...childOptions.env};
return new customChild(execFile(command, childArgs, childOptions));
}

View File

@ -1,5 +1,3 @@
export default parse;
/** /**
* Parse Proprieties files and return a map of properties. * Parse Proprieties files and return a map of properties.
* *

83
src/config/bedrock.ts Normal file
View File

@ -0,0 +1,83 @@
import { readFile } from "node:fs/promises";
import {} from "prismarine-nbt";
import { serverPath } from "../bedrock";
import * as Proprieties from "./Proprieties";
import * as path from 'node:path';
export type bedrockParseProprieties = {
"server-name": string,
gamemode: "survival"|"creative"|"adventure",
"force-gamemode": boolean,
difficulty: "peaceful"|"easy"|"normal"|"hard",
"allow-cheats": boolean,
"max-players": number,
"online-mode": boolean,
"allow-list": boolean,
"server-port": number,
"server-portv6": number,
"view-distance": number,
"tick-distance": 4|6|8|10|12,
"player-idle-timeout": number,
"max-threads": number,
"level-name": string,
"level-seed": string|number|bigint|null,
"default-player-permission-level": "visitor"|"member"|"operator",
"texturepack-required": boolean,
"content-log-file-enabled": boolean,
"compression-threshold": number,
"server-authoritative-movement": "client-auth"|"server-auth"|"server-auth-with-rewind",
"player-movement-score-threshold": number,
"player-movement-action-direction-threshold": number,
"player-movement-distance-threshold": number,
"player-movement-duration-threshold-in-ms": number,
"correct-player-movement": boolean,
"server-authoritative-block-breaking": boolean,
"chat-restriction": "None"|"Dropped"|"Disabled",
"disable-player-interaction": boolean
};
export async function getConfig(): Promise<bedrockParseProprieties> {
return Proprieties.parse(await readFile(path.join(serverPath, "server.proprieties"), "utf8")) as bedrockParseProprieties;
}
const keys = RegExp("("+(["server-name", "gamemode", "force-gamemode", "difficulty", "allow-cheats", "max-players", "online-mode", "allow-list", "server-port", "server-portv6", "view-distance", "tick-distance", "player-idle-timeout", "max-threads", "level-name", "level-seed", "default-player-permission-level", "texturepack-required", "content-log-file-enabled", "compression-threshold", "server-authoritative-movement", "player-movement-score-threshold", "player-movement-action-direction-threshold", "player-movement-distance-threshold", "player-movement-duration-threshold-in-ms", "correct-player-movement", "server-authoritative-block-breaking", "chat-restriction", "disable-player-interaction"]).join("|")+")")
export function createConfig(config: bedrockParseProprieties): string {
let configString = "";
for (const key of Object.keys(config).filter(a => keys.test(a))) configString += `${key}=${config[key]}\n`;
return configString.trim();
}
/*
console.log(createConfig({
"server-name": "string",
gamemode: "survival",
"force-gamemode": true,
difficulty: "easy",
"allow-cheats": false,
"max-players": 20,
"online-mode": false,
"allow-list": true,
"server-port": 19135,
"server-portv6": 19136,
"view-distance": 32,
"tick-distance": 8,
"player-idle-timeout": 0,
"max-threads": 16,
"level-name": "string",
"level-seed": null,
"default-player-permission-level": "member",
"texturepack-required": true,
"content-log-file-enabled": false,
"compression-threshold": 0,
"server-authoritative-movement": "server-auth-with-rewind",
"player-movement-score-threshold": 0.9,
"player-movement-action-direction-threshold": 0.6,
"player-movement-distance-threshold": 0.6,
"player-movement-duration-threshold-in-ms": 0.6,
"correct-player-movement": false,
"server-authoritative-block-breaking": false,
"chat-restriction": "Disabled",
"disable-player-interaction": false
}));
*/

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;

123
src/globalPlatfroms.ts Normal file
View File

@ -0,0 +1,123 @@
import type { customChild } from "./childPromisses";
import { EventEmitter } from "node:events";
export type playerClass = {[player: string]: {action: "connect"|"disconnect"|"unknown"; date: Date; history: Array<{action: "connect"|"disconnect"|"unknown"; date: Date}>}};
export type playerBase = {playerName: string, connectTime: Date, xuid?: string};
export type actionsPlayer = {
name: "playerConnect"|"playerDisconnect"|"playerUnknown",
callback: (data: string, done: (player: playerBase) => void) => void
}
export type portListen = {port: number, host?: string, type: "TCP"|"UDP"|"TCP/UDP", protocol: "IPv4"|"IPv6"|"IPV4/IPv6"|"Unknown"};
export type actionsPort = {
name: "portListening",
callback: (data: string, done: (portInfo: portListen) => void) => void
}
export type serverStarted = Date;
export type actionsServerStarted = {
name: "serverStarted",
callback: (data: string, done: (started: serverStarted) => void) => void
}
export type actionsServerStop = {
name: "serverStop",
run: (childProcess: customChild) => void
}
export type actionTp = {
name: "tp",
run: (childProcess: customChild, x: number|string, y: number|string, z: number|string) => void
}
export type actionRun = actionsServerStop|actionTp;
export type actionCallback = actionsPlayer|actionsPort|actionsServerStarted|actionsServerStarted;
export type actionConfig = actionCallback|actionRun;
export class actions {
private events = new EventEmitter({captureRejections: false});
public childProcess: customChild;
private stopServerFunction?: (childProcess: customChild) => void;
private tpfunction?: (childProcess: customChild, x: number|string, y: number|string, z: number|string) => void;
public on(act: "playerConnect"|"playerDisconnect"|"playerUnknown", fn: (data: playerBase) => void): this;
public on(act: "portListening", fn: (data: portListen) => void): this;
public on(act: "serverStarted", fn: (data: serverStarted) => void): this;
public on(act: "log_stderr", fn: (data: string) => void): this;
public on(act: "log_stdout", fn: (data: string) => void): this;
public on(act: "exit", fn: (data: {code: number, signal: NodeJS.Signals}) => void): this;
public on(act: string, fn: (...args: any[]) => void) {this.events.on(act, fn); return this;}
public once(act: "playerConnect"|"playerDisconnect"|"playerUnknown", fn: (data: playerBase) => void): this;
public once(act: "portListening", fn: (data: portListen) => void): this;
public once(act: "serverStarted", fn: (data: serverStarted) => void): this;
public once(act: "log_stderr", fn: (data: string) => void): this;
public once(act: "log_stdout", fn: (data: string) => void): this;
public once(act: "exit", fn: (data: {code: number, signal: NodeJS.Signals}) => void): this;
public once(act: string, fn: (...args: any[]) => void) {this.events.once(act, fn); return this;}
public stopServer() {
if (typeof this.stopServer === "undefined") this.childProcess.kill("SIGKILL");
this.stopServerFunction(this.childProcess);
return this;
}
public tp(x: number|string = 0, y: number|string = 0, z: number|string = 0) {
if (typeof this.stopServer === "undefined") throw new Error("TP command not configured!");
this.tpfunction(this.childProcess, x, y, z);
return this;
}
public runCommand(...command: Array<string|number>) {
const psCommand = command.map(a => String(a));
this.childProcess.writeStdin(psCommand.join(" "));
}
public portListening: portListen[] = [];
public playerActions: playerClass = {};
constructor(child: customChild, config: actionConfig[]) {
this.childProcess = child;
child.on("close", data => this.events.emit("exit", data));
child.on("breakStdout", data => this.events.emit("log_stdout", data));
child.on("breakStderr", data => this.events.emit("log_stderr", data));
this.on("portListening", data => this.portListening.push(data));
this.on("playerConnect", (data): any => {
if (!this.playerActions[data.playerName]) return this.playerActions[data.playerName] = {
action: "connect",
date: data.connectTime,
history: [{action: "connect", date: data.connectTime}]
}
this.playerActions[data.playerName].action = "connect";
this.playerActions[data.playerName].date = data.connectTime;
this.playerActions[data.playerName].history.push({action: "connect", date: data.connectTime});
});
this.on("playerDisconnect", (data): any => {
if (!this.playerActions[data.playerName]) return this.playerActions[data.playerName] = {
action: "disconnect",
date: data.connectTime,
history: [{action: "disconnect", date: data.connectTime}]
}
this.playerActions[data.playerName].action = "disconnect";
this.playerActions[data.playerName].date = data.connectTime;
this.playerActions[data.playerName].history.push({action: "disconnect", date: data.connectTime});
});
this.on("playerUnknown", (data): any => {
if (!this.playerActions[data.playerName]) return this.playerActions[data.playerName] = {
action: "unknown",
date: data.connectTime,
history: [{action: "unknown", date: data.connectTime}]
}
this.playerActions[data.playerName].action = "unknown";
this.playerActions[data.playerName].date = data.connectTime;
this.playerActions[data.playerName].history.push({action: "unknown", date: data.connectTime});
});
const actions = config.filter((a: actionCallback) => typeof a?.callback === "function") as actionCallback[];
child.on("breakStdout", data => actions.forEach(fn => fn.callback(data, (...args: any[]) => this.events.emit(fn.name, ...args))));
child.on("breakStderr", data => actions.forEach(fn => fn.callback(data, (...args: any[]) => this.events.emit(fn.name, ...args))));
for (const action of (config.filter((a: actionRun) => typeof a?.run === "function") as actionRun[])) {
if (action.name === "serverStop") this.stopServerFunction = action.run;
}
}
}

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

21
src/httpRequest.ts Normal file
View File

@ -0,0 +1,21 @@
import axios from "axios";
export async function getBuffer(url: string, config?: {body?: any, header?: {[key: string]: string}}): Promise<Buffer> {
const Headers = {};
let Body: any;
if (config) {
if (config.header) Object.keys(config.header).forEach(key => Headers[key] = config.header[key]);
if (config.body) Body = config.body;
}
if (typeof fetch === "undefined") return axios.get(url, {
responseEncoding: "arraybuffer",
responseType: "arraybuffer",
headers: Headers,
data: Body
}).then(({data}) => Buffer.from(data));
return fetch(url, {
method: "GET",
body: typeof Body === "object" ? JSON.stringify(Body, null, 2):Body,
headers: Headers
}).then(res => res.arrayBuffer()).then(res => Buffer.from(res));
}

View File

@ -1,5 +1,4 @@
export * as globalType from "./globalType"; export * as Bedrock from "./bedrock";
export * as bedrock from "./bedrock/index"; export * as Java from "./java";
export * as pocketmine from "./pocketmine/index"; export * as Spigot from "./spigot";
export * as java from "./java/index"; export * as PocketmineMP from "./pocketmine";
export * as spigot from "./spigot/index";

49
src/java.ts Normal file
View File

@ -0,0 +1,49 @@
import * as path from "node:path";
import * as fs from "node:fs/promises";
import * as fsOld from "node:fs";
import { getJavaJar } from "@the-bds-maneger/server_versions";
import { serverRoot } from "./pathControl";
import { exec } from "./childPromisses";
import { actions, actionConfig } from './globalPlatfroms';
export const serverPath = path.join(serverRoot, "java");
const jarPath = path.join(serverPath, "server.jar");
export const started = /\[.*\].*\s+Done\s+\(.*\)\!.*/;
export const portListen = /Starting\s+Minecraft\s+server\s+on\s+(.*)\:(\d+)/;
export async function installServer(version: string|boolean) {
if (!fsOld.existsSync(serverPath)) await fs.mkdir(serverPath, {recursive: true});
await fs.writeFile(jarPath, await getJavaJar(version));
}
const serverConfig: actionConfig[] = [
{
name: "serverStarted",
callback(data, done) {
// [22:35:26] [Server thread/INFO]: Done (6.249s)! For help, type "help"
if (started.test(data)) done(new Date());
}
},
{
name: "portListening",
callback(data, done) {
const portParse = data.match(portListen);
if (!!portParse) done({port: parseInt(portParse[2]), host: (portParse[1]||"").trim()||undefined, type: "TCP", protocol: "IPV4/IPv6",});
}
},
{
name: "serverStop",
run: (child) => child.writeStdin("stop")
}
];
export async function startServer(Config?: {maxMemory?: number, minMemory?: number}) {
if (!fsOld.existsSync(jarPath)) throw new Error("Install server fist.");
const command = "java";
const args = ["-jar"];
if (Config) {
if (Config?.minMemory) args.push(`-Xms${Config?.minMemory}m`);
if (Config?.maxMemory) args.push(`-Xmx${Config?.maxMemory}m`);
}
args.push(jarPath);
return new actions(exec(command, args, {cwd: serverPath, maxBuffer: Infinity}), serverConfig);
}

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

@ -0,0 +1,32 @@
import * as path from "node:path";
import * as fs from "node:fs/promises";
import { existsSync as fsExistsSync } from "node:fs";
import { worldFolder } from "../pathControl";
import { serverPath as bedrockServerPath } from "../bedrock";
import { serverPath as pocketmineServerPath } from "../pocketmine";
export const bedrockWorld = path.join(worldFolder, "bedrock");
export const bedrockServerWorld = path.join(bedrockServerPath, "worlds");
export async function linkBedrock() {
if (!fsExistsSync(bedrockWorld)) await fs.mkdir(bedrockWorld, {recursive: true});
if (fsExistsSync(bedrockServerWorld)) {
if (await fs.realpath(bedrockWorld) === bedrockServerWorld) return;
for (const folder of await fs.readdir(bedrockServerWorld)) await fs.cp(path.join(bedrockServerWorld, folder), path.join(bedrockWorld, folder), {recursive: true, force: true, preserveTimestamps: true, verbatimSymlinks: true});
if (!fsExistsSync(bedrockServerWorld+"_backup")) await fs.rename(bedrockServerWorld, bedrockServerWorld+"_backup");
}
await fs.symlink(bedrockWorld, bedrockServerWorld);
return;
}
export const pocketmineWorld = path.join(worldFolder, "pocketmine");
export const pocketmineServerWorld = path.join(pocketmineServerPath, "worlds");
export async function linkPocketmine() {
if (!fsExistsSync(pocketmineWorld)) await fs.mkdir(pocketmineWorld, {recursive: true});
if (fsExistsSync(pocketmineServerWorld)) {
if (await fs.realpath(pocketmineWorld) === pocketmineServerWorld) return;
for (const folder of await fs.readdir(pocketmineServerWorld)) await fs.cp(path.join(pocketmineServerWorld, folder), path.join(pocketmineWorld, folder), {recursive: true, force: true, preserveTimestamps: true, verbatimSymlinks: true});
if (!fsExistsSync(pocketmineServerWorld+"_backup")) await fs.rename(pocketmineServerWorld, pocketmineServerWorld+"_backup");
}
await fs.symlink(pocketmineWorld, pocketmineServerWorld);
return;
}

View File

@ -1,7 +1,20 @@
import * as os from "node:os"; import * as fs from "node:fs/promises";
import * as fsOld from "node:fs";
import * as path from "node:path"; import * as path from "node:path";
import * as os from "node:os";
const bdsCorePathHome = path.join(os.homedir(), "bds_core"); // bds Root
export const serverRoot = (!!process.env.SERVER_PATH) ? path.resolve(process.env.SERVER_PATH) : path.join(bdsCorePathHome, "servers"); export const bdsRoot = process.env.BDS_HOME||path.join(os.homedir(), ".bdsManeger");
export const backupRoot = (!!process.env.BACKUP_PATH) ? path.resolve(process.env.BACKUP_PATH) : path.join(bdsCorePathHome, "backups"); if (!fsOld.existsSync(bdsRoot)) fs.mkdir(bdsRoot, {recursive: true}).then(() => console.log("Bds Root created"));
export const worldStorageRoot = (!!process.env.WORLD_STORAGE) ? path.resolve(process.env.WORLD_STORAGE) : path.join(bdsCorePathHome, "worlds");
// Server Folder
export const serverRoot = path.join(bdsRoot, "Servers");
if (!fsOld.existsSync(serverRoot)) fs.mkdir(serverRoot, {recursive: true});
// Worlds Folder
export const worldFolder = path.join(bdsRoot, "Worlds");
if (!fsOld.existsSync(worldFolder)) fs.mkdir(serverRoot, {recursive: true});
// Bds backup
export const backupFolder = path.join(bdsRoot, "Backup");
if (!fsOld.existsSync(backupFolder)) fs.mkdir(backupFolder, {recursive: true});

131
src/pocketmine.ts Normal file
View File

@ -0,0 +1,131 @@
import * as path from "node:path";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as tar from "tar";
import { existsSync as fsExistsSync } from "node:fs";
import { getPocketminePhar, versionAPIs } from "@the-bds-maneger/server_versions";
import { execFileAsync, exec } from './childPromisses';
import { serverRoot } from "./pathControl";
import { getBuffer } from "./httpRequest";
import { actionConfig, actions } from './globalPlatfroms';
import AdmZip from "adm-zip";
import { promisify } from 'node:util';
export { pocketmineServerWorld, pocketmineWorld, linkPocketmine } from "./linkWorlds/bedrock_pocketmine";
export const serverPath = path.join(serverRoot, "pocketmine");
export const serverPhar = path.join(serverPath, "pocketmine.phar");
export const phpBinPath = path.join(serverPath, "bin", (process.platform === "win32"?"php":"bin"), "php");
async function Readdir(pathRead: string, filter?: RegExp[]) {
if (!filter) filter = [/.*/];
const fixedPath = path.resolve(pathRead);
const files: {path: string, name: string}[] = [];
for (const file of await fs.readdir(fixedPath)) {
const FullFilePath = path.join(fixedPath, file);
const stats = await fs.stat(FullFilePath);
if (stats.isDirectory()) files.push(...(await Readdir(FullFilePath, filter)));
else if (stats.isSymbolicLink()) {
const realPath = await fs.realpath(FullFilePath);
const statsSys = await fs.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;
}
async function buildPhp() {
if (process.platform === "win32") throw new Error("Script is to Linux and MacOS");
if (fsExistsSync(path.resolve(serverPath, "bin"))) await fs.rm(path.resolve(serverPath, "bin"), {recursive: true});
const tempFolder = path.join(os.tmpdir(), "bdsPhp_"+(Math.random()*19999901).toString(16).replace(".", "").replace(/[0-9]/g, (_, a) =>a=="1"?"a":a=="2"?"b":a=="3"?"S":"k"));
if (!fsExistsSync(tempFolder)) fs.mkdir(tempFolder, {recursive: true});
await fs.writeFile(path.join(tempFolder, "build.sh"), await getBuffer("https://raw.githubusercontent.com/pmmp/php-build-scripts/stable/compile.sh"));
await fs.chmod(path.join(tempFolder, "build.sh"), "777");
console.info("Building PHP!");
await execFileAsync(path.join(tempFolder, "build.sh"), ["-j"+os.cpus().length], {cwd: tempFolder, stdio: "inherit"});
await fs.cp(path.join(tempFolder, "bin", (await fs.readdir(path.join(tempFolder, "bin")))[0]), path.join(serverPath, "bin"), {force: true, recursive: true, preserveTimestamps: true, verbatimSymlinks: true});
console.log("PHP Build success!");
}
async function installPhp(): Promise<void> {
const file = (await getBuffer(`${versionAPIs[0]}/pocketmine/bin?os=${process.platform}&arch=${process.arch}`).then(res => JSON.parse(res.toString("utf8")) as {url: string, name: string}[]))[0];
if (!file) return buildPhp();
if (fsExistsSync(path.resolve(serverPath, "bin"))) await fs.rm(path.resolve(serverPath, "bin"), {recursive: true});
await fs.mkdir(path.resolve(serverPath, "bin"), {recursive: true});
// Tar.gz
if (/tar\.gz/.test(file.name)) {
await fs.writeFile(path.join(os.tmpdir(), file.name), await getBuffer(file.url));
await tar.extract({file: path.join(os.tmpdir(), file.name), C: path.join(serverPath, "bin"), keep: true, p: true, noChmod: false});
} else {
const zip = new AdmZip(await getBuffer(file.url));
await promisify(zip.extractAllToAsync)(serverPath, false, true);
}
if (process.platform === "linux"||process.platform === "android"||process.platform === "darwin") {
const ztsFind = await Readdir(path.resolve(serverPath, "bin"), [/.*debug-zts.*/]);
if (ztsFind.length > 0) {
const phpIniPath = (await Readdir(path.resolve(serverPath, "bin"), [/php\.ini$/]))[0].path;
let phpIni = await fs.readFile(phpIniPath, "utf8");
if (phpIni.includes("extension_dir")) await fs.writeFile(phpIniPath, phpIni.replace(/extension_dir=.*/g, ""));
phpIni = phpIni+`\nextension_dir=${path.resolve(ztsFind[0].path, "..")}`
await fs.writeFile(phpIniPath, phpIni);
}
}
// test it's works php
await fs.writeFile(path.join(os.tmpdir(), "test.php"), `<?php echo "Hello World";`);
await execFileAsync(phpBinPath, ["-f", path.join(os.tmpdir(), "test.php")]).catch(buildPhp);
}
export async function installServer(version: string|boolean) {
if (!fsExistsSync(serverPath)) await fs.mkdir(serverPath, {recursive: true});
await installPhp();
await fs.writeFile(serverPhar, await getPocketminePhar(version));
}
// [16:47:35.405] [Server thread/INFO]: Minecraft network interface running on 0.0.0.0:19132
export const portListen = /\[.*\]:\s+Minecraft\s+network\s+interface\s+running\s+on\s+(([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+|\[[A-Za-z0-9:]+\]|):([0-9]+))/;
export const started = /\[.*\].*\s+Done\s+\(.*\)\!.*/;
export const player = /[.*]:\s+(.*)\s+(.*)\s+the\s+game/gi;
const serverConfig: actionConfig[] = [
{
name: "portListening",
callback(data, done) {
const portParse = data.match(portListen);
if (!portParse) return;
const [,, host, port] = portParse;
done({
protocol: /::/.test(host?.trim())?"IPv6":/[0-9]+\.[0-9]+/.test(host?.trim())?"IPv4":"IPV4/IPv6",
type: "UDP",
port: parseInt(port),
host: host?.trim()
});
}
},
{
name: "serverStarted",
callback(data, done) {
// [22:35:26] [Server thread/INFO]: Done (6.249s)! For help, type "help"
if (started.test(data)) done(new Date());
}
},
{
name: "playerConnect",
callback(data, done) {
data;
},
},
{
name: "serverStop",
run: (child) => child.writeStdin("stop")
}
];
export async function startServer() {
if (!fsExistsSync(serverPath)) throw new Error("Install server fist!");
return new actions(exec(phpBinPath, [serverPhar], {cwd: serverPath, maxBuffer: Infinity}), serverConfig);
}

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

50
src/spigot.ts Normal file
View File

@ -0,0 +1,50 @@
import * as path from "node:path";
import * as fs from "node:fs/promises";
import * as fsOld from "node:fs";
import { getSpigotJar } from "@the-bds-maneger/server_versions";
import { serverRoot } from "./pathControl";
import { exec } from "./childPromisses";
import { actions, actionConfig } from './globalPlatfroms';
export const serverPath = path.join(serverRoot, "spigot");
const jarPath = path.join(serverPath, "server.jar");
export const started = /\[.*\].*\s+Done\s+\(.*\)\!.*/;
export const portListen = /Starting\s+Minecraft\s+server\s+on\s+(.*)\:(\d+)/;
export async function installServer(version: string|boolean) {
if (!fsOld.existsSync(serverPath)) await fs.mkdir(serverPath, {recursive: true});
await fs.writeFile(jarPath, await getSpigotJar(version));
}
const serverConfig: actionConfig[] = [
{
name: "serverStarted",
callback(data, done) {
// [22:35:26] [Server thread/INFO]: Done (6.249s)! For help, type "help"
if (started.test(data)) done(new Date());
}
},
{
name: "portListening",
callback(data, done) {
const portParse = data.match(portListen);
if (!!portParse) done({port: parseInt(portParse[2]), host: (portParse[1]||"").trim()||undefined, type: "TCP", protocol: "IPV4/IPv6",});
}
},
{
name: "serverStop",
run: (child) => child.writeStdin("stop")
}
];
export async function startServer(Config?: {maxMemory?: number, minMemory?: number}) {
if (!fsOld.existsSync(jarPath)) throw new Error("Install server fist.");
const command = "java";
const args = ["-jar"];
if (Config) {
if (Config?.minMemory) args.push(`-Xms${Config?.minMemory}m`);
if (Config?.maxMemory) args.push(`-Xmx${Config?.maxMemory}m`);
}
args.push(jarPath);
return new actions(exec(command, args, {cwd: serverPath, maxBuffer: Infinity}), serverConfig);
}

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

14
tests/bedrock.ts Normal file
View File

@ -0,0 +1,14 @@
import { installServer, startServer } from "../src/bedrock";
describe("Bedrock", () => {
it("Install and Start", async function(){
this.timeout(1000*60*60*15);
await installServer("latest");
const serverManeger = await startServer();
serverManeger.on("log_stdout", console.log);
serverManeger.on("log_stderr", console.log);
serverManeger.on("portListening", console.log);
serverManeger.once("serverStarted", () => serverManeger.stopServer());
return new Promise((done, reject) => serverManeger.on("exit", ({code}) => code === 0?done():reject(new Error("Exit another code "+code))));
});
});

22
tests/pocketmine.ts Normal file
View File

@ -0,0 +1,22 @@
import { installServer, startServer } from "../src/pocketmine";
describe("Pocketmine", () => {
it("Install and Start", async function() {
this.timeout(1000*60*60*15);
await installServer("latest");
const serverManeger = await startServer();
serverManeger.on("log_stdout", console.log);
serverManeger.on("log_stderr", console.info);
serverManeger.on("portListening", console.log);
serverManeger.on("log_stdout", data => {
if(/set-up.*wizard/.test(data)) {
serverManeger.runCommand("eng");
serverManeger.runCommand("y");
serverManeger.runCommand("y");
serverManeger.runCommand("");
}
});
serverManeger.on("serverStarted", () => serverManeger.stopServer());
return new Promise((done, reject) => serverManeger.on("exit", ({code}) => code === 0?done():reject(new Error("Exit another code "+code))));
});
});

View File

@ -1,16 +1,10 @@
{ {
"include": ["src/**/*"],
"exclude": [
"node_modules/",
"test/"
],
"compilerOptions": { "compilerOptions": {
"esModuleInterop": true, "esModuleInterop": true,
"moduleResolution": "node", "moduleResolution": "node",
"module": "commonjs", "module": "commonjs",
"outDir": "./dist/cjs", "outDir": "./dist",
"declaration": true, "declaration": true,
"declarationDir": "./dist/dts",
"strict": false, "strict": false,
"noUnusedLocals": true, "noUnusedLocals": true,
"noImplicitReturns": true, "noImplicitReturns": true,
@ -19,7 +13,16 @@
"allowJs": true, "allowJs": true,
"target": "ES6", "target": "ES6",
"paths": { "paths": {
"@/*": ["./node_modules/@*"] "@/*": [
} "./node_modules/@*"
]
} }
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules/",
"test/"
]
} }