Core apis #528
@@ -1,37 +1,3 @@
 | 
			
		||||
# Bds Maneger Core
 | 
			
		||||
 | 
			
		||||
Basic core to install, update and manage several minecraft servers automatically, depending on a few dependencies, the basic being **Nodejs**.
 | 
			
		||||
 | 
			
		||||
## Servers supports and TODO
 | 
			
		||||
 | 
			
		||||
**Bedrock Mojang**:
 | 
			
		||||
  - [x] Install/Update.
 | 
			
		||||
  - [ ] Hot backup.
 | 
			
		||||
  - [x] Start.
 | 
			
		||||
  - [x] Port Listened.
 | 
			
		||||
  - [ ] Player connect/disconnect/spawn.
 | 
			
		||||
  - [ ] Player kick/ban.
 | 
			
		||||
 | 
			
		||||
**Pocketmine PMMP**:
 | 
			
		||||
  - [x] Install/Update.
 | 
			
		||||
  - [ ] Hot backup.
 | 
			
		||||
  - [x] Start.
 | 
			
		||||
  - [ ] Port listened.
 | 
			
		||||
  - [ ] Player connect/disconnect.
 | 
			
		||||
  - [ ] Player kick/ban.
 | 
			
		||||
 | 
			
		||||
**Powernukkit** and **Cloudbust**:
 | 
			
		||||
  - [x] Install/Update.
 | 
			
		||||
  - 🚫 Hot backup.
 | 
			
		||||
  - [x] Start.
 | 
			
		||||
  - [ ] Port listened.
 | 
			
		||||
  - [ ] Player connect/disconnect.
 | 
			
		||||
  - [ ] Player kick/ban.
 | 
			
		||||
 | 
			
		||||
**Java Mojang**, **Purpur**, **Paper** and **Spigot**:
 | 
			
		||||
  - [x] Install/Update.
 | 
			
		||||
  - 🚫 Hot Backup.
 | 
			
		||||
  - [x] Start.
 | 
			
		||||
  - [ ] Port listened.
 | 
			
		||||
  - [ ] Player connect/disconect action.
 | 
			
		||||
  - [ ] Player kick/ban.
 | 
			
		||||
Um simples nucleo com exposição de APIs para integração com os Servidores de Minecraft para gerenciamento no Nodejs.
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "@the-bds-maneger/core",
 | 
			
		||||
  "version": "6.0.4",
 | 
			
		||||
  "description": "",
 | 
			
		||||
  "description": "Core APIs to maneger Minecraft servers platforms",
 | 
			
		||||
  "main": "src/index.js",
 | 
			
		||||
  "types": "src/index.d.ts",
 | 
			
		||||
  "type": "module",
 | 
			
		||||
@@ -11,24 +11,22 @@
 | 
			
		||||
    "access": "public"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "prepack": "tsc --build --clean && tsc",
 | 
			
		||||
    "postpack": "tsc --build --clean",
 | 
			
		||||
    "build": "tsc --build --clean && tsc"
 | 
			
		||||
    "prepack": "tsc --build --clean && tsc --build",
 | 
			
		||||
    "postpack": "tsc --build --clean"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@sirherobrine23/cloud": "^3.6.11",
 | 
			
		||||
    "@sirherobrine23/extends": "^3.6.11",
 | 
			
		||||
    "@sirherobrine23/http": "^3.6.11",
 | 
			
		||||
    "adm-zip": "0.5.10",
 | 
			
		||||
    "sanitize-filename": "^1.6.3",
 | 
			
		||||
    "semver": "^7.5.1",
 | 
			
		||||
    "tar": "^6.1.15",
 | 
			
		||||
    "unzip-stream": "^0.3.1",
 | 
			
		||||
    "unzipper": "^0.10.14"
 | 
			
		||||
    "xml-js": "^1.6.11"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@types/adm-zip": "0.5.0",
 | 
			
		||||
    "@types/semver": "^7.5.0",
 | 
			
		||||
    "@types/tar": "^6.1.5",
 | 
			
		||||
    "@types/unzip-stream": "^0.3.1",
 | 
			
		||||
    "@types/unzipper": "^0.10.6"
 | 
			
		||||
    "@types/tar": "^6.1.5"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
import { oracleBucket } from "@sirherobrine23/cloud";
 | 
			
		||||
/**
 | 
			
		||||
 * Bucket readonly
 | 
			
		||||
 */
 | 
			
		||||
export const oracleStorage = oracleBucket.oracleBucketPreAuth("sa-saopaulo-1", "grwodtg32n4d", "bdsFiles", "0IKM-5KFpAF8PuWoVe86QFsF4sipU2rXfojpaOMEdf4QgFQLcLlDWgMSPHWmjf5W");
 | 
			
		||||
							
								
								
									
										5
									
								
								packages/core/src/internalClouds.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								packages/core/src/internalClouds.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
import { oracleBucket } from "@sirherobrine23/cloud";
 | 
			
		||||
/**
 | 
			
		||||
 * Bucket readonly
 | 
			
		||||
 */
 | 
			
		||||
export const bdsFilesBucket = oracleBucket.oracleBucketPreAuth("sa-saopaulo-1", "grwodtg32n4d", "bdsFiles", "0IKM-5KFpAF8PuWoVe86QFsF4sipU2rXfojpaOMEdf4QgFQLcLlDWgMSPHWmjf5W");
 | 
			
		||||
							
								
								
									
										1
									
								
								packages/core/src/platform/bedrock/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packages/core/src/platform/bedrock/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
export * as listVersion from "./listVersion.js";
 | 
			
		||||
							
								
								
									
										168
									
								
								packages/core/src/platform/bedrock/listVersion.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								packages/core/src/platform/bedrock/listVersion.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,168 @@
 | 
			
		||||
import { http, Github } from "@sirherobrine23/http";
 | 
			
		||||
import util from "node:util";
 | 
			
		||||
import xml from "xml-js";
 | 
			
		||||
 | 
			
		||||
export interface mojangInfo {
 | 
			
		||||
  releaseDate: Date;
 | 
			
		||||
  release: "oficial" | "beta";
 | 
			
		||||
  files: { [P in NodeJS.Platform]?: { [A in NodeJS.Architecture]?: string } }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const mojangCache = new Map<string, mojangInfo>();
 | 
			
		||||
export async function listMojang() {
 | 
			
		||||
  const versions = await http.jsonRequestBody<{ version: string, release?: "stable" | "preview", date: string, url: { [P in NodeJS.Platform]?: { [A in NodeJS.Architecture]?: string } } }[]>("https://raw.githubusercontent.com/Sirherobrine23/BedrockFetch/main/versions/all.json");
 | 
			
		||||
  versions.forEach(rel => mojangCache.has(rel.version) ? null : mojangCache.set(rel.version, {
 | 
			
		||||
    releaseDate: new Date(rel.release),
 | 
			
		||||
    release: !rel.release ? "oficial" : rel.release === "preview" ? "beta" : "oficial",
 | 
			
		||||
    files: rel.url
 | 
			
		||||
  }));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type powerNukkitRelease = {
 | 
			
		||||
  version: string;
 | 
			
		||||
  minecraftVersion: string;
 | 
			
		||||
  releaseTime: number;
 | 
			
		||||
  commitId: string;
 | 
			
		||||
  snapshotBuild?: number;
 | 
			
		||||
  artefacts: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface powernukkitDownload {
 | 
			
		||||
  version: string;
 | 
			
		||||
  releaseDate: Date;
 | 
			
		||||
  releaseType: "snapshot"|"oficial";
 | 
			
		||||
  url: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const powernukkitCache = new Map<string, powernukkitDownload>();
 | 
			
		||||
export async function listPowernukkitProject() {
 | 
			
		||||
  const releases = await http.jsonRequestBody<{[k in "releases"|"snapshots"]: powerNukkitRelease[]}>("https://raw.githubusercontent.com/PowerNukkit/powernukkit-version-aggregator/master/powernukkit-versions.json");
 | 
			
		||||
  const releasesData = (Object.keys(releases) as (keyof typeof releases)[]).map(releaseType => releases[releaseType].map(data => ({...data, releaseType}))).flat(1).sort((b, a) => Math.min(1, Math.max(-1, a.releaseTime - b.releaseTime)));
 | 
			
		||||
  for (const data of releasesData) {
 | 
			
		||||
    if (powernukkitCache.has(data.minecraftVersion)) continue;
 | 
			
		||||
    const releateDate = new Date(data.releaseTime);
 | 
			
		||||
    const getArtefactExtension = (artefactId: string) => (artefactId.includes("REDUCED_JAR")) ? ".jar" : (artefactId.includes("REDUCED_SOURCES_JAR")) ? "-sources.jar" : (artefactId.includes("SHADED_JAR")) ? "-shaded.jar" : (artefactId.includes("SHADED_SOURCES_JAR")) ? "-shaded-sources.jar" : (artefactId.includes("JAVADOC_JAR")) ? "-javadoc.jar" : ".unknown";
 | 
			
		||||
    function buildArtefactUrl(data: any, artefactId?: string) {
 | 
			
		||||
      const buildReleaseArtefactUrl = (data: any, artefactId?: string) => !data.artefacts.includes(artefactId) ? null : util.format("https://search.maven.org/remotecontent?filepath=org/powernukkit/powernukkit/%s/powernukkit-%s%s", data.version, data.version, getArtefactExtension(artefactId));
 | 
			
		||||
      const buildSnapshotArtefactUrl = (data: any, artefactId?: string) => !data.artefacts.includes(artefactId) ? null : util.format("https://oss.sonatype.org/content/repositories/snapshots/org/powernukkit/powernukkit/%s-SNAPSHOT/powernukkit-%s-%s%s", data.version.substring(0, data.version.indexOf("-SNAPSHOT")), data.version.substring(0, data.version.indexOf("-SNAPSHOT")), releateDate.getUTCFullYear().toString().padStart(4, "0") + (releateDate.getUTCMonth() + 1).toString().padStart(2, "0") + releateDate.getUTCDate().toString().padStart(2, "0") + "." + releateDate.getUTCHours().toString().padStart(2, "0") + releateDate.getUTCMinutes().toString().padStart(2, "0") + releateDate.getUTCSeconds().toString().padStart(2, "0") + "-" + data.snapshotBuild, getArtefactExtension(artefactId));
 | 
			
		||||
      if (artefactId == "GIT_SOURCE") {
 | 
			
		||||
        if (data.commitId) return util.format("https://github.com/PowerNukkit/PowerNukkit/tree/%s", data.commitId);
 | 
			
		||||
        else if (data.snapshotBuild && data.artefacts.includes("SHADED_SOURCES_JAR")) return buildSnapshotArtefactUrl(data, "SHADED_SOURCES_JAR");
 | 
			
		||||
        else if (data.snapshotBuild && data.artefacts.includes("REDUCED_SOURCES_JAR")) return buildSnapshotArtefactUrl(data, "REDUCED_SOURCES_JAR");
 | 
			
		||||
        else if (data.artefacts.includes("SHADED_SOURCES_JAR")) return buildReleaseArtefactUrl(data, "SHADED_SOURCES_JAR");
 | 
			
		||||
        else if (data.artefacts.includes("REDUCED_SOURCES_JAR")) return buildReleaseArtefactUrl(data, "REDUCED_SOURCES_JAR");
 | 
			
		||||
      } else if (data.snapshotBuild) return buildSnapshotArtefactUrl(data, artefactId);
 | 
			
		||||
      return buildReleaseArtefactUrl(data, artefactId);
 | 
			
		||||
    }
 | 
			
		||||
    const artefacts = data.artefacts.reduce<{[key: string]: string}>((acc, artefactId) => {acc[artefactId] = buildArtefactUrl(data, artefactId); return acc;}, {});
 | 
			
		||||
    if (!(artefacts.SHADED_JAR || artefacts.REDUCED_JAR)) continue;
 | 
			
		||||
    powernukkitCache.set(data.minecraftVersion, {
 | 
			
		||||
      version: data.minecraftVersion,
 | 
			
		||||
      releaseDate: releateDate,
 | 
			
		||||
      releaseType: data.releaseType === "releases" ? "oficial" : "snapshot",
 | 
			
		||||
      url: artefacts.SHADED_JAR || artefacts.REDUCED_JAR,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface cloudburstDownload {
 | 
			
		||||
  releaseDate: Date;
 | 
			
		||||
  url: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const nukkitCache = new Map<string, cloudburstDownload>();
 | 
			
		||||
export const cloudburstCache = new Map<string, cloudburstDownload>();
 | 
			
		||||
export async function listCloudburstProject() {
 | 
			
		||||
  const Projects = [ "Nukkit", "Server" ] as const;
 | 
			
		||||
  for (const Project of Projects) {
 | 
			
		||||
    const { body: { jobs } } = await http.jsonRequest<{jobs: {name: string, _class: string}[]}>(`https://ci.opencollab.dev/job/NukkitX/job/${Project}/api/json`);
 | 
			
		||||
    const buildFiles = await Promise.all(jobs.filter(b => b._class === "org.jenkinsci.plugins.workflow.job.WorkflowJob").map(b => b.name).map(async branch => {
 | 
			
		||||
      const { body: { builds } } = await http.jsonRequest<{builds: {_class: string, number: number, url: string}[]}>(`https://ci.opencollab.dev/job/NukkitX/job/${Project}/job/${branch}/api/json`);
 | 
			
		||||
      return Promise.all(builds.map(async build => {
 | 
			
		||||
        const { body: { artifacts, result, timestamp, actions } } = await http.jsonRequest<{result: "SUCCESS", actions: {_class: string, [k: string]: any}[], timestamp: number, artifacts: {displayPath: string, fileName: string, relativePath: string}[]}>(`https://ci.opencollab.dev/job/NukkitX/job/${Project}/job/${branch}/${build.number}/api/json`);
 | 
			
		||||
        if (result !== "SUCCESS") return [];
 | 
			
		||||
        const branchBuild = actions.find(r => typeof r["buildsByBranchName"] === "object");
 | 
			
		||||
        if (!branch) return [];
 | 
			
		||||
        const commitID = ((branchBuild?.buildsByBranchName[Object.keys(branchBuild.buildsByBranchName).at(0)]?.marked?.SHA1 || branchBuild.buildsByBranchName[Object.keys(branchBuild.buildsByBranchName).at(0)]?.revision?.SHA1) as string|undefined)
 | 
			
		||||
        let mcpeVersion: string;
 | 
			
		||||
        if (Project === "Server") {
 | 
			
		||||
          const json = xml.xml2js((await http.bufferRequestBody(`https://raw.githubusercontent.com/CloudburstMC/Server/${commitID}/pom.xml`)).toString("utf8"), {compact: true});
 | 
			
		||||
          const info = json["project"].dependencies.dependency.find(dep => dep.groupId._text === "com.nukkitx");
 | 
			
		||||
          mcpeVersion = info.version._text;
 | 
			
		||||
        } else {
 | 
			
		||||
          try {
 | 
			
		||||
            // https://raw.githubusercontent.com/CloudburstMC/Nukkit/master/src/main/java/cn/nukkit/network/protocol/ProtocolInfo.java
 | 
			
		||||
            const lines = (await http.bufferRequestBody(`https://raw.githubusercontent.com/CloudburstMC/Nukkit/${commitID}/src/main/java/cn/nukkit/network/protocol/ProtocolInfo.java`)).toString("utf8").split("\n");
 | 
			
		||||
            const versions = lines.filter(l => l.trim().startsWith("String") && l.toLowerCase().includes("version")).reduce<{[k: string]: string}>((acc, line) => {
 | 
			
		||||
              line = line.trim().slice(6).trim();
 | 
			
		||||
              const i = line.indexOf("=");
 | 
			
		||||
              const def = line.slice(0, i).trim();
 | 
			
		||||
              let version = line.slice(i+1).trim().slice(1, -1);
 | 
			
		||||
              if (version.endsWith("\"")) version = version.slice(0, -1).trim();
 | 
			
		||||
              if (version.startsWith("v")) version = version.slice(1);
 | 
			
		||||
              if (!(version.includes("+"))) acc[def] = version;
 | 
			
		||||
              return acc;
 | 
			
		||||
            }, {});
 | 
			
		||||
            mcpeVersion = versions.MINECRAFT_VERSION || versions.MINECRAFT_VERSION_NETWORK;
 | 
			
		||||
          } catch {
 | 
			
		||||
            return [];
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        if (!mcpeVersion) return [];
 | 
			
		||||
        return artifacts.filter(f => f.relativePath.endsWith(".jar")).map(target => ({
 | 
			
		||||
          buildNumber: build.number,
 | 
			
		||||
          branch,
 | 
			
		||||
          mcpeVersion,
 | 
			
		||||
          releaseDate: new Date(timestamp),
 | 
			
		||||
          url: `https://ci.opencollab.dev/job/NukkitX/job/${Project}/job/${branch}/${build.number}/artifact/${target.relativePath}`,
 | 
			
		||||
        }));
 | 
			
		||||
      }));
 | 
			
		||||
    })).then(r => r.flat(2));
 | 
			
		||||
    for (const build of buildFiles.sort((b, a) => a.releaseDate.getTime() - b.releaseDate.getTime())) {
 | 
			
		||||
      if (Project === "Server") {
 | 
			
		||||
        if (cloudburstCache.has(build.mcpeVersion)) continue;
 | 
			
		||||
        cloudburstCache.set(build.mcpeVersion, {
 | 
			
		||||
          releaseDate: build.releaseDate,
 | 
			
		||||
          url: build.url
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        if (nukkitCache.has(build.mcpeVersion)) continue;
 | 
			
		||||
        nukkitCache.set(build.mcpeVersion, {
 | 
			
		||||
          releaseDate: build.releaseDate,
 | 
			
		||||
          url: build.url
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface pocketmineDownload {
 | 
			
		||||
  releateDate: Date;
 | 
			
		||||
  releaseType: "preview" | "oficial";
 | 
			
		||||
  url: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const pocketmineCache = new Map<string, pocketmineDownload>();
 | 
			
		||||
const pocketmineGithub = await Github.repositoryManeger("pmmp", "PocketMine-MP");
 | 
			
		||||
export async function listPocketmineProject() {
 | 
			
		||||
  const pocketmineReleases = (await pocketmineGithub.release.getRelease()).filter(rel => (rel.assets.find(assert => assert.name.endsWith(".phar")) ?? {}).browser_download_url);
 | 
			
		||||
  for (const data of pocketmineReleases) {
 | 
			
		||||
    if (pocketmineCache.has(data.tag_name)) continue;
 | 
			
		||||
    const assest = data.assets.find(assert => assert.name.endsWith(".phar"));
 | 
			
		||||
    pocketmineCache.set(data.tag_name, {
 | 
			
		||||
      releaseType: data.prerelease ? "preview" : "oficial",
 | 
			
		||||
      releateDate: new Date(assest.created_at),
 | 
			
		||||
      url: assest.browser_download_url
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getCacheVersions() {
 | 
			
		||||
  return {
 | 
			
		||||
    mojang: Array.from(mojangCache.keys()).reduce<{[k: string]: mojangInfo}>((acc, key) => {acc[key] = mojangCache.get(key); return acc;}, {}),
 | 
			
		||||
    pocketmine: Array.from(pocketmineCache.keys()).reduce<{[k: string]: pocketmineDownload}>((acc, key) => {acc[key] = pocketmineCache.get(key); return acc;}, {}),
 | 
			
		||||
    powernukkit: Array.from(powernukkitCache.keys()).reduce<{[k: string]: powernukkitDownload}>((acc, key) => {acc[key] = powernukkitCache.get(key); return acc;}, {}),
 | 
			
		||||
    nukkit: Array.from(nukkitCache.keys()).reduce<{[k: string]: cloudburstDownload}>((acc, key) => {acc[key] = nukkitCache.get(key); return acc;}, {}),
 | 
			
		||||
    cloudburst: Array.from(cloudburstCache.keys()).reduce<{[k: string]: cloudburstDownload}>((acc, key) => {acc[key] = cloudburstCache.get(key); return acc;}, {}),
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								packages/core/src/platform/bedrock/main.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								packages/core/src/platform/bedrock/main.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
import { getCacheVersions } from "./listVersion.js";
 | 
			
		||||
import { serverManeger } from "../../serverRun.js";
 | 
			
		||||
import semver from "semver";
 | 
			
		||||
 | 
			
		||||
export type platforms = "mojang"|"pocketmine"|"powernukkit"|"nukkit"|"cloudburst";
 | 
			
		||||
export async function installServer({platform, version}: {platform?: platforms, version?: string} = {}) {
 | 
			
		||||
  if (!platform) platform = "mojang";
 | 
			
		||||
  else if ((!(["mojang", "pocketmine", "powernukkit", "nukkit", "cloudburst"]).includes(platform))) throw new Error("Invalid platform");
 | 
			
		||||
  const versions = getCacheVersions();
 | 
			
		||||
  const getLatest = (keys: IterableIterator<string>|string[]) => Array.from(keys).sort((b, a) => semver.compare(semver.valid(semver.coerce(a)), semver.valid(semver.coerce(b)))).at(0);
 | 
			
		||||
  if (!version) version = "latest";
 | 
			
		||||
  if (platform === "mojang") {
 | 
			
		||||
    if (version === "latest") version = getLatest(Object.keys(versions.mojang));
 | 
			
		||||
    const release = versions.mojang[version];
 | 
			
		||||
    if (!release) throw new Error("Not valid Release");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function runServer() {}
 | 
			
		||||
							
								
								
									
										1
									
								
								packages/core/src/platform/java/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packages/core/src/platform/java/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
export * as listVersion from "./listVersion.js";
 | 
			
		||||
							
								
								
									
										163
									
								
								packages/core/src/platform/java/listVersion.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								packages/core/src/platform/java/listVersion.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,163 @@
 | 
			
		||||
import { Github, http } from "@sirherobrine23/http";
 | 
			
		||||
import stream from "node:stream";
 | 
			
		||||
import semver from "semver";
 | 
			
		||||
import path from "path";
 | 
			
		||||
import { bdsFilesBucket } from "../../internalClouds.js";
 | 
			
		||||
 | 
			
		||||
interface baseDownload {
 | 
			
		||||
  URL: string;
 | 
			
		||||
  releaseDate: Date;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface mojangInfo extends baseDownload {
 | 
			
		||||
  release: "oficial" | "snapshot" | "beta" | "alpha";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function PromiseSplit<T extends (any[])>(arrayInput: T, fn: (value: T[number]) => any): Promise<Awaited<ReturnType<typeof fn>>[]> {
 | 
			
		||||
  const backup = ([]).concat(...arrayInput);
 | 
			
		||||
  let i = arrayInput.length, b = 1; while (i > 2) { b++; i /= 2 };
 | 
			
		||||
  const arraySplit = Math.max(2, Math.floor(b));
 | 
			
		||||
  let result: Awaited<ReturnType<typeof fn>>[] = [];
 | 
			
		||||
  await Promise.all(Array(arraySplit).fill(async function call() {
 | 
			
		||||
    return Promise.resolve().then(() => backup.length > 0 ? fn(backup.shift()) : null).then(data => {
 | 
			
		||||
      result.push(data);
 | 
			
		||||
      if (backup.length > 0) return call();
 | 
			
		||||
      return null;
 | 
			
		||||
    });
 | 
			
		||||
  }).map(a => a()));
 | 
			
		||||
  return result.flat(1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const mojangCache = new Map<string, mojangInfo>();
 | 
			
		||||
export async function listMojang() {
 | 
			
		||||
  const versions = (await http.jsonRequestBody<{ versions: { id: string, releaseTime: string, url: string, type: "snapshot" | "release" | "old_beta" | "old_alpha" }[] }>("https://launchermeta.mojang.com/mc/game/version_manifest_v2.json")).versions;
 | 
			
		||||
  await PromiseSplit(versions, async version => {
 | 
			
		||||
    if (mojangCache.has(version.id)) return;
 | 
			
		||||
    const { downloads: { server } } = await http.jsonRequestBody<{ downloads: { server?: { url: string } } }>(version.url);
 | 
			
		||||
    if (!server) return;
 | 
			
		||||
    mojangCache.set(version.id, {
 | 
			
		||||
      release: version.type === "release" ? "oficial" : version.type === "snapshot" ? "snapshot" : version.type === "old_beta" ? "beta" : "alpha",
 | 
			
		||||
      releaseDate: new Date(version.releaseTime),
 | 
			
		||||
      URL: server.url,
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface spigotDownload {
 | 
			
		||||
  getServer(): Promise<stream.Readable>;
 | 
			
		||||
  craftbukkit?(): Promise<stream.Readable>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const spigotCache = new Map<string, spigotDownload>();
 | 
			
		||||
export async function listSpigot() {
 | 
			
		||||
  const spigotFiles = await bdsFilesBucket.listFiles("SpigotBuild/");
 | 
			
		||||
  for (const file of spigotFiles.filter(file => file.name.slice(12).startsWith("1.")).sort((b, a) => {
 | 
			
		||||
    const valid = (c: typeof a) => semver.valid(semver.coerce(c.name.slice(12, -4), { loose: true }), { loose: true });
 | 
			
		||||
    return semver.compare(valid(a), valid(b));
 | 
			
		||||
  })) {
 | 
			
		||||
    const version = file.name.slice(12, -4), fixVersion = semver.valid(semver.coerce(version, { loose: true }), { loose: true });
 | 
			
		||||
    if (spigotCache.has(fixVersion)) continue;
 | 
			
		||||
    const craftbukkit = spigotFiles.find(file => file.name.slice(12).startsWith(`craftbukkit-${version}.jar`));
 | 
			
		||||
    spigotCache.set(fixVersion, {
 | 
			
		||||
      getServer: file.getFile,
 | 
			
		||||
      craftbukkit: craftbukkit ? craftbukkit.getFile : undefined,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const paperCache = new Map<string, baseDownload>();
 | 
			
		||||
export const velocityCache = new Map<string, baseDownload>();
 | 
			
		||||
export const foliaCache = new Map<string, baseDownload>();
 | 
			
		||||
export async function listPaperProject() {
 | 
			
		||||
  const paperProjects = ["paper", "velocity", "folia"] as const;
 | 
			
		||||
  await Promise.all(paperProjects.map(async projectName => {
 | 
			
		||||
    const baseURL = new URL(path.posix.join("/v2/projects", projectName), "https://api.papermc.io");
 | 
			
		||||
    const projectInfo = await http.jsonRequestBody<{ versions: string[] }>(baseURL);
 | 
			
		||||
    for (const projectVersion of projectInfo.versions) {
 | 
			
		||||
      if (projectName === "paper" && paperCache.has(projectVersion)) continue;
 | 
			
		||||
      else if (projectName === "velocity" && velocityCache.has(projectVersion)) continue;
 | 
			
		||||
      else if (projectName === "folia" && foliaCache.has(projectVersion)) continue;
 | 
			
		||||
      else {
 | 
			
		||||
        const versionBase = new URL(path.posix.join(baseURL.pathname, "versions", projectVersion), baseURL);
 | 
			
		||||
        const builds = await http.jsonRequestBody<{ builds: number[] }>(versionBase);
 | 
			
		||||
        for (const build of builds.builds) {
 | 
			
		||||
          const buildURL = new URL(path.posix.join(versionBase.pathname, "builds", String(build)), versionBase);
 | 
			
		||||
          const downloadInfo = await http.jsonRequestBody<{ time: string; downloads: { application?: { name: string } } }>(buildURL);
 | 
			
		||||
          if (downloadInfo.downloads.application) {
 | 
			
		||||
            const downloadURL = new URL(path.posix.join(buildURL.pathname, "downloads", downloadInfo.downloads.application.name), buildURL);
 | 
			
		||||
            if (projectName === "paper") paperCache.set(projectVersion, { URL: downloadURL.toString(), releaseDate: new Date(downloadInfo.time) });
 | 
			
		||||
            else if (projectName === "velocity") velocityCache.set(projectVersion, { URL: downloadURL.toString(), releaseDate: new Date(downloadInfo.time) });
 | 
			
		||||
            else if (projectName === "folia") foliaCache.set(projectVersion, { URL: downloadURL.toString(), releaseDate: new Date(downloadInfo.time) });
 | 
			
		||||
            break;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const purpurCache = new Map<string, baseDownload>();
 | 
			
		||||
export async function listPurpurProject() {
 | 
			
		||||
  const baseURL = new URL("https://api.purpurmc.org/v2/purpur");
 | 
			
		||||
  const { versions } = await http.jsonRequestBody<{ versions: string[] }>(baseURL);
 | 
			
		||||
  for (const version of versions) {
 | 
			
		||||
    if (purpurCache.has(version)) continue;
 | 
			
		||||
    const infoBase = new URL(path.posix.join(baseURL.pathname, version, "latest"), baseURL);
 | 
			
		||||
    const relInfo = await http.jsonRequestBody<{ timestamp: number }>(infoBase);
 | 
			
		||||
    purpurCache.set(version, {
 | 
			
		||||
      URL: (new URL(path.posix.join(infoBase.pathname, "download"), infoBase)).toString(),
 | 
			
		||||
      releaseDate: new Date(relInfo.timestamp)
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const glowstoneCache = new Map<string, baseDownload>();
 | 
			
		||||
export async function listGlowstoneProject() {
 | 
			
		||||
  const repo = await Github.repositoryManeger("GlowstoneMC", "Glowstone");
 | 
			
		||||
  const rels = (await repo.release.getRelease()).filter(rel => rel.assets.some(asset => asset.name.endsWith(".jar")));
 | 
			
		||||
  rels.forEach(rel => rel.assets.forEach(asset => glowstoneCache.has(rel.tag_name) ? null : glowstoneCache.set(rel.tag_name, {
 | 
			
		||||
    URL: asset.browser_download_url,
 | 
			
		||||
    releaseDate: new Date(asset.created_at)
 | 
			
		||||
  })));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const cuberiteCache = new Map<string, { URL: string[] }>([
 | 
			
		||||
  [
 | 
			
		||||
    "win32-x64",
 | 
			
		||||
    {
 | 
			
		||||
      URL: ["https://download.cuberite.org/windows-x86_64/Cuberite.zip"]
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  [
 | 
			
		||||
    "win32-ia32",
 | 
			
		||||
    {
 | 
			
		||||
      URL: ["https://download.cuberite.org/windows-i386/Cuberite.zip"]
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
]);
 | 
			
		||||
export async function listCuberite() {
 | 
			
		||||
  const projects = ["android", "linux-aarch64", "linux-armhf", "linux-i386", "linux-x86_64", "darwin-x86_64"] as const;
 | 
			
		||||
  await Promise.all(projects.map(async project => {
 | 
			
		||||
    const { builds = [] } = await http.jsonRequestBody<{ builds: { number: number, _class: string }[] }>(`https://builds.cuberite.org/job/${project}/api/json`);
 | 
			
		||||
    for (const job of builds) {
 | 
			
		||||
      const { artifacts = [], result } = await http.jsonRequestBody<{ result: string, artifacts: { relativePath: string, fileName: string }[] }>(`https://builds.cuberite.org/job/${project}/${job.number}/api/json`);
 | 
			
		||||
      if (result !== "SUCCESS") continue;
 | 
			
		||||
      let map = artifacts.filter(file => !file.fileName.endsWith(".sha1")).map(file => `https://builds.cuberite.org/job/${project}/${job.number}/artifact/${file.relativePath}`);
 | 
			
		||||
      if (project === "android") {
 | 
			
		||||
        const serverIndex = map.findIndex(file => file.toLowerCase().endsWith("server.zip"));
 | 
			
		||||
        const server = map[serverIndex];
 | 
			
		||||
        delete map[serverIndex];
 | 
			
		||||
        map = map.filter(Boolean);
 | 
			
		||||
        for (const file of map) {
 | 
			
		||||
          const fileURL = new URL(file);
 | 
			
		||||
          const plat = path.basename(fileURL.pathname).replace(path.extname(fileURL.pathname), "");
 | 
			
		||||
          if (plat.startsWith("x86_64")) cuberiteCache.set("android-x64", { URL: [server, file] });
 | 
			
		||||
          else if (plat.startsWith("x86")) cuberiteCache.set("android-ia32", { URL: [server, file] });
 | 
			
		||||
          else if (plat.startsWith("arm64")) cuberiteCache.set("android-arm64", { URL: [server, file] });
 | 
			
		||||
          else if (plat.startsWith("arm")) cuberiteCache.set("android-arm", { URL: [server, file] });
 | 
			
		||||
        }
 | 
			
		||||
      } else cuberiteCache.set(project, { URL: map });
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
  }));
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,7 @@
 | 
			
		||||
export * from "./serverManeger.js";
 | 
			
		||||
export * as serverManeger from "./serverManeger.js";
 | 
			
		||||
export * as Bedrock from "./servers/bedrock.js";
 | 
			
		||||
export * as Java from "./servers/java.js";
 | 
			
		||||
// Bedrock platform
 | 
			
		||||
export * as Bedrock from "./platform/bedrock/index.js";
 | 
			
		||||
export * as bedrock from "./platform/bedrock/index.js";
 | 
			
		||||
 | 
			
		||||
// Java platform
 | 
			
		||||
export * as Java from "./platform/java/index.js";
 | 
			
		||||
export * as java from "./platform/java/index.js";
 | 
			
		||||
@@ -1,317 +0,0 @@
 | 
			
		||||
import readline from "node:readline";
 | 
			
		||||
import { createWriteStream } from "node:fs";
 | 
			
		||||
import { extendsFS } from "@sirherobrine23/extends";
 | 
			
		||||
import { pipeline } from "node:stream/promises";
 | 
			
		||||
import { format } from "node:util";
 | 
			
		||||
import sanitizeFilename from "sanitize-filename";
 | 
			
		||||
import child_process from "node:child_process";
 | 
			
		||||
import crypto from "node:crypto";
 | 
			
		||||
import stream from "node:stream";
 | 
			
		||||
import path from "node:path";
 | 
			
		||||
import tar from "tar";
 | 
			
		||||
import fs from "node:fs/promises";
 | 
			
		||||
import os from "node:os";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// Default bds maneger core
 | 
			
		||||
const ENVROOT = process.env.BDSCOREROOT || process.env.bdscoreroot;
 | 
			
		||||
export const bdsManegerRoot = ENVROOT ? path.resolve(process.cwd(), ENVROOT) : path.join(os.homedir(), ".bdsmaneger");
 | 
			
		||||
if (!(await extendsFS.exists(bdsManegerRoot))) await fs.mkdir(bdsManegerRoot, {recursive: true});
 | 
			
		||||
export type withPromise<T> = T|Promise<T>;
 | 
			
		||||
 | 
			
		||||
export interface manegerOptions {
 | 
			
		||||
  ID?: string,
 | 
			
		||||
  newID?: boolean,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// only letters and numbers
 | 
			
		||||
const idReg = /^[a-zA-Z0-9_]+$/;
 | 
			
		||||
 | 
			
		||||
export interface serverManegerV1 {
 | 
			
		||||
  id: string,
 | 
			
		||||
  rootPath: string,
 | 
			
		||||
  serverFolder: string,
 | 
			
		||||
  backup: string,
 | 
			
		||||
  logs: string,
 | 
			
		||||
  platform: "java"|"bedrock",
 | 
			
		||||
  runCommand(options: Omit<runOptions, "cwd">): ReturnType<typeof runServer>
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Platform path maneger
 | 
			
		||||
 */
 | 
			
		||||
export async function serverManeger(platform: serverManegerV1["platform"], options: manegerOptions): Promise<serverManegerV1> {
 | 
			
		||||
  if (!((["java", "bedrock"]).includes(platform))) throw new TypeError("Invalid platform target!");
 | 
			
		||||
  if (!options) throw new TypeError("Please add serverManeger options!");
 | 
			
		||||
  const platformFolder = path.join(bdsManegerRoot, platform);
 | 
			
		||||
  if ((await fs.readdir(platformFolder).then(a => a.length).catch(() => 0)) === 0 && options.newID === undefined) options.newID = true;
 | 
			
		||||
 | 
			
		||||
  // Create or check if exists
 | 
			
		||||
  if (options.newID === true) {
 | 
			
		||||
    while(true) {
 | 
			
		||||
      options.ID = typeof crypto.randomUUID === "function" ?  crypto.randomUUID().split("-").join("_") : crypto.randomBytes(crypto.randomInt(8, 14)).toString("hex");
 | 
			
		||||
      if (!(idReg.test(options.ID))) continue;
 | 
			
		||||
      if (!((await fs.readdir(platformFolder).catch(() => [])).includes(options.ID))) break;
 | 
			
		||||
    }
 | 
			
		||||
    await fs.mkdir(path.join(platformFolder, options.ID), {recursive: true});
 | 
			
		||||
  } else {
 | 
			
		||||
    // Test invalid ID
 | 
			
		||||
    if (String(options.ID).length > 32) throw new TypeError("options.ID is invalid, is very long!");
 | 
			
		||||
    if (!(!!options.ID && idReg.test(options.ID))) throw new TypeError("options.ID is invalid");
 | 
			
		||||
    else if (!((await fs.readdir(platformFolder)).includes(options.ID))) throw new Error("ID not exists")
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Folders
 | 
			
		||||
  const rootPath = path.join(platformFolder, path.posix.resolve("/", sanitizeFilename(options.ID)));
 | 
			
		||||
  const serverFolder = path.join(rootPath, "server");
 | 
			
		||||
  const backup = path.join(rootPath, "backups");
 | 
			
		||||
  const log = path.join(rootPath, "logs");
 | 
			
		||||
 | 
			
		||||
  for await (const p of [
 | 
			
		||||
    serverFolder,
 | 
			
		||||
    backup,
 | 
			
		||||
    log,
 | 
			
		||||
  ]) if (!(await extendsFS.exists(p))) await fs.mkdir(p, {recursive: true});
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    id: options.ID,
 | 
			
		||||
    platform,
 | 
			
		||||
    rootPath,
 | 
			
		||||
    serverFolder,
 | 
			
		||||
    backup,
 | 
			
		||||
    logs: log,
 | 
			
		||||
    async runCommand(options: Omit<runOptions, "cwd">) {
 | 
			
		||||
      return runServer({...options, cwd: serverFolder});
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function listIDs(): Promise<{id: string, platform: "bedrock"|"java", delete: () => Promise<void>}[]> {
 | 
			
		||||
  const main = [];
 | 
			
		||||
  for await (const platform of ["bedrock", "java"]) {
 | 
			
		||||
    try {
 | 
			
		||||
      const platformFolder = path.join(bdsManegerRoot, platform);
 | 
			
		||||
      if (!(await extendsFS.exists(platformFolder))) continue;
 | 
			
		||||
      const IDs = await fs.readdir(platformFolder);
 | 
			
		||||
      for await (const id of IDs) main.push({
 | 
			
		||||
        id: id,
 | 
			
		||||
        platform,
 | 
			
		||||
        async delete() {
 | 
			
		||||
          return fs.rm(path.join(platformFolder, id), {recursive: true, force: true});
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    } catch {}
 | 
			
		||||
  }
 | 
			
		||||
  return main;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type portListen = {
 | 
			
		||||
  port: number,
 | 
			
		||||
  protocol: "TCP"|"UDP"|"both",
 | 
			
		||||
  listenOn?: string,
 | 
			
		||||
  listenFrom?: "server"|"plugin"
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type playerAction = {
 | 
			
		||||
  playerName: string,
 | 
			
		||||
  onDate: Date,
 | 
			
		||||
  action: string,
 | 
			
		||||
  extra?: any
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type runOptions = {
 | 
			
		||||
  cwd: string,
 | 
			
		||||
  env?: {[k: string]: string|number|boolean},
 | 
			
		||||
  command: string,
 | 
			
		||||
  args?: (string|number|boolean)[],
 | 
			
		||||
  stdio?: child_process.StdioOptions,
 | 
			
		||||
  paths: serverManegerV1,
 | 
			
		||||
  serverActions?: {
 | 
			
		||||
    stop?(this: serverRun): withPromise<void>,
 | 
			
		||||
    playerAction?(this: serverRun, lineString: string): withPromise<null|void|playerAction>,
 | 
			
		||||
    hotBackup?(this: serverRun): withPromise<stream.Readable|void>,
 | 
			
		||||
    portListen?(this: serverRun, lineString: string): withPromise<void|portListen>,
 | 
			
		||||
    onAvaible?(this: serverRun, lineString: string): withPromise<void|Date>,
 | 
			
		||||
    postStart?: ((this: serverRun) => withPromise<void>)[],
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export declare class serverRun extends child_process.ChildProcess {
 | 
			
		||||
  on(event: string, listener: (...args: any[]) => void): this;
 | 
			
		||||
  once(event: string, listener: (...args: any[]) => void): this;
 | 
			
		||||
  on(event: "error", listener: (err: Error) => void): this;
 | 
			
		||||
  once(event: "error", listener: (err: Error) => void): this;
 | 
			
		||||
  on(event: "close", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
 | 
			
		||||
  once(event: "close", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
 | 
			
		||||
  on(event: "disconnect", listener: () => void): this;
 | 
			
		||||
  once(event: "disconnect", listener: () => void): this;
 | 
			
		||||
  on(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
 | 
			
		||||
  once(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
 | 
			
		||||
  on(event: "message", listener: (message: child_process.Serializable, sendHandle: child_process.SendHandle) => void): this;
 | 
			
		||||
  once(event: "message", listener: (message: child_process.Serializable, sendHandle: child_process.SendHandle) => void): this;
 | 
			
		||||
  on(event: "spawn", listener: () => void): this;
 | 
			
		||||
  once(event: "spawn", listener: () => void): this;
 | 
			
		||||
  on(event: "warning", listener: (data: any) => void): this;
 | 
			
		||||
  once(event: "warning", listener: (data: any) => void): this;
 | 
			
		||||
 | 
			
		||||
  // BDS Assigns
 | 
			
		||||
  once(event: "line", fn: (data: string, from: "stdout"|"stderr") => void): this;
 | 
			
		||||
  on(event: "line", fn: (data: string, from: "stdout"|"stderr") => void): this;
 | 
			
		||||
  once(event: "player", fn: (playerInfo: playerAction) => void): this;
 | 
			
		||||
  on(event: "player", fn: (playerInfo: playerAction) => void): this;
 | 
			
		||||
  once(event: "portListening", fn: (portInfo: portListen) => void): this;
 | 
			
		||||
  on(event: "portListening", fn: (portInfo: portListen) => void): this;
 | 
			
		||||
  once(event: "serverAvaible", fn: (date: Date) => void): this;
 | 
			
		||||
  on(event: "serverAvaible", fn: (date: Date) => void): this;
 | 
			
		||||
  once(event: "backup", fn: (filePath: string) => void): this;
 | 
			
		||||
  on(event: "backup", fn: (filePath: string) => void): this;
 | 
			
		||||
  once(event: "hotBackup", fn: (fileStream: stream.Readable) => void): this;
 | 
			
		||||
  on(event: "hotBackup", fn: (fileStream: stream.Readable) => void): this;
 | 
			
		||||
 | 
			
		||||
  avaibleDate?: Date;
 | 
			
		||||
  runOptions: runOptions;
 | 
			
		||||
  portListening: portListen[];
 | 
			
		||||
  logPath: {stderr: string, stdout: string, merged: string};
 | 
			
		||||
  playerActions: playerAction[];
 | 
			
		||||
  stdoutInterface: readline.Interface;
 | 
			
		||||
  stderrInterface: readline.Interface;
 | 
			
		||||
 | 
			
		||||
  stopServer(): Promise<{code?: number, signal?: NodeJS.Signals}>;
 | 
			
		||||
  sendCommand(streamPipe: stream.Readable): this;
 | 
			
		||||
  sendCommand(...args: (string|number|boolean)[]): this;
 | 
			
		||||
  hotBackup(): this & Promise<Awaited<ReturnType<runOptions["serverActions"]["hotBackup"]>>>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Run servers globally and hormonally across servers
 | 
			
		||||
 */
 | 
			
		||||
export async function runServer(options: runOptions): Promise<serverRun> {
 | 
			
		||||
  if (!options.stdio) options.stdio = ["pipe", "pipe", "pipe"];
 | 
			
		||||
  const child = child_process.spawn(options.command, [...((options.args ?? []).map(String))], {
 | 
			
		||||
    // maxBuffer: Infinity,
 | 
			
		||||
    stdio: options.stdio,
 | 
			
		||||
    cwd: options.cwd,
 | 
			
		||||
    env: {
 | 
			
		||||
      ...process.env,
 | 
			
		||||
      ...Object.keys(options.env ?? {}).reduce((acc, a) => {
 | 
			
		||||
        acc[a] = String(options.env[a]);
 | 
			
		||||
        return acc;
 | 
			
		||||
      }, {})
 | 
			
		||||
    }
 | 
			
		||||
  }) as serverRun;
 | 
			
		||||
  child.runOptions = options;
 | 
			
		||||
  child.portListening = [];
 | 
			
		||||
  child.playerActions = [];
 | 
			
		||||
  for (const std of [child.stdout, child.stderr]) if (!std) {
 | 
			
		||||
    child.kill("SIGKILL");
 | 
			
		||||
    throw new TypeError("Stdout or Stderr stream disabled, killed process, cannot continue to exec server, check stdio passed to spawn!");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Log Write
 | 
			
		||||
  const currentDate = new Date();
 | 
			
		||||
  const baseLog = path.join(options.paths.logs, format("%s_%s_%s_%s-%s-%s", currentDate.getDate(), currentDate.getMonth()+1, currentDate.getFullYear(), currentDate.getHours(), currentDate.getMinutes(), currentDate.getSeconds()));
 | 
			
		||||
  await fs.mkdir(baseLog, {recursive: true});
 | 
			
		||||
  child.logPath = {stdout: path.join(baseLog, "stdout.log"), stderr: path.join(baseLog, "stderr.log"), merged: path.join(baseLog, "server.log")};
 | 
			
		||||
  const allLog = createWriteStream(child.logPath.merged);
 | 
			
		||||
  child.stdout.pipe(allLog);
 | 
			
		||||
  child.stdout.pipe(createWriteStream(child.logPath.stdout));
 | 
			
		||||
  child.stderr.pipe(allLog);
 | 
			
		||||
  child.stderr.pipe(createWriteStream(child.logPath.stderr));
 | 
			
		||||
 | 
			
		||||
  // Lines
 | 
			
		||||
  const stdout = child.stdoutInterface = readline.createInterface(child.stdout).on("line", data => child.emit("line", data, "stdout")).on("error", err => child.emit("error", err));
 | 
			
		||||
  const stderr = child.stderrInterface = readline.createInterface(child.stderr).on("line", data => child.emit("line", data, "stderr")).on("error", err => child.emit("error", err));
 | 
			
		||||
 | 
			
		||||
  if (typeof options.serverActions?.playerAction === "function") {
 | 
			
		||||
    for (const std of [stdout, stderr]) std.on("line", async data => {
 | 
			
		||||
      const playerData = await Promise.resolve(options.serverActions.playerAction.call(child, data) as ReturnType<typeof options.serverActions.playerAction>);
 | 
			
		||||
      if (!playerData) return;
 | 
			
		||||
      child.playerActions.push(playerData);
 | 
			
		||||
      child.emit("player", playerData);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (typeof options.serverActions?.portListen === "function") {
 | 
			
		||||
    for (const std of [stdout, stderr]) std.on("line", async data => {
 | 
			
		||||
      const portData = await Promise.resolve(options.serverActions.portListen.call(child, data) as ReturnType<typeof options.serverActions.portListen>);
 | 
			
		||||
      if (!portData) return;
 | 
			
		||||
      portData.listenFrom ??= "server";
 | 
			
		||||
      child.portListening.push(portData);
 | 
			
		||||
      child.emit("portListening", portData);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  child.sendCommand = function (...args) {
 | 
			
		||||
    if (!child.stdin.writable) {
 | 
			
		||||
      child.emit("error", new Error("cannot send command to server"));
 | 
			
		||||
      return child;
 | 
			
		||||
    };
 | 
			
		||||
    if (args[0] instanceof stream.Readable) {
 | 
			
		||||
      args[0].on("data", data => child.stdin.write(data)).once("close", () => child.stdin.write("\n"));
 | 
			
		||||
      return child;
 | 
			
		||||
    }
 | 
			
		||||
    child.stdin.write(args.map(String).join(" ")+"\n");
 | 
			
		||||
    return child;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  child.stopServer = async function () {
 | 
			
		||||
    child.sendCommand("");
 | 
			
		||||
    const stop = options.serverActions?.stop ?? function () {
 | 
			
		||||
      child.kill("SIGINT");
 | 
			
		||||
      const kill = setTimeout(() => {
 | 
			
		||||
        clearTimeout(kill);
 | 
			
		||||
        if (child.exitCode !== null) return;
 | 
			
		||||
        child.kill("SIGKILL");
 | 
			
		||||
      }, 2500);
 | 
			
		||||
    };
 | 
			
		||||
    Promise.resolve().then(() => stop.call(child)).catch(err => child.emit("error", err));
 | 
			
		||||
    return new Promise((done, reject) => child.once("error", reject).once("exit", (code, signal) => done({code, signal})));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  child.hotBackup = function hotBackup() {
 | 
			
		||||
    return Object.assign({}, Promise.resolve().then((async () => {
 | 
			
		||||
      if (!options.serverActions?.hotBackup) throw new Error("Hot backup disabled to current platform!");
 | 
			
		||||
      child.emit("backup", "start");
 | 
			
		||||
      return Promise.resolve(options.serverActions.hotBackup.call(child) as ReturnType<typeof options.serverActions.hotBackup>).then(data => {
 | 
			
		||||
        child.emit("backup", "success");
 | 
			
		||||
        return data;
 | 
			
		||||
      }).catch(err => {
 | 
			
		||||
        child.emit("backup", "fail");
 | 
			
		||||
        return Promise.reject(err);
 | 
			
		||||
      });
 | 
			
		||||
    })), child);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (typeof options.serverActions?.onAvaible === "function") {
 | 
			
		||||
    let run = options.serverActions.onAvaible;
 | 
			
		||||
    for (const std of [stdout, stderr]) std.on("line", async data => {
 | 
			
		||||
      if (!run) return null;
 | 
			
		||||
      const avaibleDate = await Promise.resolve(run.call(child, data) as ReturnType<typeof run>);
 | 
			
		||||
      if (!avaibleDate) return;
 | 
			
		||||
      child.avaibleDate = avaibleDate;
 | 
			
		||||
      if (options.serverActions?.postStart) for (const ss of options.serverActions?.postStart) Promise.resolve().then(() => ss.call(child)).catch(err => child.emit("error", err));
 | 
			
		||||
    });
 | 
			
		||||
  } else if (options.serverActions?.postStart?.length > 0) child.emit("warning", "no post actions run!");
 | 
			
		||||
 | 
			
		||||
  child.once("close", async () => {
 | 
			
		||||
    const cDate = new Date();
 | 
			
		||||
    const month = String(cDate.getMonth()+1 > 9 ? cDate.getMonth()+1 : "0"+(cDate.getMonth()+1).toString());
 | 
			
		||||
    const day = String(cDate.getDate() > 9 ? cDate.getDate() : "0"+((cDate.getDate()).toString()));
 | 
			
		||||
    const backupFile = path.join(options.paths.backup, String(cDate.getFullYear()), month, day, `${cDate.getHours()}_${cDate.getMinutes()}.tgz`);
 | 
			
		||||
    try {
 | 
			
		||||
      if (!(await extendsFS.exists(path.dirname(backupFile)))) await fs.mkdir(path.dirname(backupFile), {recursive: true});
 | 
			
		||||
      const ff = await fs.readdir(options.paths.serverFolder);
 | 
			
		||||
      await pipeline(tar.create({
 | 
			
		||||
        gzip: true,
 | 
			
		||||
        cwd: options.paths.serverFolder,
 | 
			
		||||
        prefix: ""
 | 
			
		||||
      }, ff), createWriteStream(backupFile));
 | 
			
		||||
      child.emit("backup", backupFile);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (await extendsFS.exists(backupFile)) await fs.unlink(backupFile);
 | 
			
		||||
      child.emit("error", err);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return child;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										142
									
								
								packages/core/src/serverRun.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								packages/core/src/serverRun.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,142 @@
 | 
			
		||||
import { extendsFS } from "@sirherobrine23/extends";
 | 
			
		||||
import child_process from "node:child_process";
 | 
			
		||||
import EventEmitter from "node:events";
 | 
			
		||||
import { createWriteStream, promises as fs } from "node:fs";
 | 
			
		||||
import { createInterface as readline } from "node:readline";
 | 
			
		||||
import path from "node:path";
 | 
			
		||||
import stream from "node:stream";
 | 
			
		||||
import tar from "tar";
 | 
			
		||||
 | 
			
		||||
export interface ManegerOptions {
 | 
			
		||||
  rootPath: string;
 | 
			
		||||
  runServer: {
 | 
			
		||||
    command: string;
 | 
			
		||||
    args?: (string | number | boolean)[];
 | 
			
		||||
    env?: Map<string, string | number | boolean> | { [K: string]: string | number | boolean },
 | 
			
		||||
    stdio?: child_process.StdioPipeNamed | child_process.StdioPipe[];
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type EventMap = Record<string, (...args: any[]) => void>;
 | 
			
		||||
type EventKey<T extends EventMap> = string & keyof T;
 | 
			
		||||
export type defaultEvents = {
 | 
			
		||||
  "onServerSpawn": (child: child_process.ChildProcess) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Extens
 | 
			
		||||
 */
 | 
			
		||||
export class serverManeger<T extends EventMap = {}> extends EventEmitter {
 | 
			
		||||
  #io: ManegerOptions;
 | 
			
		||||
  #serverStorage: string;
 | 
			
		||||
  #logStorage: string;
 | 
			
		||||
  constructor(options: ManegerOptions) {
 | 
			
		||||
    super({ captureRejections: true });
 | 
			
		||||
    this.#io = options;
 | 
			
		||||
    this.#io.rootPath = path.resolve(process.cwd(), this.#io.rootPath);
 | 
			
		||||
    this.#serverStorage = path.join(this.#io.rootPath, "storage");
 | 
			
		||||
    this.#logStorage = path.join(this.#io.rootPath, "logs");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  on<K extends EventKey<T>>(eventName: K, fn: T[K]): this;
 | 
			
		||||
  on<K extends EventKey<defaultEvents>>(eventName: K, fn: defaultEvents[K]): this;
 | 
			
		||||
  on(eventName: "line", fn: (line: string) => void): this;
 | 
			
		||||
  on(eventName: "error", fn: (err: Error) => void): this;
 | 
			
		||||
  on(eventName: string, fn: (...args: any) => void): this {
 | 
			
		||||
    super.on(eventName, fn);
 | 
			
		||||
    return this;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  once<K extends EventKey<T>>(eventName: K, fn: T[K]): this;
 | 
			
		||||
  once<K extends EventKey<defaultEvents>>(eventName: K, fn: defaultEvents[K]): this;
 | 
			
		||||
  once(eventName: "line", fn: (line: string) => void): this;
 | 
			
		||||
  once(eventName: "error", fn: (err: Error) => void): this;
 | 
			
		||||
  once(eventName: string, fn: (...args: any) => void): this {
 | 
			
		||||
    super.once(eventName, fn);
 | 
			
		||||
    return this;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  emit<K extends EventKey<T>>(eventName: K, ...args: Parameters<T[K]>): boolean;
 | 
			
		||||
  emit<K extends EventKey<defaultEvents>>(eventName: K, ...args: Parameters<defaultEvents[K]>): boolean;
 | 
			
		||||
  emit(name: "line", line: string): boolean;
 | 
			
		||||
  emit(name: "error", err: Error): boolean;
 | 
			
		||||
  emit(eventName: string, ...args: any): boolean {
 | 
			
		||||
    return super.emit(eventName, args);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** Get root from paths */
 | 
			
		||||
  getRoot(): string { return this.#io.rootPath; };
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Create tar.gz from server Storage, if server running create "snapshot" from server running state.
 | 
			
		||||
   *
 | 
			
		||||
   * @returns - Gzip tar
 | 
			
		||||
   */
 | 
			
		||||
  hotBackup(): stream.Readable {
 | 
			
		||||
    return tar.create({
 | 
			
		||||
      gzip: true,
 | 
			
		||||
      cwd: this.#serverStorage
 | 
			
		||||
    }, []);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #severProcess: child_process.ChildProcess;
 | 
			
		||||
  getStdout() {
 | 
			
		||||
    return this.#severProcess.stdout;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getStderr() {
 | 
			
		||||
    return this.#severProcess.stderr;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getStdin() {
 | 
			
		||||
    return this.#severProcess.stdin;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Start server
 | 
			
		||||
   *
 | 
			
		||||
   * @returns get from server actions
 | 
			
		||||
   */
 | 
			
		||||
  async startServer() {
 | 
			
		||||
    if (this.#severProcess) return;
 | 
			
		||||
    let processEnv: { [k: string]: string } = {};
 | 
			
		||||
    if (this.#io.runServer.env) {
 | 
			
		||||
      const { env } = this.#io.runServer;
 | 
			
		||||
      if (env instanceof Map) {
 | 
			
		||||
        processEnv = Array.from(env.keys()).reduce<typeof processEnv>((acc, keyName) => {
 | 
			
		||||
          if (env.get(keyName) !== undefined) acc[keyName] = String(env.get(keyName));
 | 
			
		||||
          return acc;
 | 
			
		||||
        }, {});
 | 
			
		||||
      } else {
 | 
			
		||||
        processEnv = Object.keys(env).reduce<typeof processEnv>((acc, keyName) => {
 | 
			
		||||
          if (env[keyName] !== undefined) acc[keyName] = String(env[keyName]);
 | 
			
		||||
          return acc;
 | 
			
		||||
        }, {});
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const runDate = new Date();
 | 
			
		||||
    this.#severProcess = child_process.spawn(this.#io.runServer.command, (this.#io.runServer.args || []).map(String), {
 | 
			
		||||
      env: { ...process.env, ...processEnv },
 | 
			
		||||
      cwd: this.#serverStorage,
 | 
			
		||||
      stdio: this.#io.runServer.stdio,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const logPath = path.join(this.#logStorage, String(runDate.getFullYear()), String(runDate.getMonth() + 1), String(runDate.getDate()));
 | 
			
		||||
    const logpathRoot = createWriteStream(path.join(this.#logStorage, "all.log"));
 | 
			
		||||
    if (!(await extendsFS.exists(logPath))) await fs.mkdir(logPath, {recursive: true});
 | 
			
		||||
    if (this.#severProcess.stdout) {
 | 
			
		||||
      this.#severProcess.stdout.pipe(createWriteStream(path.join(logPath, "stdout.log")));
 | 
			
		||||
      this.#severProcess.stdout.pipe(logpathRoot);
 | 
			
		||||
      readline(this.#severProcess.stdout).on("line", line => this.emit("line", line));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.#severProcess.stderr) {
 | 
			
		||||
      this.#severProcess.stderr.pipe(createWriteStream(path.join(logPath, "stderr.log")));
 | 
			
		||||
      this.#severProcess.stderr.pipe(logpathRoot);
 | 
			
		||||
      readline(this.#severProcess.stderr).on("line", line => this.emit("line", line));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.emit("onServerSpawn", this.#severProcess);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
@@ -1,388 +0,0 @@
 | 
			
		||||
import fsOld, { promises as fs } from "node:fs";
 | 
			
		||||
import coreHttp, { Github } from "@sirherobrine23/http";
 | 
			
		||||
import { runOptions, serverManegerV1 } from "../serverManeger.js";
 | 
			
		||||
import { oracleStorage } from "../internal.js";
 | 
			
		||||
import { pipeline } from "node:stream/promises";
 | 
			
		||||
import { Readable } from "node:stream";
 | 
			
		||||
import extendsFS, { promiseChildProcess } from "@sirherobrine23/extends";
 | 
			
		||||
import semver from "semver";
 | 
			
		||||
import unzip from "unzipper";
 | 
			
		||||
import utils from "node:util";
 | 
			
		||||
import path from "node:path";
 | 
			
		||||
import tar from "tar";
 | 
			
		||||
 | 
			
		||||
export interface bedrockOptions {
 | 
			
		||||
  /**
 | 
			
		||||
   * Alternative server instead of official Mojang server
 | 
			
		||||
   */
 | 
			
		||||
  altServer?: "mojang"|"pocketmine"|"powernukkit"|"nukkit"|"cloudbust",
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const pocketmineGithub = await Github.repositoryManeger("pmmp", "PocketMine-MP");
 | 
			
		||||
export type bedrockList = {
 | 
			
		||||
  date: Date,
 | 
			
		||||
  version: string,
 | 
			
		||||
  release: "preview"|"stable",
 | 
			
		||||
  downloads: {
 | 
			
		||||
    php?: {
 | 
			
		||||
      installPHP(serverPath: serverManegerV1): Promise<void>,
 | 
			
		||||
    },
 | 
			
		||||
    server: {
 | 
			
		||||
      getServer(): Promise<Readable>,
 | 
			
		||||
      url?: string,
 | 
			
		||||
      urls?: {
 | 
			
		||||
        [platform in NodeJS.Platform]?: {
 | 
			
		||||
          [arch in NodeJS.Architecture]?: string
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      [K: string]: any
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * List Minecrft bedrock server versions
 | 
			
		||||
 *
 | 
			
		||||
 * @param altServer - Alternative server of official Mojang
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
export async function listVersions(altServer?: bedrockOptions["altServer"]): Promise<bedrockList[]> {
 | 
			
		||||
  if (!altServer) altServer = "mojang";
 | 
			
		||||
  if (altServer) if (!(["mojang", "cloudbust", "cloudbust", "nukkit", "pocketmine", "powernukkit"]).includes(altServer)) throw new TypeError("Invalid alt server");
 | 
			
		||||
  if (altServer === "pocketmine") {
 | 
			
		||||
    return (await pocketmineGithub.release.getRelease()).filter(rel => (rel.assets.find(assert => assert.name.endsWith(".phar")) ?? {}).browser_download_url).map(rel => ({
 | 
			
		||||
      date: new Date(rel.created_at),
 | 
			
		||||
      version: rel.tag_name,
 | 
			
		||||
      release: rel.prerelease ? "preview" : "stable",
 | 
			
		||||
      downloads: {
 | 
			
		||||
        php: {
 | 
			
		||||
          async installPHP(serverPath: serverManegerV1) {
 | 
			
		||||
            const phpFile = (await oracleStorage.listFiles("php_bin")).find(file => file.name.includes(process.platform) && file.name.includes(process.arch));
 | 
			
		||||
            if (!phpFile) throw new Error(`Unable to find php files for ${process.platform} with architecture ${process.arch}`);
 | 
			
		||||
            if (phpFile.name.endsWith(".tar.gz")||phpFile.name.endsWith(".tgz")||phpFile.name.endsWith(".tar")) await pipeline(oracleStorage.getFile(phpFile.name), tar.extract({cwd: serverPath.serverFolder}));
 | 
			
		||||
            else if (phpFile.name.endsWith(".zip")) await pipeline(oracleStorage.getFile(phpFile.name), unzip.Extract({path: serverPath.serverFolder}));
 | 
			
		||||
            else throw new Error("Found file is not supported!");
 | 
			
		||||
            return null
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        server: {
 | 
			
		||||
          url: (rel.assets.find(assert => assert.name.endsWith(".phar")) ?? {}).browser_download_url,
 | 
			
		||||
          async getServer() {
 | 
			
		||||
            const pharFile = rel.assets.find(assert => assert.name.endsWith(".phar"));
 | 
			
		||||
            if (!pharFile) throw new Error("Version not includes server file!");
 | 
			
		||||
            return coreHttp.streamRequest(pharFile.browser_download_url);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }));
 | 
			
		||||
  } else if (altServer === "powernukkit") {
 | 
			
		||||
    const releases_version = (await coreHttp.jsonRequest<{[k: string]: {version: string, releaseTime: number, minecraftVersion: string, artefacts: string[], commitId:  string, snapshotBuild?: number}[]}>("https://raw.githubusercontent.com/PowerNukkit/powernukkit-version-aggregator/master/powernukkit-versions.json")).body;
 | 
			
		||||
    return Object.keys(releases_version).reduce((acc, key) => acc.concat(releases_version[key]), [] as (typeof releases_version)[string]).map(data => {
 | 
			
		||||
      const dt = new Date(data.releaseTime);
 | 
			
		||||
      const getArtefactExtension = (artefactId: string) => (artefactId.includes("REDUCED_JAR")) ? ".jar" : (artefactId.includes("REDUCED_SOURCES_JAR")) ? "-sources.jar" : (artefactId.includes("SHADED_JAR")) ? "-shaded.jar" : (artefactId.includes("SHADED_SOURCES_JAR")) ? "-shaded-sources.jar" : (artefactId.includes("JAVADOC_JAR")) ? "-javadoc.jar" : ".unknown";
 | 
			
		||||
      function buildArtefactUrl(data: any, artefactId?: string) {
 | 
			
		||||
        const buildReleaseArtefactUrl = (data: any, artefactId?: string) => !data.artefacts.includes(artefactId) ? null : utils.format("https://search.maven.org/remotecontent?filepath=org/powernukkit/powernukkit/%s/powernukkit-%s%s", data.version, data.version, getArtefactExtension(artefactId));
 | 
			
		||||
        const buildSnapshotArtefactUrl = (data: any, artefactId?: string) => !data.artefacts.includes(artefactId) ? null : utils.format("https://oss.sonatype.org/content/repositories/snapshots/org/powernukkit/powernukkit/%s-SNAPSHOT/powernukkit-%s-%s%s", data.version.substring(0, data.version.indexOf("-SNAPSHOT")), data.version.substring(0, data.version.indexOf("-SNAPSHOT")), dt.getUTCFullYear().toString().padStart(4, "0") + (dt.getUTCMonth() + 1).toString().padStart(2, "0") + dt.getUTCDate().toString().padStart(2, "0") + "." + dt.getUTCHours().toString().padStart(2, "0") + dt.getUTCMinutes().toString().padStart(2, "0") + dt.getUTCSeconds().toString().padStart(2, "0") + "-" + data.snapshotBuild, getArtefactExtension(artefactId));
 | 
			
		||||
        if (artefactId == "GIT_SOURCE") {
 | 
			
		||||
          if (data.commitId) return utils.format("https://github.com/PowerNukkit/PowerNukkit/tree/%s", data.commitId);
 | 
			
		||||
          else if (data.snapshotBuild && data.artefacts.includes("SHADED_SOURCES_JAR")) return buildSnapshotArtefactUrl(data, "SHADED_SOURCES_JAR");
 | 
			
		||||
          else if (data.snapshotBuild && data.artefacts.includes("REDUCED_SOURCES_JAR")) return buildSnapshotArtefactUrl(data, "REDUCED_SOURCES_JAR");
 | 
			
		||||
          else if (data.artefacts.includes("SHADED_SOURCES_JAR")) return buildReleaseArtefactUrl(data, "SHADED_SOURCES_JAR");
 | 
			
		||||
          else if (data.artefacts.includes("REDUCED_SOURCES_JAR")) return buildReleaseArtefactUrl(data, "REDUCED_SOURCES_JAR");
 | 
			
		||||
        } else if (data.snapshotBuild) return buildSnapshotArtefactUrl(data, artefactId);
 | 
			
		||||
        else return buildReleaseArtefactUrl(data, artefactId);
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
      const artefacts = data.artefacts.reduce((acc, artefactId) => {acc[artefactId] = buildArtefactUrl(data, artefactId); return acc;}, {} as {[key: string]: string});
 | 
			
		||||
      return {
 | 
			
		||||
        date: dt,
 | 
			
		||||
        version: data.version,
 | 
			
		||||
        release: data.snapshotBuild ? "stable" : "preview",
 | 
			
		||||
        downloads: {
 | 
			
		||||
          server: {
 | 
			
		||||
            mcpeVersion: data.minecraftVersion,
 | 
			
		||||
            url: artefacts.SHADED_JAR || artefacts.REDUCED_JAR,
 | 
			
		||||
            async getServer() {
 | 
			
		||||
              if (!(artefacts.SHADED_JAR || artefacts.REDUCED_JAR)) throw new Error("Cannot get server file to the version!");
 | 
			
		||||
              return coreHttp.streamRequest(artefacts.SHADED_JAR || artefacts.REDUCED_JAR)
 | 
			
		||||
            },
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
  } else if (altServer === "cloudbust"||altServer === "nukkit") {
 | 
			
		||||
    const { body: { jobs } } = await coreHttp.jsonRequest<{jobs: {name: string, _class: string}[]}>(`https://ci.opencollab.dev/job/NukkitX/job/${altServer === "nukkit" ? "Nukkit" : "Server"}/api/json`);
 | 
			
		||||
    const buildFiles = await Promise.all(jobs.filter(b => b._class === "org.jenkinsci.plugins.workflow.job.WorkflowJob").map(b => b.name).map(async branch => {
 | 
			
		||||
      const { body: { builds } } = await coreHttp.jsonRequest<{builds: {_class: string, number: number, url: string}[]}>(`https://ci.opencollab.dev/job/NukkitX/job/${altServer === "nukkit" ? "Nukkit" : "Server"}/job/${branch}/api/json`);
 | 
			
		||||
      return Promise.all(builds.map(async build => {
 | 
			
		||||
        const { body: { artifacts, result, timestamp } } = await coreHttp.jsonRequest<{result: "SUCCESS", timestamp: number, artifacts: {displayPath: string, fileName: string, relativePath: string}[]}>(`https://ci.opencollab.dev/job/NukkitX/job/${altServer === "nukkit" ? "Nukkit" : "Server"}/job/${branch}/${build.number}/api/json`);
 | 
			
		||||
        if (result !== "SUCCESS") return [];
 | 
			
		||||
        return artifacts.filter(f => f.relativePath.endsWith(".jar")).map(target => ({
 | 
			
		||||
          buildNumber: build.number,
 | 
			
		||||
          branch,
 | 
			
		||||
          releaseDate: new Date(timestamp),
 | 
			
		||||
          url: `https://ci.opencollab.dev/job/NukkitX/job/${altServer === "nukkit" ? "Nukkit" : "Server"}/job/${branch}/${build.number}/artifact/${target.relativePath}`,
 | 
			
		||||
        }));
 | 
			
		||||
      }));
 | 
			
		||||
    })).then(r => r.flat(2));
 | 
			
		||||
    return buildFiles.sort((b, a) => a.releaseDate.getTime() - b.releaseDate.getTime()).map(rel => ({
 | 
			
		||||
      date: rel.releaseDate,
 | 
			
		||||
      release: "preview",
 | 
			
		||||
      version: `${rel.branch}_${rel.buildNumber}`,
 | 
			
		||||
      downloads: {
 | 
			
		||||
        server: {
 | 
			
		||||
          url: rel.url,
 | 
			
		||||
          async getServer() {
 | 
			
		||||
            return coreHttp.streamRequest(rel.url);
 | 
			
		||||
          },
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }));
 | 
			
		||||
  } else if (altServer === "mojang") {
 | 
			
		||||
 | 
			
		||||
    return (await coreHttp.jsonRequest<{version: string, date: Date, release?: "stable"|"preview", url: {[platform in NodeJS.Platform]?: {[arch in NodeJS.Architecture]?: string}}}[]>("https://sirherobrine23.github.io/BedrockFetch/all.json")).body.sort((b, a) => semver.compare(semver.valid(semver.coerce(a.version)), semver.valid(semver.coerce(b.version)))).map(rel => ({
 | 
			
		||||
      version: rel.version,
 | 
			
		||||
      date: new Date(rel.date),
 | 
			
		||||
      release: rel.release === "preview" ? "preview" : "stable",
 | 
			
		||||
      downloads: {
 | 
			
		||||
        server: {
 | 
			
		||||
          url: rel.url[process.platform]?.[process.arch],
 | 
			
		||||
          async getServer() {
 | 
			
		||||
            const platformURL = (rel.url[process.platform] ?? rel.url["linux"]);
 | 
			
		||||
            if (!platformURL) throw new Error("Cannot get platform URL");
 | 
			
		||||
            const arch = platformURL[process.arch] ?? platformURL["x64"];
 | 
			
		||||
            if (!arch) throw new Error("Cannot get bedrock server to current arch");
 | 
			
		||||
            return coreHttp.streamRequest(arch);
 | 
			
		||||
          },
 | 
			
		||||
          urls: rel.url
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }));
 | 
			
		||||
  } else throw new Error("Invalid platform");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function installServer(serverPath: serverManegerV1, options: bedrockOptions & {version?: string, allowBeta?: boolean}) {
 | 
			
		||||
  const versions = await listVersions(options?.altServer);
 | 
			
		||||
  if (!options.altServer) options.altServer = "mojang";
 | 
			
		||||
  if (options.altServer === "pocketmine") {
 | 
			
		||||
    const rel = options.version === "latest" ? versions.at(0) : versions.find(rel => rel.version === options.version);
 | 
			
		||||
    if (!rel) throw new Error("Version not exsists");
 | 
			
		||||
    await rel.downloads.php.installPHP(serverPath);
 | 
			
		||||
    await pipeline(await rel.downloads.server.getServer(), fsOld.createWriteStream(path.join(serverPath.serverFolder, "server.phar")));
 | 
			
		||||
    return {
 | 
			
		||||
      ...rel,
 | 
			
		||||
      id: serverPath.id,
 | 
			
		||||
    };
 | 
			
		||||
  } else if (options.altServer === "cloudbust" || options.altServer === "powernukkit" || options.altServer === "nukkit") {
 | 
			
		||||
    if ((["cloudbust", "nukkit"]).includes(options.altServer)) options.version = "latest";
 | 
			
		||||
    const rel = options.version === "latest" ? versions.at(0) : versions.find(rel => rel.version === options.version);
 | 
			
		||||
    if (!rel) throw new Error("Version not exists");
 | 
			
		||||
    await pipeline(await rel.downloads.server.getServer(), fsOld.createWriteStream(path.join(serverPath.serverFolder, "server.jar")));
 | 
			
		||||
    return {
 | 
			
		||||
      ...rel,
 | 
			
		||||
      id: serverPath.id,
 | 
			
		||||
    };
 | 
			
		||||
  } else if (options.altServer === "mojang") {
 | 
			
		||||
    const bedrockVersion = versions.find(rel => {
 | 
			
		||||
      if (rel.release === "preview") if (options.allowBeta !== true) return false;
 | 
			
		||||
      const version = (options.version ?? "latest").trim();
 | 
			
		||||
      if (version.toLowerCase() === "latest") return true;
 | 
			
		||||
      return rel.version === version;
 | 
			
		||||
    });
 | 
			
		||||
    if (!bedrockVersion) throw new Error("Não existe essa versão");
 | 
			
		||||
    let downloadUrl = bedrockVersion.downloads.server.url;
 | 
			
		||||
    if ((["android", "linux"] as NodeJS.Process["platform"][]).includes(process.platform) && process.arch !== "x64") {
 | 
			
		||||
      if (!downloadUrl) {
 | 
			
		||||
        for (const emu of ["qemu-x86_64-static", "qemu-x86_64", "box64"]) {
 | 
			
		||||
          if (downloadUrl) break;
 | 
			
		||||
          if (await promiseChildProcess.commandExists(emu)) downloadUrl = bedrockVersion.downloads.server.urls.linux?.x64;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (!downloadUrl) throw new Error(`Não existe o URL de download para ${process.platform} na arquitetura ${process.arch}`);
 | 
			
		||||
 | 
			
		||||
    const filesBackup = ["server.properties", "valid_known_packs.json", "permissions.json", "allowlist.json", "whitelist.json"];
 | 
			
		||||
    const datS = (await Promise.all(filesBackup.map(async f => !await extendsFS.exists(path.join(serverPath.serverFolder, f)) ? null : ({path: f, data: await fs.readFile(path.join(serverPath.serverFolder, f))})))).filter(a => !!a);
 | 
			
		||||
    await pipeline(await coreHttp.streamRequest(downloadUrl), unzip.Extract({path: serverPath.serverFolder}));
 | 
			
		||||
    await Promise.all(datS.map(async f => fs.writeFile(f.path, f.data)));
 | 
			
		||||
    return {
 | 
			
		||||
      ...bedrockVersion,
 | 
			
		||||
      id: serverPath.id,
 | 
			
		||||
    };
 | 
			
		||||
  } else throw new Error("Invalid platform");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function startServer(maneger: serverManegerV1, options: bedrockOptions) {
 | 
			
		||||
  if (!options.altServer) options.altServer = "mojang";
 | 
			
		||||
  if (options.altServer === "powernukkit"||options.altServer === "cloudbust") {
 | 
			
		||||
    return maneger.runCommand({
 | 
			
		||||
      command: "java",
 | 
			
		||||
      args: [
 | 
			
		||||
        "-XX:+UseG1GC",
 | 
			
		||||
        "-XX:+ParallelRefProcEnabled",
 | 
			
		||||
        "-XX:MaxGCPauseMillis=200",
 | 
			
		||||
        "-XX:+UnlockExperimentalVMOptions",
 | 
			
		||||
        "-XX:+DisableExplicitGC",
 | 
			
		||||
        "-XX:+AlwaysPreTouch",
 | 
			
		||||
        "-XX:G1NewSizePercent=30",
 | 
			
		||||
        "-XX:G1MaxNewSizePercent=40",
 | 
			
		||||
        "-XX:G1HeapRegionSize=8M",
 | 
			
		||||
        "-XX:G1ReservePercent=20",
 | 
			
		||||
        "-XX:G1HeapWastePercent=5",
 | 
			
		||||
        "-XX:G1MixedGCCountTarget=4",
 | 
			
		||||
        "-XX:InitiatingHeapOccupancyPercent=15",
 | 
			
		||||
        "-XX:G1MixedGCLiveThresholdPercent=90",
 | 
			
		||||
        "-XX:G1RSetUpdatingPauseTimePercent=5",
 | 
			
		||||
        "-XX:SurvivorRatio=32",
 | 
			
		||||
        "-XX:+PerfDisableSharedMem",
 | 
			
		||||
        "-XX:MaxTenuringThreshold=1",
 | 
			
		||||
        "-Dusing.aikars.flags=https://mcflags.emc.gs",
 | 
			
		||||
        "-Daikars.new.flags=true",
 | 
			
		||||
        "-jar", "server.jar",
 | 
			
		||||
      ],
 | 
			
		||||
      paths: maneger,
 | 
			
		||||
      serverActions: {
 | 
			
		||||
        stop() {
 | 
			
		||||
          this.sendCommand("stop");
 | 
			
		||||
        },
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  } else if (options.altServer === "pocketmine") {
 | 
			
		||||
    return maneger.runCommand({
 | 
			
		||||
      command: (await extendsFS.readdir(maneger.serverFolder)).find(file => file.endsWith("php")||file.endsWith("php.exe")),
 | 
			
		||||
      args: [
 | 
			
		||||
        "server.phar",
 | 
			
		||||
        "--no-wizard"
 | 
			
		||||
      ],
 | 
			
		||||
      paths: maneger,
 | 
			
		||||
      serverActions: {
 | 
			
		||||
        stop() {
 | 
			
		||||
          this.sendCommand("stop")
 | 
			
		||||
        },
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  if (process.platform === "darwin") throw new Error("Run in docker or podman!");
 | 
			
		||||
  const run: Omit<runOptions, "cwd"> = {
 | 
			
		||||
    command: path.join(maneger.serverFolder, "bedrock_server"+(process.platform === "win32" ? ".exe" : "")),
 | 
			
		||||
    paths: maneger,
 | 
			
		||||
    serverActions: {
 | 
			
		||||
      stop() {
 | 
			
		||||
        this.sendCommand("stop");
 | 
			
		||||
      },
 | 
			
		||||
      portListen(lineString) {
 | 
			
		||||
        // [INFO] IPv4 supported, port: 19132
 | 
			
		||||
        // [2023-03-08 13:01:57 INFO] Listening on IPv4 port: 19132
 | 
			
		||||
        const ipProtocol = lineString.slice(lineString.indexOf("IPv"), lineString.indexOf("IPv")+4);
 | 
			
		||||
        if (ipProtocol) {
 | 
			
		||||
          let port = lineString.slice(lineString.lastIndexOf("port:")+5).trim();
 | 
			
		||||
          if (port.indexOf(":") !== -1) port = port.slice(0, port.lastIndexOf(":"));
 | 
			
		||||
          return {
 | 
			
		||||
            protocol: "UDP",
 | 
			
		||||
            listenOn: ipProtocol.toLowerCase() === "ipv4" ? "0.0.0.0" : "[::]",
 | 
			
		||||
            port: Number(port),
 | 
			
		||||
          };
 | 
			
		||||
        }
 | 
			
		||||
        return null;
 | 
			
		||||
      },
 | 
			
		||||
      playerAction(lineString) {
 | 
			
		||||
        lineString = lineString.replace(/^(.*)?\[.*\]/, "").trim();
 | 
			
		||||
        if (lineString.startsWith("Player")) {
 | 
			
		||||
          lineString = lineString.replace("Player", "").trim();
 | 
			
		||||
 | 
			
		||||
          // Spawned, disconnected, connected
 | 
			
		||||
          let action: string;
 | 
			
		||||
          if (lineString.startsWith("Spawned")) action = "Spawned";
 | 
			
		||||
          else if (lineString.startsWith("disconnected")) action = "disconnected";
 | 
			
		||||
          else if (lineString.startsWith("connected")) action = "connected";
 | 
			
		||||
          if (!action) return null;
 | 
			
		||||
          lineString = lineString.replace(action, "").trim();
 | 
			
		||||
          if (lineString.startsWith(":")) lineString = lineString.slice(1).trim();
 | 
			
		||||
 | 
			
		||||
          let playerName = lineString.substring(0, lineString.indexOf("xuid:")-1).trim();
 | 
			
		||||
          if (!playerName) return null;
 | 
			
		||||
          if (playerName.endsWith(",")) playerName = playerName.slice(0, playerName.length - 1);
 | 
			
		||||
 | 
			
		||||
          let xuid: string;
 | 
			
		||||
          if (lineString.indexOf("xuid:") !== -1) {
 | 
			
		||||
            xuid = lineString.slice(lineString.indexOf("xuid:")+5).trim();
 | 
			
		||||
            if (!xuid) xuid = null;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return {
 | 
			
		||||
            onDate: new Date(),
 | 
			
		||||
            action,
 | 
			
		||||
            playerName,
 | 
			
		||||
            extra: {
 | 
			
		||||
              xuid
 | 
			
		||||
            }
 | 
			
		||||
          };
 | 
			
		||||
        };
 | 
			
		||||
        return null;
 | 
			
		||||
      },
 | 
			
		||||
      onAvaible(data) {
 | 
			
		||||
        // [2023-03-06 21:37:27:699 INFO] Server started.
 | 
			
		||||
        data = data.replace(/^.*?\[.*\]/, "").trim();
 | 
			
		||||
        if (data.includes("started") && data.includes("Server")) return new Date();
 | 
			
		||||
        return null
 | 
			
		||||
      },
 | 
			
		||||
      async hotBackup() {
 | 
			
		||||
        const ff = (await fs.readdir(this.runOptions.paths.serverFolder)).filter(ff => {
 | 
			
		||||
          let ok = ff.endsWith(".json");
 | 
			
		||||
          if (!ok) ok = ff === "server.properties";
 | 
			
		||||
          if (!ok) ok = ff === "worlds";
 | 
			
		||||
          return ok;
 | 
			
		||||
        });
 | 
			
		||||
        return tar.create({
 | 
			
		||||
          gzip: true,
 | 
			
		||||
          cwd: this.runOptions.paths.serverFolder,
 | 
			
		||||
          prefix: ""
 | 
			
		||||
        }, ff);
 | 
			
		||||
      },
 | 
			
		||||
      postStart: [
 | 
			
		||||
        async function() {
 | 
			
		||||
          let breaked = false;
 | 
			
		||||
          this.once("close", () => breaked = true);
 | 
			
		||||
          let w: any;
 | 
			
		||||
          this.once("close", () => w.close());
 | 
			
		||||
          while(true) {
 | 
			
		||||
            if (breaked) break;
 | 
			
		||||
            await new Promise(done => {
 | 
			
		||||
              this.once("close", done);
 | 
			
		||||
              w = fsOld.watch(this.runOptions.paths.serverFolder, {recursive: true}, () => {w.close(); done(null);}).on("error", () => {});
 | 
			
		||||
            });
 | 
			
		||||
            await new Promise(done => setTimeout(done, 1000));
 | 
			
		||||
            const cDate = new Date();
 | 
			
		||||
            const month = String(cDate.getMonth()+1 > 9 ? cDate.getMonth()+1 : "0"+(cDate.getMonth()+1).toString());
 | 
			
		||||
            const day = String(cDate.getDate() > 9 ? cDate.getDate() : "0"+((cDate.getDate()).toString()));
 | 
			
		||||
            const backupFile = path.join(this.runOptions.paths.backup, "hotBackup", String(cDate.getFullYear()), month, day, `${cDate.getHours()}_${cDate.getMinutes()}.tgz`);
 | 
			
		||||
            if (!(await extendsFS.exists(path.dirname(backupFile)))) await fs.mkdir(path.dirname(backupFile), {recursive: true});
 | 
			
		||||
            const ff = (await fs.readdir(this.runOptions.paths.serverFolder)).filter(ff => {
 | 
			
		||||
              let ok = ff.endsWith(".json");
 | 
			
		||||
              if (!ok) ok = ff === "server.properties";
 | 
			
		||||
              if (!ok) ok = ff === "worlds";
 | 
			
		||||
              return ok;
 | 
			
		||||
            });
 | 
			
		||||
            const hotTar = tar.create({
 | 
			
		||||
              gzip: true,
 | 
			
		||||
              cwd: this.runOptions.paths.serverFolder,
 | 
			
		||||
              prefix: ""
 | 
			
		||||
            }, ff);
 | 
			
		||||
            this.emit("hotBackup", hotTar);
 | 
			
		||||
            await pipeline(hotTar, fsOld.createWriteStream(backupFile));
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  if ((["android", "linux"] as NodeJS.Process["platform"][]).includes(process.platform) && process.arch !== "x64") {
 | 
			
		||||
    for (const emu of ["qemu-x86_64-static", "qemu-x86_64", "box64"]) {
 | 
			
		||||
      if (await promiseChildProcess.commandExists(emu)) {
 | 
			
		||||
        run.args = [run.command, ...run.args];
 | 
			
		||||
        run.command = emu;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return maneger.runCommand(run);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,237 +0,0 @@
 | 
			
		||||
import { serverManegerV1 } from "../serverManeger.js";
 | 
			
		||||
import { oracleStorage } from "../internal.js";
 | 
			
		||||
import { extendsFS } from "@sirherobrine23/extends";
 | 
			
		||||
import { pipeline } from "node:stream/promises";
 | 
			
		||||
import coreHttp, { Github } from "@sirherobrine23/http";
 | 
			
		||||
import semver from "semver";
 | 
			
		||||
import stream from "node:stream";
 | 
			
		||||
import utils from "node:util";
 | 
			
		||||
import path from "node:path";
 | 
			
		||||
import fs from "node:fs";
 | 
			
		||||
 | 
			
		||||
export interface javaOptions {
 | 
			
		||||
  /**
 | 
			
		||||
   * Alternative server instead of official Mojang server
 | 
			
		||||
   */
 | 
			
		||||
  altServer?: "mojang"|"spigot"|"paper"|"purpur"|"glowstone"|"folia"|"cuberite"
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type javaList = {
 | 
			
		||||
  version: string,
 | 
			
		||||
  release: "stable"|"snapshot",
 | 
			
		||||
  date?: Date,
 | 
			
		||||
  platform?: NodeJS.Platform;
 | 
			
		||||
  architecture?: NodeJS.Architecture;
 | 
			
		||||
  getFile: {
 | 
			
		||||
    fileName: string;
 | 
			
		||||
    stream(): Promise<stream.Readable>;
 | 
			
		||||
    fileURL?: string;
 | 
			
		||||
  }[],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export async function listVersions(altServer?: javaOptions["altServer"]): Promise<javaList[]> {
 | 
			
		||||
  if (!altServer) altServer = "mojang";
 | 
			
		||||
  if (altServer) if(!(["mojang", "paper", "folia", "purpur", "spigot", "glowstone", "cuberite"]).includes(altServer)) throw new TypeError("Invalid alt server!");
 | 
			
		||||
  if (altServer === "purpur") {
 | 
			
		||||
    return (await Promise.all((await coreHttp.jsonRequest<{versions: string[]}>("https://api.purpurmc.org/v2/purpur")).body.versions.map(async (version): Promise<javaList> => ({
 | 
			
		||||
      version,
 | 
			
		||||
      release: "stable",
 | 
			
		||||
      date: new Date((await coreHttp.jsonRequest<{timestamp: number}>(utils.format("https://api.purpurmc.org/v2/purpur/%s/latest", version))).body.timestamp),
 | 
			
		||||
      getFile: [
 | 
			
		||||
        {
 | 
			
		||||
          fileName: "server.jar",
 | 
			
		||||
          fileURL: utils.format("https://api.purpurmc.org/v2/purpur/%s/latest/download", version),
 | 
			
		||||
          async stream() {return coreHttp.streamRequest(utils.format("https://api.purpurmc.org/v2/purpur/%s/latest/download", version));},
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
    })))).sort((b, a) => semver.compare(semver.valid(semver.coerce(a.version)), semver.valid(semver.coerce(b.version))));
 | 
			
		||||
  } else if (altServer === "paper" || altServer === "folia") {
 | 
			
		||||
    const uriPATH = ["/v2/projects", altServer];
 | 
			
		||||
    return (await Promise.all((await coreHttp.jsonRequest<{versions: string[]}>(new URL(uriPATH.join("/"), "https://api.papermc.io"))).body.versions.map(async (version): Promise<javaList> => {
 | 
			
		||||
      const build = (await coreHttp.jsonRequest<{builds: number[]}>(new URL(uriPATH.concat(["versions", version]).join("/"), "https://api.papermc.io"))).body.builds.at(-1);
 | 
			
		||||
      const data = (await coreHttp.jsonRequest<{time: string, downloads: {[k: string]: {name: string, sha256: string}}}>(new URL(uriPATH.concat(["versions", version, "builds", build.toString()]).join("/"), "https://api.papermc.io"))).body;
 | 
			
		||||
      const fileUrl = new URL(uriPATH.concat(["versions", version, "builds", build.toString(), "downloads", data.downloads["application"].name]).join("/"), "https://api.papermc.io");
 | 
			
		||||
      return {
 | 
			
		||||
        version,
 | 
			
		||||
        date: new Date(data.time),
 | 
			
		||||
        release: "stable",
 | 
			
		||||
        getFile: [{
 | 
			
		||||
          fileURL: fileUrl.toString(),
 | 
			
		||||
          fileName: "server.jar",
 | 
			
		||||
          async stream() {return coreHttp.streamRequest(fileUrl);}
 | 
			
		||||
        }]
 | 
			
		||||
      }
 | 
			
		||||
    }))).sort((b, a) => semver.compare(semver.valid(semver.coerce(a.version)), semver.valid(semver.coerce(b.version))));
 | 
			
		||||
  } else if (altServer === "spigot") {
 | 
			
		||||
    const fileList = await oracleStorage.listFiles("SpigotBuild");
 | 
			
		||||
    return fileList.filter(f => f.name.endsWith(".jar") && !f.name.includes("craftbukkit-")).map((file): javaList => {
 | 
			
		||||
      let version: string;
 | 
			
		||||
      if (!(version = semver.valid(semver.coerce(file.name.replace("SpigotBuild/", "").replace(".jar", ""))))) return null;
 | 
			
		||||
      const craftBuckit = ([...fileList]).reverse().find(file => file.name.startsWith("SpigotBuild/craftbukkit-"+version) || file.name.startsWith("SpigotBuild/craftbukkit-"+(version.endsWith(".0") ? version.slice(0, -2) : version)));
 | 
			
		||||
      return {
 | 
			
		||||
        release: "stable",
 | 
			
		||||
        version,
 | 
			
		||||
        getFile: [
 | 
			
		||||
          ...(craftBuckit ? [{
 | 
			
		||||
            fileName: path.basename(craftBuckit.name),
 | 
			
		||||
            async stream() {
 | 
			
		||||
              return craftBuckit.getFile();
 | 
			
		||||
            },
 | 
			
		||||
          }] : []),
 | 
			
		||||
          {
 | 
			
		||||
            fileName: "server.jar",
 | 
			
		||||
            async stream() {
 | 
			
		||||
              return file.getFile();
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      };
 | 
			
		||||
    }).filter(rel => !!rel).sort((b, a) => semver.compare(semver.valid(semver.coerce(a.version)), semver.valid(semver.coerce(b.version))));
 | 
			
		||||
  } else if (altServer === "glowstone") {
 | 
			
		||||
    const repo = await Github.repositoryManeger("GlowstoneMC", "Glowstone");
 | 
			
		||||
    const rels = await repo.release.getRelease();
 | 
			
		||||
    return rels.filter(rel => !!rel.assets.find(asset => asset.name.endsWith(".jar"))).map(rel => ({
 | 
			
		||||
      version: rel.tag_name,
 | 
			
		||||
      release: "stable",
 | 
			
		||||
      fileUrl: rel.assets.find(asset => asset.name.endsWith(".jar")).browser_download_url,
 | 
			
		||||
      getFile: [{
 | 
			
		||||
        fileURL: rel.assets.find(asset => asset.name.endsWith(".jar")).browser_download_url,
 | 
			
		||||
        fileName: "server.jar",
 | 
			
		||||
        async stream() {
 | 
			
		||||
          return coreHttp.streamRequest(rel.assets.find(asset => asset.name.endsWith(".jar")).browser_download_url);
 | 
			
		||||
        },
 | 
			
		||||
      }]
 | 
			
		||||
    }));
 | 
			
		||||
  } else if (altServer === "cuberite") {
 | 
			
		||||
    const buildFiles: {buildNumber: number; project: string; releaseDate: Date; url: string;}[] = [];
 | 
			
		||||
    for (const project of ["android", "linux-aarch64", "linux-armhf", "linux-i386", "linux-x86_64", "darwin-x86_64"]) {
 | 
			
		||||
      const { builds = [] } = await coreHttp.jsonRequestBody<{builds: {number: number, _class: string}[]}>(`https://builds.cuberite.org/job/${project}/api/json`);
 | 
			
		||||
      await Promise.all(builds.slice(0, 16).map(async job => {
 | 
			
		||||
        const { artifacts = [], result, timestamp } = await coreHttp.jsonRequestBody<{timestamp: number, result: string, artifacts: {relativePath: string, fileName: string}[]}>(`https://builds.cuberite.org/job/${project}/${job.number}/api/json`);
 | 
			
		||||
        if (result !== "SUCCESS") return;
 | 
			
		||||
        artifacts.filter(file => !file.fileName.endsWith(".sha1")).forEach(file => buildFiles.push({
 | 
			
		||||
          project,
 | 
			
		||||
          url: `https://builds.cuberite.org/job/${project}/${job.number}/artifact/${file.relativePath}`,
 | 
			
		||||
          buildNumber: job.number,
 | 
			
		||||
          releaseDate: new Date(timestamp),
 | 
			
		||||
        }));
 | 
			
		||||
      }));
 | 
			
		||||
    }
 | 
			
		||||
    return buildFiles.concat([
 | 
			
		||||
      {
 | 
			
		||||
        buildNumber: -1,
 | 
			
		||||
        project: "windows-x86_64",
 | 
			
		||||
        releaseDate: new Date(),
 | 
			
		||||
        url: "https://download.cuberite.org/windows-x86_64/Cuberite.zip",
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        buildNumber: -1,
 | 
			
		||||
        project: "windows-x86",
 | 
			
		||||
        releaseDate: new Date(),
 | 
			
		||||
        url: "https://download.cuberite.org/windows-i386/Cuberite.zip",
 | 
			
		||||
      },
 | 
			
		||||
    ]).sort((b, a) => a.releaseDate.getTime() - b.releaseDate.getTime()).map(rel => ({
 | 
			
		||||
      date: rel.releaseDate,
 | 
			
		||||
      version: `${rel.project}_${rel.buildNumber}`,
 | 
			
		||||
      platform: rel.project.startsWith("windows") ? "win32" : rel.project.startsWith("linux") ? "linux" : rel.project.startsWith("android") ? "android" : rel.project.startsWith("darwin") ? "darwin" : undefined,
 | 
			
		||||
      architecture: rel.project.endsWith("x86_64") ? "x64" : rel.project.endsWith("i386") ? "ia32" : rel.project.endsWith("armhf") ? "arm" : rel.project.endsWith("aarch64") ? "arm64" : undefined,
 | 
			
		||||
      release: "stable",
 | 
			
		||||
      fileUrl: rel.url,
 | 
			
		||||
      getFile: [{
 | 
			
		||||
        fileName: "server.jar",
 | 
			
		||||
        async stream() {
 | 
			
		||||
          return coreHttp.streamRequest(rel.url);
 | 
			
		||||
        },
 | 
			
		||||
      }]
 | 
			
		||||
    }));
 | 
			
		||||
  } else if (altServer === "mojang") {
 | 
			
		||||
    return (await Promise.all((await coreHttp.jsonRequest<{versions: {id: string, releaseTime: string, url: string, type: "snapshot"|"release"}[]}>("https://launchermeta.mojang.com/mc/game/version_manifest_v2.json")).body.versions.map(async (data): Promise<javaList> => {
 | 
			
		||||
      const fileURL = (await coreHttp.jsonRequest<{downloads: {[k: string]: {size: number, url: string}}}>(data.url)).body.downloads?.["server"]?.url;
 | 
			
		||||
      if (!fileURL) return null;
 | 
			
		||||
      return {
 | 
			
		||||
        version: data.id,
 | 
			
		||||
        date: new Date(data.releaseTime),
 | 
			
		||||
        release: data.type === "snapshot" ? "snapshot" : "stable",
 | 
			
		||||
        getFile: [{
 | 
			
		||||
          fileName: "server.jar",
 | 
			
		||||
          async stream() {
 | 
			
		||||
            return coreHttp.streamRequest(fileURL);
 | 
			
		||||
          },
 | 
			
		||||
        }],
 | 
			
		||||
      };
 | 
			
		||||
    }))).filter(a => !!a);
 | 
			
		||||
  } else throw new Error("Invalid platform");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function installServer(serverPath: serverManegerV1, options: javaOptions & {version?: string, allowBeta?: boolean}) {
 | 
			
		||||
  if (!options.altServer) options.altServer = "mojang";
 | 
			
		||||
  const version = (await listVersions(options.altServer)).filter(rel => rel.release === "stable" ? true : !!options.allowBeta).find(rel => (!options.version || options.version === "latest" || rel.version === options.version));
 | 
			
		||||
  if (!version) throw new Error("The specified version does not exist!");
 | 
			
		||||
  for (const file of version.getFile) await pipeline(await file.stream(), fs.createWriteStream(path.join(serverPath.serverFolder, file.fileName)));
 | 
			
		||||
  await fs.promises.writeFile(path.join(serverPath.serverFolder, "eula.txt"), "eula=true\n");
 | 
			
		||||
  return {
 | 
			
		||||
    id: serverPath.id,
 | 
			
		||||
    version: version.version,
 | 
			
		||||
    release: version.release,
 | 
			
		||||
    date: version.date,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function startServer(serverPath: serverManegerV1, options: javaOptions) {
 | 
			
		||||
  if (!options.altServer) options.altServer = "mojang";
 | 
			
		||||
  // Java server
 | 
			
		||||
  if (await extendsFS.exists(path.join(serverPath.serverFolder, "server.jar"))) {
 | 
			
		||||
    return serverPath.runCommand({
 | 
			
		||||
      command: "java",
 | 
			
		||||
      args: [
 | 
			
		||||
        "-XX:+UseG1GC",
 | 
			
		||||
        "-XX:+ParallelRefProcEnabled",
 | 
			
		||||
        "-XX:MaxGCPauseMillis=200",
 | 
			
		||||
        "-XX:+UnlockExperimentalVMOptions",
 | 
			
		||||
        "-XX:+DisableExplicitGC",
 | 
			
		||||
        "-XX:+AlwaysPreTouch",
 | 
			
		||||
        "-XX:G1NewSizePercent=30",
 | 
			
		||||
        "-XX:G1MaxNewSizePercent=40",
 | 
			
		||||
        "-XX:G1HeapRegionSize=8M",
 | 
			
		||||
        "-XX:G1ReservePercent=20",
 | 
			
		||||
        "-XX:G1HeapWastePercent=5",
 | 
			
		||||
        "-XX:G1MixedGCCountTarget=4",
 | 
			
		||||
        "-XX:InitiatingHeapOccupancyPercent=15",
 | 
			
		||||
        "-XX:G1MixedGCLiveThresholdPercent=90",
 | 
			
		||||
        "-XX:G1RSetUpdatingPauseTimePercent=5",
 | 
			
		||||
        "-XX:SurvivorRatio=32",
 | 
			
		||||
        "-XX:+PerfDisableSharedMem",
 | 
			
		||||
        "-XX:MaxTenuringThreshold=1",
 | 
			
		||||
        "-Dusing.aikars.flags=https://mcflags.emc.gs",
 | 
			
		||||
        "-Daikars.new.flags=true",
 | 
			
		||||
        "-jar", "server.jar",
 | 
			
		||||
        "--nogui",
 | 
			
		||||
        // Save world in worlds folder
 | 
			
		||||
        "--universe", "worlds",
 | 
			
		||||
        // ...extraArgs,
 | 
			
		||||
      ],
 | 
			
		||||
      paths: serverPath,
 | 
			
		||||
      serverActions: {
 | 
			
		||||
        stop() {
 | 
			
		||||
          this.sendCommand("stop");
 | 
			
		||||
        },
 | 
			
		||||
        portListen(lineString) {
 | 
			
		||||
          // [21:19:18] [Server thread/INFO]: Starting Minecraft server on *:25565
 | 
			
		||||
          lineString = lineString.replace(/^.*?\[.*\]:/, "").trim();
 | 
			
		||||
          if (lineString.startsWith("Starting Minecraft server on")) {
 | 
			
		||||
            if (lineString.lastIndexOf(":") === -1) return null;
 | 
			
		||||
            const port = Number(lineString.slice(lineString.lastIndexOf(":")+1));
 | 
			
		||||
            return {
 | 
			
		||||
              port,
 | 
			
		||||
              protocol: "TCP",
 | 
			
		||||
              listenFrom: "server"
 | 
			
		||||
            };
 | 
			
		||||
          }
 | 
			
		||||
          return null;
 | 
			
		||||
        },
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  throw new Error("install server or check if server installed correctly!");
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user