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