Add Nomifactory's build scripts
Modified slightly to use LICENSE rather than LICENSE.md
This commit is contained in:
parent
ff5f5642a4
commit
7a329bc07b
17
buildtools/.eslintrc.js
Normal file
17
buildtools/.eslintrc.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
module.exports = {
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
sourceType: "module",
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: ["@typescript-eslint"],
|
||||||
|
extends: ["plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"],
|
||||||
|
rules: {
|
||||||
|
quotes: [2, "double", { avoidEscape: true }],
|
||||||
|
semi: [2, "always"],
|
||||||
|
"eol-last": [2, "always"],
|
||||||
|
},
|
||||||
|
};
|
1
buildtools/.gitignore
vendored
Normal file
1
buildtools/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
node_modules
|
7
buildtools/.prettierrc.js
Normal file
7
buildtools/.prettierrc.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
semi: true,
|
||||||
|
trailingComma: "all",
|
||||||
|
printWidth: 120,
|
||||||
|
useTabs: true,
|
||||||
|
alignObjectProperties: true
|
||||||
|
};
|
9
buildtools/.vscode/settings.json
vendored
Normal file
9
buildtools/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"typescript.tsdk": "node_modules\\typescript\\lib",
|
||||||
|
"eslint.workingDirectories": [
|
||||||
|
"./",
|
||||||
|
],
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": true
|
||||||
|
}
|
||||||
|
}
|
8
buildtools/buildConfig.ts
Normal file
8
buildtools/buildConfig.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import buildConfig from "./util/buildConfig.default.json";
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
if (fs.existsSync("./config.json")) {
|
||||||
|
Object.assign(buildConfig, JSON.parse(fs.readFileSync("./config.json").toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default buildConfig;
|
12
buildtools/config.json
Normal file
12
buildtools/config.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"copyFromSharedServerGlobs": [
|
||||||
|
"overrides/**/*",
|
||||||
|
"!overrides/resources/**/*"
|
||||||
|
],
|
||||||
|
"copyFromSharedClientGlobs": [
|
||||||
|
"overrides/**/*",
|
||||||
|
"!overrides/resources/minecraft/textures/gui/title/background/*"
|
||||||
|
],
|
||||||
|
"nightlyHookAvatar": "https://github.com/NomifactoryDevs.png",
|
||||||
|
"nightlyHookName": "Nightly Builds"
|
||||||
|
}
|
13
buildtools/globals.ts
Normal file
13
buildtools/globals.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import buildConfig from "./buildConfig";
|
||||||
|
import upath from "upath";
|
||||||
|
import manifest from "./../manifest.json";
|
||||||
|
import { ModpackManifest } from "./types/modpackManifest";
|
||||||
|
|
||||||
|
export const sharedDestDirectory = upath.join(buildConfig.buildDestinationDirectory, "shared");
|
||||||
|
export const clientDestDirectory = upath.join(buildConfig.buildDestinationDirectory, "client");
|
||||||
|
export const mmcDestDirectory = upath.join(buildConfig.buildDestinationDirectory, "mmc");
|
||||||
|
export const serverDestDirectory = upath.join(buildConfig.buildDestinationDirectory, "server");
|
||||||
|
export const langDestDirectory = upath.join(buildConfig.buildDestinationDirectory, "lang");
|
||||||
|
export const tempDirectory = upath.join(buildConfig.buildDestinationDirectory, "temp");
|
||||||
|
export const modpackManifest = manifest as ModpackManifest;
|
||||||
|
export const overridesFolder = modpackManifest.overrides || "overrides";
|
38
buildtools/gulpfile.ts
Normal file
38
buildtools/gulpfile.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import * as gulp from "gulp";
|
||||||
|
|
||||||
|
import pruneCacheTask from "./tasks/misc/pruneCache";
|
||||||
|
export const pruneCache = pruneCacheTask;
|
||||||
|
|
||||||
|
import sharedTasks from "./tasks/shared";
|
||||||
|
import clientTasks from "./tasks/client";
|
||||||
|
import serverTasks from "./tasks/server";
|
||||||
|
import langTasks from "./tasks/lang";
|
||||||
|
import mmcTasks from "./tasks/mmc";
|
||||||
|
|
||||||
|
export const buildClient = gulp.series(sharedTasks, clientTasks);
|
||||||
|
export const buildServer = gulp.series(sharedTasks, serverTasks);
|
||||||
|
export const buildLang = gulp.series(sharedTasks, langTasks);
|
||||||
|
export const buildAll = gulp.series(sharedTasks, gulp.series(clientTasks, serverTasks, langTasks));
|
||||||
|
export const buildMMC = gulp.series(sharedTasks, clientTasks, mmcTasks);
|
||||||
|
|
||||||
|
import checkTasks from "./tasks/checks";
|
||||||
|
export const check = gulp.series(checkTasks);
|
||||||
|
|
||||||
|
import * as zip from "./tasks/misc/zip";
|
||||||
|
export const zipClient = zip.zipClient;
|
||||||
|
export const zipServer = zip.zipServer;
|
||||||
|
export const zipLang = zip.zipLang;
|
||||||
|
export const zipAll = zip.zipAll;
|
||||||
|
export const zipMMC = zip.zipMMC;
|
||||||
|
|
||||||
|
import * as gha from "./tasks/misc/gha";
|
||||||
|
export const makeArtifactNames = gha.makeArtifactNames;
|
||||||
|
|
||||||
|
import deployCurseForgeTask from "./tasks/deploy/curseforge";
|
||||||
|
export const deployCurseForge = deployCurseForgeTask;
|
||||||
|
|
||||||
|
import deployReleasesTask from "./tasks/deploy/releases";
|
||||||
|
export const deployReleases = deployReleasesTask;
|
||||||
|
|
||||||
|
import fireNightlyWebhookTask from "./tasks/misc/webhook";
|
||||||
|
export const fireNightlyWebhook = fireNightlyWebhookTask;
|
20418
buildtools/package-lock.json
generated
Normal file
20418
buildtools/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
47
buildtools/package.json
Normal file
47
buildtools/package.json
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "nomifactory-buildtools",
|
||||||
|
"version": "1.2.2",
|
||||||
|
"description": "Nomifactory Server Builder.",
|
||||||
|
"main": "gulpfile.js",
|
||||||
|
"author": "NotMyWing",
|
||||||
|
"license": "LGPL-3.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@octokit/rest": "^18.3.5",
|
||||||
|
"@types/bluebird": "^3.5.33",
|
||||||
|
"@types/fancy-log": "^1.3.1",
|
||||||
|
"@types/gulp": "^4.0.8",
|
||||||
|
"@types/gulp-imagemin": "^7.0.2",
|
||||||
|
"@types/gulp-rename": "^2.0.0",
|
||||||
|
"@types/gulp-zip": "^4.0.1",
|
||||||
|
"@types/merge-stream": "^1.1.2",
|
||||||
|
"@types/mustache": "^4.1.1",
|
||||||
|
"@types/requestretry": "^1.12.7",
|
||||||
|
"@types/sha1": "^1.1.2",
|
||||||
|
"@types/unzipper": "^0.10.3",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^4.18.0",
|
||||||
|
"@typescript-eslint/parser": "^4.18.0",
|
||||||
|
"bluebird": "^3.7.2",
|
||||||
|
"del": "^6.0.0",
|
||||||
|
"discord-webhook-node": "^1.1.8",
|
||||||
|
"eslint": "^7.22.0",
|
||||||
|
"eslint-config-prettier": "^8.1.0",
|
||||||
|
"eslint-plugin-prettier": "^3.3.1",
|
||||||
|
"fancy-log": "^1.3.3",
|
||||||
|
"gulp": "^4.0.2",
|
||||||
|
"gulp-imagemin": "^7.1.0",
|
||||||
|
"gulp-rename": "^2.0.0",
|
||||||
|
"gulp-zip": "^5.1.0",
|
||||||
|
"merge-stream": "^2.0.0",
|
||||||
|
"mustache": "^4.1.0",
|
||||||
|
"png-to-jpeg": "^1.0.1",
|
||||||
|
"prettier": "^2.2.1",
|
||||||
|
"request": "^2.88.2",
|
||||||
|
"requestretry": "^5.0.0",
|
||||||
|
"sanitize-filename": "^1.6.3",
|
||||||
|
"sha1": "^1.1.1",
|
||||||
|
"ts-node": "^9.1.1",
|
||||||
|
"typescript": "^4.2.3",
|
||||||
|
"unzipper": "^0.10.11",
|
||||||
|
"upath": "^2.0.1"
|
||||||
|
}
|
||||||
|
}
|
25
buildtools/tasks/checks/index.ts
Normal file
25
buildtools/tasks/checks/index.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import gulp from "gulp";
|
||||||
|
import { checkEnvironmentalVariables } from "../../util/util";
|
||||||
|
|
||||||
|
const vars = [
|
||||||
|
"GITHUB_TOKEN",
|
||||||
|
"GITHUB_REPOSITORY",
|
||||||
|
"GITHUB_SHA",
|
||||||
|
"GITHUB_REF",
|
||||||
|
"CURSEFORGE_PROJECT_ID",
|
||||||
|
"CURSEFORGE_API_TOKEN",
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check required env. variables for vailidity and cancel
|
||||||
|
* the build if something is unset.
|
||||||
|
*/
|
||||||
|
async function checkEnv() {
|
||||||
|
checkEnvironmentalVariables(vars);
|
||||||
|
|
||||||
|
if (!/.+\/.+/.exec(process.env.GITHUB_REPOSITORY)) {
|
||||||
|
throw new Error("Malformed repository slug.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default gulp.series(checkEnv);
|
181
buildtools/tasks/client/index.ts
Normal file
181
buildtools/tasks/client/index.ts
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
import gulp from "gulp";
|
||||||
|
import { clientDestDirectory, modpackManifest, overridesFolder, sharedDestDirectory } from "../../globals";
|
||||||
|
import fs from "fs";
|
||||||
|
import upath from "upath";
|
||||||
|
import buildConfig from "../../buildConfig";
|
||||||
|
import { fetchProjectsBulk } from "../../util/curseForgeAPI";
|
||||||
|
import log from "fancy-log";
|
||||||
|
import rename from "gulp-rename";
|
||||||
|
import imagemin from "gulp-imagemin";
|
||||||
|
import pngToJpeg from "png-to-jpeg";
|
||||||
|
import { MainMenuConfig } from "../../types/mainMenuConfig";
|
||||||
|
import del from "del";
|
||||||
|
|
||||||
|
async function clientCleanUp() {
|
||||||
|
return del(upath.join(clientDestDirectory, "*"), { force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks and creates all necessary directories so we can build the client safely.
|
||||||
|
*/
|
||||||
|
async function createClientDirs() {
|
||||||
|
if (!fs.existsSync(clientDestDirectory)) {
|
||||||
|
return fs.promises.mkdir(clientDestDirectory, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports the modpack manifest.
|
||||||
|
*/
|
||||||
|
async function exportModpackManifest() {
|
||||||
|
const manifestPath = upath.join(clientDestDirectory, "manifest.json");
|
||||||
|
|
||||||
|
// Filter client side files only and prune build-specific fields.
|
||||||
|
const newFiles = modpackManifest.files
|
||||||
|
.map((file) => {
|
||||||
|
if (file.sides) {
|
||||||
|
if (!file.sides.includes("client")) return;
|
||||||
|
|
||||||
|
const newFile = Object.assign({}, file);
|
||||||
|
delete newFile.sides;
|
||||||
|
|
||||||
|
return newFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
return file;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
return fs.promises.writeFile(
|
||||||
|
manifestPath,
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
...modpackManifest,
|
||||||
|
files: newFiles,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
" ",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies the license file.
|
||||||
|
*/
|
||||||
|
async function copyClientLicense() {
|
||||||
|
return gulp.src("../LICENSE").pipe(gulp.dest(clientDestDirectory));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies the update notes file.
|
||||||
|
*/
|
||||||
|
function copyClientUpdateNotes() {
|
||||||
|
return gulp.src("../UPDATENOTES.md", { allowEmpty: true }).pipe(gulp.dest(clientDestDirectory));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies the changelog file.
|
||||||
|
*/
|
||||||
|
function copyClientChangelog() {
|
||||||
|
return gulp.src(upath.join(sharedDestDirectory, "CHANGELOG.md")).pipe(gulp.dest(clientDestDirectory));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies modpack overrides.
|
||||||
|
*/
|
||||||
|
function copyClientOverrides() {
|
||||||
|
return gulp
|
||||||
|
.src(buildConfig.copyFromSharedClientGlobs, { nodir: true, cwd: sharedDestDirectory, allowEmpty: true })
|
||||||
|
.pipe(gulp.symlink(upath.join(clientDestDirectory, "overrides")));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches mod links and builds modlist.html.
|
||||||
|
*/
|
||||||
|
async function fetchModList() {
|
||||||
|
log("Fetching mod infos...");
|
||||||
|
|
||||||
|
// Fetch project/addon infos.
|
||||||
|
const modInfos = await fetchProjectsBulk(modpackManifest.files.map((mod) => mod.projectID));
|
||||||
|
|
||||||
|
log(`Fetched ${modInfos.length} mod infos`);
|
||||||
|
|
||||||
|
// Create modlist.html
|
||||||
|
const output = [
|
||||||
|
"<ul>\r\n",
|
||||||
|
...modInfos
|
||||||
|
// Sort mods by their project IDs.
|
||||||
|
.sort((a, b) => a.id - b.id)
|
||||||
|
|
||||||
|
// Create a <li> node for each mod.
|
||||||
|
.map((modInfo) => {
|
||||||
|
return `\t<li><a href="${modInfo.websiteUrl}">${modInfo.name || "Unknown"} (by ${modInfo.authors
|
||||||
|
.map((author) => author.name || "Someone")
|
||||||
|
.join(", ")})</a></li>\r\n`;
|
||||||
|
}),
|
||||||
|
"</ul>",
|
||||||
|
];
|
||||||
|
|
||||||
|
return fs.promises.writeFile(upath.join(clientDestDirectory, "modlist.html"), output.join(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
const bgImageNamespace = "minecraft";
|
||||||
|
const bgImagePath = "textures/gui/title/background";
|
||||||
|
const mainMenuConfigPath = "config/CustomMainMenu/mainmenu.json";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minifies (converts to jpeg) main menu files so they don't take up 60% of the pack size.
|
||||||
|
*/
|
||||||
|
async function compressMainMenuImages() {
|
||||||
|
const mainMenuImages = [];
|
||||||
|
const bgImagePathReal = upath.join("resources", bgImageNamespace, bgImagePath);
|
||||||
|
|
||||||
|
// Convert each slideshow image to 80% jpg.
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
gulp
|
||||||
|
.src(upath.join(sharedDestDirectory, overridesFolder, bgImagePathReal, "**/*"))
|
||||||
|
.pipe(imagemin([pngToJpeg({ quality: 80 })]))
|
||||||
|
.pipe(
|
||||||
|
rename((f) => {
|
||||||
|
// xd
|
||||||
|
f.extname = ".jpg";
|
||||||
|
|
||||||
|
// Ping back the file name so we don't have to scan the folder again.
|
||||||
|
mainMenuImages.push(`${f.basename}${f.extname}`);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.pipe(gulp.dest(upath.join(clientDestDirectory, overridesFolder, bgImagePathReal)))
|
||||||
|
.on("end", resolve);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mainMenuImages.length > 0) {
|
||||||
|
// Read the CustomMainMenu config and parse it.
|
||||||
|
const mainMenuConfig: MainMenuConfig = JSON.parse(
|
||||||
|
(await fs.promises.readFile(upath.join(clientDestDirectory, overridesFolder, mainMenuConfigPath))).toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fill the config with image paths using the weird "namespace:path" scheme.
|
||||||
|
mainMenuConfig.other.background.slideshow.images = mainMenuImages.map(
|
||||||
|
(img) => bgImageNamespace + ":" + upath.join(bgImagePath, img),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Write it back.
|
||||||
|
return fs.promises.writeFile(
|
||||||
|
upath.join(clientDestDirectory, overridesFolder, mainMenuConfigPath),
|
||||||
|
JSON.stringify(mainMenuConfig, null, " "),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default gulp.series(
|
||||||
|
clientCleanUp,
|
||||||
|
createClientDirs,
|
||||||
|
copyClientOverrides,
|
||||||
|
exportModpackManifest,
|
||||||
|
copyClientLicense,
|
||||||
|
copyClientOverrides,
|
||||||
|
copyClientChangelog,
|
||||||
|
copyClientUpdateNotes,
|
||||||
|
fetchModList,
|
||||||
|
compressMainMenuImages,
|
||||||
|
);
|
120
buildtools/tasks/deploy/curseforge.ts
Normal file
120
buildtools/tasks/deploy/curseforge.ts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import { modpackManifest, sharedDestDirectory } from "../../globals";
|
||||||
|
|
||||||
|
import request from "requestretry";
|
||||||
|
import fs from "fs";
|
||||||
|
import log from "fancy-log";
|
||||||
|
import upath from "upath";
|
||||||
|
import buildConfig from "../../buildConfig";
|
||||||
|
import { makeArtifactNameBody } from "../../util/util";
|
||||||
|
import sanitize from "sanitize-filename";
|
||||||
|
|
||||||
|
const CURSEFORGE_ENDPOINT = "https://minecraft.curseforge.com/";
|
||||||
|
const variablesToCheck = ["CURSEFORGE_API_TOKEN", "CURSEFORGE_PROJECT_ID", "GITHUB_TAG"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads build artifacts to CurseForge.
|
||||||
|
*/
|
||||||
|
async function deployCurseForge(): Promise<void> {
|
||||||
|
/**
|
||||||
|
* Obligatory variable check.
|
||||||
|
*/
|
||||||
|
variablesToCheck.forEach((vari) => {
|
||||||
|
if (!process.env[vari]) {
|
||||||
|
throw new Error(`Environmental variable ${vari} is unset.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const tag = process.env.GITHUB_TAG;
|
||||||
|
const flavorTitle = process.env.BUILD_FLAVOR_TITLE;
|
||||||
|
const displayName = [modpackManifest.name, tag.replace(/^v/, ""), flavorTitle].filter(Boolean).join(" - ");
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
{
|
||||||
|
name: sanitize((makeArtifactNameBody(modpackManifest.name) + "-client.zip").toLowerCase()),
|
||||||
|
displayName: displayName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: sanitize((makeArtifactNameBody(modpackManifest.name) + "-server.zip").toLowerCase()),
|
||||||
|
displayName: `${displayName} Server`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obligatory file check.
|
||||||
|
*/
|
||||||
|
files.forEach((file) => {
|
||||||
|
const path = upath.join(buildConfig.buildDestinationDirectory, file.name);
|
||||||
|
if (!fs.existsSync(path)) {
|
||||||
|
throw new Error(`File ${path} doesn't exist!`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Since we've built everything beforehand, the changelog must be available in the shared directory.
|
||||||
|
const changelog = await (await fs.promises.readFile(upath.join(sharedDestDirectory, "CHANGELOG.md")))
|
||||||
|
.toString()
|
||||||
|
.replace(/\n/g, " \n")
|
||||||
|
.replace(/\n\*/g, "\n•");
|
||||||
|
|
||||||
|
const tokenHeaders = {
|
||||||
|
"X-Api-Token": process.env.CURSEFORGE_API_TOKEN,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch the list of Minecraft versions from CurseForge.
|
||||||
|
log("Fetching CurseForge version manifest...");
|
||||||
|
const versionsManifest =
|
||||||
|
(await request({
|
||||||
|
uri: CURSEFORGE_ENDPOINT + "api/game/versions",
|
||||||
|
headers: tokenHeaders,
|
||||||
|
method: "GET",
|
||||||
|
json: true,
|
||||||
|
fullResponse: false,
|
||||||
|
maxAttempts: 5,
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
const version = versionsManifest.find((m) => m.name == modpackManifest.minecraft.version);
|
||||||
|
|
||||||
|
if (!version) {
|
||||||
|
throw new Error(`Version ${modpackManifest.minecraft.version} not found on CurseForge.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let clientFileID: number | null;
|
||||||
|
|
||||||
|
// Upload artifacts.
|
||||||
|
for (const file of files) {
|
||||||
|
const options = {
|
||||||
|
uri: CURSEFORGE_ENDPOINT + `api/projects/${process.env.CURSEFORGE_PROJECT_ID}/upload-file`,
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
...tokenHeaders,
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
formData: {
|
||||||
|
metadata: JSON.stringify({
|
||||||
|
changelog: changelog,
|
||||||
|
changelogType: "markdown",
|
||||||
|
releaseType: "release",
|
||||||
|
parentFileID: clientFileID,
|
||||||
|
gameVersions: clientFileID ? undefined : [version.id],
|
||||||
|
displayName: file.displayName,
|
||||||
|
}),
|
||||||
|
file: fs.createReadStream(upath.join(buildConfig.buildDestinationDirectory, file.name)),
|
||||||
|
},
|
||||||
|
json: true,
|
||||||
|
fullResponse: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
log(`Uploading ${file.name} to CurseForge...` + (clientFileID ? `(child of ${clientFileID})` : ""));
|
||||||
|
|
||||||
|
const response = await request(options);
|
||||||
|
|
||||||
|
if (response && response.id) {
|
||||||
|
if (!clientFileID) {
|
||||||
|
clientFileID = response.id;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`Failed to upload ${file.name}: Invalid Response.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default deployCurseForge;
|
87
buildtools/tasks/deploy/releases.ts
Normal file
87
buildtools/tasks/deploy/releases.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { modpackManifest, sharedDestDirectory } from "../../globals";
|
||||||
|
|
||||||
|
import fs from "fs";
|
||||||
|
import upath from "upath";
|
||||||
|
import buildConfig from "../../buildConfig";
|
||||||
|
import { makeArtifactNameBody } from "../../util/util";
|
||||||
|
import Bluebird from "bluebird";
|
||||||
|
import { Octokit } from "@octokit/rest";
|
||||||
|
import sanitize from "sanitize-filename";
|
||||||
|
|
||||||
|
const variablesToCheck = ["GITHUB_TAG", "GITHUB_TOKEN", "GITHUB_REPOSITORY"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads build artifacts to GitHub Releases.
|
||||||
|
*/
|
||||||
|
async function deployReleases(): Promise<void> {
|
||||||
|
/**
|
||||||
|
* Obligatory variable check.
|
||||||
|
*/
|
||||||
|
variablesToCheck.forEach((vari) => {
|
||||||
|
if (!process.env[vari]) {
|
||||||
|
throw new Error(`Environmental variable ${vari} is unset.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const body = makeArtifactNameBody(modpackManifest.name);
|
||||||
|
const files = ["client", "server", "lang"].map((file) => sanitize(`${body}-${file}.zip`.toLowerCase()));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obligatory file check.
|
||||||
|
*/
|
||||||
|
files.forEach((file) => {
|
||||||
|
const path = upath.join(buildConfig.buildDestinationDirectory, file);
|
||||||
|
if (!fs.existsSync(path)) {
|
||||||
|
throw new Error(`File ${path} doesn't exist!`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const octokit = new Octokit({
|
||||||
|
auth: process.env.GITHUB_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsedSlug = /(.+)\/(.+)/.exec(process.env.GITHUB_REPOSITORY);
|
||||||
|
if (!parsedSlug) {
|
||||||
|
throw new Error("No/malformed GitHub repository slug provided.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const repo = {
|
||||||
|
owner: parsedSlug[1],
|
||||||
|
repo: parsedSlug[2],
|
||||||
|
};
|
||||||
|
|
||||||
|
const tag = process.env.GITHUB_TAG;
|
||||||
|
const flavorTitle = process.env.BUILD_FLAVOR_TITLE;
|
||||||
|
|
||||||
|
// Since we've built everything beforehand, the changelog must be available in the shared directory.
|
||||||
|
const changelog = await (await fs.promises.readFile(upath.join(sharedDestDirectory, "CHANGELOG.md"))).toString();
|
||||||
|
|
||||||
|
// Create a release.
|
||||||
|
const release = await octokit.repos.createRelease({
|
||||||
|
tag_name: tag || "latest-dev-preview",
|
||||||
|
prerelease: !tag,
|
||||||
|
name: [modpackManifest.name, tag.replace(/^v/, ""), flavorTitle].filter(Boolean).join(" - "),
|
||||||
|
body: changelog,
|
||||||
|
...repo,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Upload artifacts.
|
||||||
|
await Bluebird.map(files, async (file) => {
|
||||||
|
await octokit.repos.uploadReleaseAsset({
|
||||||
|
name: file,
|
||||||
|
release_id: release.data.id,
|
||||||
|
...repo,
|
||||||
|
|
||||||
|
// Dumb workaround thanks to broken typings.
|
||||||
|
data: (await fs.promises.readFile(upath.join(buildConfig.buildDestinationDirectory, file))) as unknown as string,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await octokit.repos.updateRelease({
|
||||||
|
release_id: release.data.id,
|
||||||
|
draft: false,
|
||||||
|
...repo,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default deployReleases;
|
38
buildtools/tasks/lang/index.ts
Normal file
38
buildtools/tasks/lang/index.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import gulp from "gulp";
|
||||||
|
import rename from "gulp-rename";
|
||||||
|
import merge from "merge-stream";
|
||||||
|
import upath from "upath";
|
||||||
|
import buildConfig from "../../buildConfig";
|
||||||
|
import { langDestDirectory, overridesFolder, sharedDestDirectory } from "../../globals";
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks and creates all necessary directories so we can build the client safely.
|
||||||
|
*/
|
||||||
|
async function createLangDirs() {
|
||||||
|
if (!fs.existsSync(langDestDirectory)) {
|
||||||
|
await fs.promises.mkdir(langDestDirectory, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyLang() {
|
||||||
|
const resourcesPath = upath.join(sharedDestDirectory, overridesFolder, "resources");
|
||||||
|
|
||||||
|
const opts = { nodir: true, base: resourcesPath };
|
||||||
|
const streams = [
|
||||||
|
gulp.src(upath.join(resourcesPath, "pack.mcmeta"), opts),
|
||||||
|
gulp.src(upath.join(resourcesPath, "**/*.lang"), opts).pipe(
|
||||||
|
rename((f) => {
|
||||||
|
f.dirname = upath.join("assets", f.dirname);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
return await new Promise((resolve) => {
|
||||||
|
merge(...streams)
|
||||||
|
.pipe(gulp.dest(upath.join(buildConfig.buildDestinationDirectory, langDestDirectory)))
|
||||||
|
.on("end", resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default gulp.series(createLangDirs, copyLang);
|
16
buildtools/tasks/misc/gha.ts
Normal file
16
buildtools/tasks/misc/gha.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { modpackManifest } from "../../globals";
|
||||||
|
import { makeArtifactNameBody } from "../../util/util";
|
||||||
|
import sanitize from "sanitize-filename";
|
||||||
|
|
||||||
|
export async function makeArtifactNames(): Promise<void> {
|
||||||
|
const body = makeArtifactNameBody(modpackManifest.name);
|
||||||
|
const names = {
|
||||||
|
client: body + "-client",
|
||||||
|
server: body + "-server",
|
||||||
|
lang: body + "-lang",
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.keys(names).forEach((name) => {
|
||||||
|
console.log(`::set-output name=${name}::${sanitize(names[name].toLowerCase())}`);
|
||||||
|
});
|
||||||
|
}
|
133
buildtools/tasks/misc/pruneCache.ts
Normal file
133
buildtools/tasks/misc/pruneCache.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import Bluebird from "bluebird";
|
||||||
|
import { modpackManifest } from "../../globals";
|
||||||
|
import { downloadOrRetrieveFileDef, getVersionManifest, libraryToPath } from "../../util/util";
|
||||||
|
import unzip from "unzipper";
|
||||||
|
import { ForgeProfile } from "../../types/forgeProfile";
|
||||||
|
import log from "fancy-log";
|
||||||
|
import sha1 from "sha1";
|
||||||
|
import { fetchFileInfo } from "../../util/curseForgeAPI";
|
||||||
|
import { VersionManifest } from "../../types/versionManifest";
|
||||||
|
import fs from "fs";
|
||||||
|
import upath from "upath";
|
||||||
|
import buildConfig from "../../buildConfig";
|
||||||
|
|
||||||
|
const FORGE_VERSION_REG = /forge-(.+)/;
|
||||||
|
const FORGE_MAVEN = "https://files.minecraftforge.net/maven/";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download the Forge jar.
|
||||||
|
*
|
||||||
|
* This is basically a carbon copy of the server Forge-fetching task,
|
||||||
|
* except we only download/fetch the Forge jar and enumerate the libraries it has.
|
||||||
|
*/
|
||||||
|
async function getForgeURLs() {
|
||||||
|
const minecraft = modpackManifest.minecraft;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Break down the Forge version defined in manifest.json.
|
||||||
|
*/
|
||||||
|
const parsedForgeEntry = FORGE_VERSION_REG.exec(
|
||||||
|
(minecraft.modLoaders.find((x) => x.id && x.id.indexOf("forge") != -1) || {}).id || "",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!parsedForgeEntry) {
|
||||||
|
throw new Error("Malformed Forge version in manifest.json.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform Forge version into Maven library path.
|
||||||
|
*/
|
||||||
|
const forgeMavenLibrary = `net.minecraftforge:forge:${minecraft.version}-${parsedForgeEntry[1]}`;
|
||||||
|
const forgeInstallerPath = libraryToPath(forgeMavenLibrary) + "-installer.jar";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the Forge installer
|
||||||
|
*/
|
||||||
|
const forgeJar = await fs.promises.readFile(
|
||||||
|
(
|
||||||
|
await downloadOrRetrieveFileDef({
|
||||||
|
url: FORGE_MAVEN + forgeInstallerPath,
|
||||||
|
})
|
||||||
|
).cachePath,
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the profile manifest.
|
||||||
|
*/
|
||||||
|
let forgeProfile: ForgeProfile;
|
||||||
|
const files = (await unzip.Open.buffer(forgeJar))?.files;
|
||||||
|
|
||||||
|
if (!files) {
|
||||||
|
throw new Error("Malformed Forge installation jar.");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
// Look for the installation profile.
|
||||||
|
if (!forgeProfile && file.path == "version.json") {
|
||||||
|
forgeProfile = JSON.parse((await file.buffer()).toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!forgeProfile || !forgeProfile.libraries) {
|
||||||
|
throw new Error("Malformed Forge installation profile.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finally, fetch libraries.
|
||||||
|
*/
|
||||||
|
const libraries = forgeProfile.libraries.filter((x) => Boolean(x?.downloads?.artifact?.url));
|
||||||
|
|
||||||
|
return [FORGE_MAVEN + forgeInstallerPath, ...libraries.map((library) => library.downloads.artifact.url)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes ALL files from the cache directory that haven't been accessed during this run.
|
||||||
|
*/
|
||||||
|
export default async function pruneCache(): Promise<void> {
|
||||||
|
const urls = [];
|
||||||
|
|
||||||
|
// Push Forge URLs.
|
||||||
|
urls.push(...(await getForgeURLs()).map((url) => url));
|
||||||
|
|
||||||
|
// Fetch file infos.
|
||||||
|
const fileInfos = await Bluebird.map(modpackManifest.files, (file) => fetchFileInfo(file.projectID, file.fileID));
|
||||||
|
urls.push(...fileInfos.map((fileInfo) => fileInfo.downloadUrl));
|
||||||
|
|
||||||
|
// Fetch the Minecraft server.
|
||||||
|
const versionManifest: VersionManifest = await getVersionManifest(modpackManifest.minecraft.version);
|
||||||
|
if (!versionManifest) {
|
||||||
|
throw new Error(`No manifest found for Minecraft ${versionManifest.id}`);
|
||||||
|
}
|
||||||
|
urls.push(versionManifest.downloads.server.url);
|
||||||
|
|
||||||
|
// Push external dependencies.
|
||||||
|
if (modpackManifest.externalDependencies) {
|
||||||
|
urls.push(...modpackManifest.externalDependencies.map((dep) => dep.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
const cache = (await fs.promises.readdir(buildConfig.downloaderCacheDirectory)).filter((entity) =>
|
||||||
|
fs.statSync(upath.join(buildConfig.downloaderCacheDirectory, entity)).isFile(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const shaMap: { [key: string]: boolean } = urls.reduce((map, url) => ((map[sha1(url)] = true), map), {});
|
||||||
|
|
||||||
|
let count = 0,
|
||||||
|
bytes = 0;
|
||||||
|
|
||||||
|
for (const sha of cache) {
|
||||||
|
if (!shaMap[sha]) {
|
||||||
|
const path = upath.join(buildConfig.downloaderCacheDirectory, sha);
|
||||||
|
const stat = fs.existsSync(path) ? await fs.promises.stat(path) : null;
|
||||||
|
|
||||||
|
if (stat && stat.isFile()) {
|
||||||
|
count += 1;
|
||||||
|
bytes += stat.size;
|
||||||
|
log(`Pruning ${sha}...`);
|
||||||
|
|
||||||
|
await fs.promises.unlink(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`Pruned ${count} files (${(bytes / 1024 / 1024).toFixed(3)} MiB)`);
|
||||||
|
}
|
25
buildtools/tasks/misc/webhook.ts
Normal file
25
buildtools/tasks/misc/webhook.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { MessageBuilder, Webhook } from "discord-webhook-node";
|
||||||
|
import buildConfig from "../../buildConfig";
|
||||||
|
import { checkEnvironmentalVariables } from "../../util/util";
|
||||||
|
|
||||||
|
export default async function fireNightlyWebhook(): Promise<void> {
|
||||||
|
checkEnvironmentalVariables(["DISCORD_WEBHOOK", "GITHUB_RUN_ID", "GITHUB_SHA"]);
|
||||||
|
|
||||||
|
const webhook = new Webhook(process.env.DISCORD_WEBHOOK);
|
||||||
|
|
||||||
|
if (buildConfig.nightlyHookName) {
|
||||||
|
webhook.setUsername(buildConfig.nightlyHookName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buildConfig.nightlyHookAvatar) {
|
||||||
|
webhook.setAvatar(buildConfig.nightlyHookAvatar);
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
||||||
|
|
||||||
|
const embed = new MessageBuilder()
|
||||||
|
.setTitle(`New snapshot available! (**${process.env.GITHUB_SHA.substr(0, 7)}**)`)
|
||||||
|
.setDescription(`Click to [view on GitHub Actions](${link}).`);
|
||||||
|
|
||||||
|
return webhook.send(embed);
|
||||||
|
}
|
46
buildtools/tasks/misc/zip.ts
Normal file
46
buildtools/tasks/misc/zip.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import {
|
||||||
|
clientDestDirectory,
|
||||||
|
langDestDirectory,
|
||||||
|
mmcDestDirectory,
|
||||||
|
modpackManifest,
|
||||||
|
serverDestDirectory,
|
||||||
|
} from "../../globals";
|
||||||
|
import upath from "upath";
|
||||||
|
import zip from "gulp-zip";
|
||||||
|
import gulp from "gulp";
|
||||||
|
import buildConfig from "../../buildConfig";
|
||||||
|
import { makeArtifactNameBody } from "../../util/util";
|
||||||
|
import sanitize from "sanitize-filename";
|
||||||
|
|
||||||
|
async function zipFolder(path: string, zipName: string = upath.basename(path) + ".zip"): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
gulp
|
||||||
|
.src(upath.join(path, "**"), { nodir: true, base: path, dot: true })
|
||||||
|
.pipe(zip(zipName))
|
||||||
|
.pipe(gulp.dest(buildConfig.buildDestinationDirectory))
|
||||||
|
.on("end", resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeZipper(src: string, artifactName: string) {
|
||||||
|
const zipFn = () => {
|
||||||
|
return zipFolder(
|
||||||
|
upath.join(src),
|
||||||
|
sanitize((makeArtifactNameBody(modpackManifest.name) + `-${artifactName}.zip`).toLowerCase()),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.defineProperty(zipFn, "name", {
|
||||||
|
value: `zip${artifactName}`,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return zipFn;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const zipServer = makeZipper(serverDestDirectory, "Server");
|
||||||
|
export const zipClient = makeZipper(clientDestDirectory, "Client");
|
||||||
|
export const zipLang = makeZipper(langDestDirectory, "Lang");
|
||||||
|
export const zipMMC = makeZipper(mmcDestDirectory, "MMC");
|
||||||
|
|
||||||
|
export const zipAll = gulp.series(zipServer, zipClient, zipLang);
|
118
buildtools/tasks/mmc/index.ts
Normal file
118
buildtools/tasks/mmc/index.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import { clientDestDirectory, mmcDestDirectory, modpackManifest } from "../../globals";
|
||||||
|
import { fetchMods } from "../../util/curseForgeAPI";
|
||||||
|
import * as upath from "upath";
|
||||||
|
import { series, src, symlink } from "gulp";
|
||||||
|
import * as fs from "fs";
|
||||||
|
|
||||||
|
async function mmcCleanUp(cb) {
|
||||||
|
if (fs.existsSync(mmcDestDirectory)) {
|
||||||
|
await fs.promises.rm(mmcDestDirectory, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks and creates all necessary directories so we can build the client safely.
|
||||||
|
*/
|
||||||
|
async function createMMCDirs(cb) {
|
||||||
|
if (!fs.existsSync(mmcDestDirectory)) {
|
||||||
|
await fs.promises.mkdir(mmcDestDirectory, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies modpack overrides.
|
||||||
|
*/
|
||||||
|
function copyOverrides() {
|
||||||
|
return src(upath.join(clientDestDirectory, "**/*.*"), {
|
||||||
|
nodir: true,
|
||||||
|
resolveSymlinks: false,
|
||||||
|
}).pipe(symlink(upath.join(mmcDestDirectory)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies modpack overrides.
|
||||||
|
*/
|
||||||
|
async function renameOverrides() {
|
||||||
|
await fs.promises.rename(upath.join(mmcDestDirectory, "overrides"), upath.join(mmcDestDirectory, ".minecraft"));
|
||||||
|
return fs.promises.rm(upath.join(mmcDestDirectory, "manifest.json"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads client mods according to manifest.json and checks hashes.
|
||||||
|
*/
|
||||||
|
async function fetchModJars() {
|
||||||
|
return fetchMods(
|
||||||
|
modpackManifest.files.filter((f) => !f.sides || f.sides.includes("client")),
|
||||||
|
upath.join(mmcDestDirectory, ".minecraft"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createMMCConfig() {
|
||||||
|
const cfg = {
|
||||||
|
InstanceType: "OneSix",
|
||||||
|
iconKey: "default",
|
||||||
|
name: modpackManifest.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
return fs.promises.writeFile(
|
||||||
|
upath.join(mmcDestDirectory, "instance.cfg"),
|
||||||
|
Object.keys(cfg)
|
||||||
|
.map((key) => {
|
||||||
|
return `${key}=${cfg[key]}`;
|
||||||
|
})
|
||||||
|
.join("\n"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createMMCManifest() {
|
||||||
|
const manifest = {
|
||||||
|
components: [],
|
||||||
|
formatVersion: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
manifest.components.push({
|
||||||
|
cachedName: "Minecraft",
|
||||||
|
cachedVersion: modpackManifest.minecraft.version,
|
||||||
|
important: true,
|
||||||
|
uid: "net.minecraft",
|
||||||
|
version: modpackManifest.minecraft.version,
|
||||||
|
});
|
||||||
|
|
||||||
|
const forgeLoader = modpackManifest.minecraft.modLoaders
|
||||||
|
.map((x) => x.id.match(/forge-(.+)/))
|
||||||
|
.filter(Boolean)
|
||||||
|
.shift();
|
||||||
|
|
||||||
|
if (forgeLoader) {
|
||||||
|
const ver = forgeLoader[1];
|
||||||
|
|
||||||
|
manifest.components.push({
|
||||||
|
cachedName: "Forge",
|
||||||
|
cachedRequires: [
|
||||||
|
{
|
||||||
|
equals: `${modpackManifest.minecraft.version}`,
|
||||||
|
uid: "net.minecraft",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
cachedVersion: ver,
|
||||||
|
uid: "net.minecraftforge",
|
||||||
|
version: ver,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs.promises.writeFile(upath.join(mmcDestDirectory, "mmc-pack.json"), JSON.stringify(manifest, null, "\t"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default series(
|
||||||
|
mmcCleanUp,
|
||||||
|
createMMCDirs,
|
||||||
|
copyOverrides,
|
||||||
|
renameOverrides,
|
||||||
|
createMMCConfig,
|
||||||
|
createMMCManifest,
|
||||||
|
fetchModJars,
|
||||||
|
);
|
269
buildtools/tasks/server/index.ts
Normal file
269
buildtools/tasks/server/index.ts
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
import upath from "upath";
|
||||||
|
import unzip from "unzipper";
|
||||||
|
import through from "through2";
|
||||||
|
import mustache from "mustache";
|
||||||
|
import log from "fancy-log";
|
||||||
|
import gulp, { src, dest, symlink } from "gulp";
|
||||||
|
import fs from "fs";
|
||||||
|
import buildConfig from "../../buildConfig";
|
||||||
|
import Bluebird from "bluebird";
|
||||||
|
import { ForgeProfile } from "../../types/forgeProfile";
|
||||||
|
import { FileDef } from "../../types/fileDef";
|
||||||
|
import { downloadOrRetrieveFileDef, getVersionManifest, libraryToPath, relative } from "../../util/util";
|
||||||
|
import { modpackManifest, serverDestDirectory, sharedDestDirectory } from "../../globals";
|
||||||
|
import del from "del";
|
||||||
|
import { VersionManifest } from "../../types/versionManifest";
|
||||||
|
import { fetchMods } from "../../util/curseForgeAPI";
|
||||||
|
|
||||||
|
const FORGE_VERSION_REG = /forge-(.+)/;
|
||||||
|
const FORGE_MAVEN = "https://files.minecraftforge.net/maven/";
|
||||||
|
|
||||||
|
let g_forgeJar;
|
||||||
|
|
||||||
|
async function serverCleanUp() {
|
||||||
|
return del(upath.join(serverDestDirectory, "*"), { force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks and creates all necessary directories so we can build the client safely.
|
||||||
|
*/
|
||||||
|
async function createServerDirs() {
|
||||||
|
if (!fs.existsSync(serverDestDirectory)) {
|
||||||
|
return fs.promises.mkdir(serverDestDirectory, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download the Forge jar.
|
||||||
|
*
|
||||||
|
* Extract, parse the profile data and download required libraries.
|
||||||
|
*/
|
||||||
|
async function downloadForge() {
|
||||||
|
const minecraft = modpackManifest.minecraft;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Break down the Forge version defined in manifest.json.
|
||||||
|
*/
|
||||||
|
const parsedForgeEntry = FORGE_VERSION_REG.exec(
|
||||||
|
(minecraft.modLoaders.find((x) => x.id && x.id.indexOf("forge") != -1) || {}).id || "",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!parsedForgeEntry) {
|
||||||
|
throw new Error("Malformed Forge version in manifest.json.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform Forge version into Maven library path.
|
||||||
|
*/
|
||||||
|
const forgeMavenLibrary = `net.minecraftforge:forge:${minecraft.version}-${parsedForgeEntry[1]}`;
|
||||||
|
const forgeInstallerPath = libraryToPath(forgeMavenLibrary) + "-installer.jar";
|
||||||
|
const forgeUniversalPath = upath.join("maven", libraryToPath(forgeMavenLibrary) + ".jar");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the Forge installer
|
||||||
|
*/
|
||||||
|
const forgeJar = await fs.promises.readFile(
|
||||||
|
(
|
||||||
|
await downloadOrRetrieveFileDef({
|
||||||
|
url: FORGE_MAVEN + forgeInstallerPath,
|
||||||
|
})
|
||||||
|
).cachePath,
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the profile manifest.
|
||||||
|
*/
|
||||||
|
let forgeUniversalJar: Buffer, forgeProfile: ForgeProfile;
|
||||||
|
const files = (await unzip.Open.buffer(forgeJar))?.files;
|
||||||
|
|
||||||
|
log("Extracting Forge installation profile & jar...");
|
||||||
|
|
||||||
|
if (!files) {
|
||||||
|
throw new Error("Malformed Forge installation jar.");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
// Look for the universal jar.
|
||||||
|
if (!forgeUniversalJar && file.path == forgeUniversalPath) {
|
||||||
|
forgeUniversalJar = await file.buffer();
|
||||||
|
}
|
||||||
|
// Look for the installation profile.
|
||||||
|
else if (!forgeProfile && file.path == "version.json") {
|
||||||
|
forgeProfile = JSON.parse((await file.buffer()).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forgeUniversalJar && forgeProfile) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!forgeProfile || !forgeProfile.libraries) {
|
||||||
|
throw new Error("Malformed Forge installation profile.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!forgeUniversalJar) {
|
||||||
|
throw new Error("Couldn't find the universal Forge jar in the installation jar.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the universal jar into the dist folder.
|
||||||
|
*/
|
||||||
|
log("Extracting the Forge jar...");
|
||||||
|
await fs.promises.writeFile(upath.join(serverDestDirectory, upath.basename(forgeUniversalPath)), forgeUniversalJar);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the universal jar file name for later.
|
||||||
|
*
|
||||||
|
* We will need it to process launchscripts.
|
||||||
|
*/
|
||||||
|
g_forgeJar = upath.basename(forgeUniversalPath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finally, fetch libraries.
|
||||||
|
*/
|
||||||
|
const libraries = forgeProfile.libraries.filter((x) => Boolean(x?.downloads?.artifact?.url));
|
||||||
|
log(`Fetching ${libraries.length} server libraries...`);
|
||||||
|
|
||||||
|
return Bluebird.map(
|
||||||
|
libraries,
|
||||||
|
async (library) => {
|
||||||
|
const libraryPath = library.downloads.artifact.path;
|
||||||
|
|
||||||
|
const def: FileDef = {
|
||||||
|
url: library.downloads.artifact.url,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (library.downloads.artifact.sha1) {
|
||||||
|
def.hashes = [{ id: "sha1", hashes: [library.downloads.artifact.sha1] }];
|
||||||
|
}
|
||||||
|
|
||||||
|
const destPath = upath.join(serverDestDirectory, "libraries", libraryPath);
|
||||||
|
|
||||||
|
await fs.promises.mkdir(upath.dirname(destPath), { recursive: true });
|
||||||
|
await fs.promises.symlink(relative(destPath, (await downloadOrRetrieveFileDef(def)).cachePath), destPath);
|
||||||
|
},
|
||||||
|
{ concurrency: buildConfig.downloaderConcurrency },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download the server jar.
|
||||||
|
*/
|
||||||
|
async function downloadMinecraftServer() {
|
||||||
|
log("Fetching the Minecraft version manifest...");
|
||||||
|
const versionManifest: VersionManifest = await getVersionManifest(modpackManifest.minecraft.version);
|
||||||
|
if (!versionManifest) {
|
||||||
|
throw new Error(`No manifest found for Minecraft ${versionManifest.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the server jar file.
|
||||||
|
*
|
||||||
|
* Pass SHA1 hash to compare against the downloaded file.
|
||||||
|
*/
|
||||||
|
const serverJar = await downloadOrRetrieveFileDef({
|
||||||
|
url: versionManifest.downloads.server.url,
|
||||||
|
hashes: [{ id: "sha1", hashes: versionManifest.downloads.server.sha1 }],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!(versionManifest.downloads && versionManifest.downloads.server)) {
|
||||||
|
throw new Error(`No server jar file found for ${versionManifest.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dest = upath.join(serverDestDirectory, `minecraft_server.${versionManifest.id}.jar`);
|
||||||
|
await fs.promises.symlink(relative(dest, serverJar.cachePath), dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads mods according to manifest.json and checks hashes.
|
||||||
|
*/
|
||||||
|
async function downloadMods() {
|
||||||
|
return fetchMods(
|
||||||
|
modpackManifest.files.filter((f) => !f.sides || f.sides.includes("server")),
|
||||||
|
serverDestDirectory,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies modpack overrides.
|
||||||
|
*/
|
||||||
|
function copyServerOverrides() {
|
||||||
|
return gulp
|
||||||
|
.src(buildConfig.copyFromSharedServerGlobs, { nodir: true, cwd: sharedDestDirectory, allowEmpty: true })
|
||||||
|
.pipe(symlink(upath.join(serverDestDirectory)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies files from ./serverfiles into dest folder.
|
||||||
|
*/
|
||||||
|
function copyServerfiles() {
|
||||||
|
return src(["../serverfiles/**"]).pipe(dest(serverDestDirectory));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies the license file.
|
||||||
|
*/
|
||||||
|
function copyServerLicense() {
|
||||||
|
return src("../LICENSE").pipe(dest(serverDestDirectory));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies the update notes file.
|
||||||
|
*/
|
||||||
|
function copyServerUpdateNotes() {
|
||||||
|
return src("../UPDATENOTES.md", { allowEmpty: true }).pipe(dest(serverDestDirectory));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies the changelog file.
|
||||||
|
*/
|
||||||
|
function copyServerChangelog() {
|
||||||
|
return src(upath.join(sharedDestDirectory, "CHANGELOG.md")).pipe(dest(serverDestDirectory));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies files from ./launchscripts into dest folder and processes them using mustache.
|
||||||
|
*
|
||||||
|
* Replaces jvmArgs, minRAM, maxRAM and forgeJar.
|
||||||
|
*/
|
||||||
|
function processLaunchscripts() {
|
||||||
|
const rules = {
|
||||||
|
jvmArgs: buildConfig.launchscriptsJVMArgs,
|
||||||
|
minRAM: buildConfig.launchscriptsMinRAM,
|
||||||
|
maxRAM: buildConfig.launchscriptsMaxRAM,
|
||||||
|
forgeJar: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (g_forgeJar) {
|
||||||
|
rules.forgeJar = g_forgeJar;
|
||||||
|
} else {
|
||||||
|
log.warn("No forgeJar specified!");
|
||||||
|
log.warn("Did downloadForge task fail?");
|
||||||
|
}
|
||||||
|
|
||||||
|
return src(["../launchscripts/**"])
|
||||||
|
.pipe(
|
||||||
|
through.obj((file, _, callback) => {
|
||||||
|
if (file.isBuffer()) {
|
||||||
|
const rendered = mustache.render(file.contents.toString(), rules);
|
||||||
|
file.contents = Buffer.from(rendered);
|
||||||
|
}
|
||||||
|
callback(null, file);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.pipe(dest(serverDestDirectory));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default gulp.series([
|
||||||
|
serverCleanUp,
|
||||||
|
createServerDirs,
|
||||||
|
downloadForge,
|
||||||
|
downloadMinecraftServer,
|
||||||
|
downloadMods,
|
||||||
|
copyServerOverrides,
|
||||||
|
copyServerfiles,
|
||||||
|
copyServerLicense,
|
||||||
|
copyServerChangelog,
|
||||||
|
copyServerUpdateNotes,
|
||||||
|
processLaunchscripts,
|
||||||
|
]);
|
177
buildtools/tasks/shared/index.ts
Normal file
177
buildtools/tasks/shared/index.ts
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import gulp from "gulp";
|
||||||
|
import upath from "upath";
|
||||||
|
import buildConfig from "../../buildConfig";
|
||||||
|
import { modpackManifest, overridesFolder, sharedDestDirectory, tempDirectory } from "../../globals";
|
||||||
|
import del from "del";
|
||||||
|
import { FileDef } from "../../types/fileDef";
|
||||||
|
import Bluebird from "bluebird";
|
||||||
|
import {
|
||||||
|
compareAndExpandManifestDependencies,
|
||||||
|
downloadOrRetrieveFileDef,
|
||||||
|
getChangeLog,
|
||||||
|
getFileAtRevision,
|
||||||
|
getLastGitTag,
|
||||||
|
relative,
|
||||||
|
} from "../../util/util";
|
||||||
|
|
||||||
|
async function sharedCleanUp() {
|
||||||
|
await del(upath.join(sharedDestDirectory, "*"), { force: true });
|
||||||
|
await del(upath.join(tempDirectory, "*"), { force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks and creates all necessary directories so we can build everything safely.
|
||||||
|
*/
|
||||||
|
async function createSharedDirs() {
|
||||||
|
if (!fs.existsSync(sharedDestDirectory)) {
|
||||||
|
await fs.promises.mkdir(sharedDestDirectory, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(tempDirectory)) {
|
||||||
|
await fs.promises.mkdir(tempDirectory, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies modpack overrides.
|
||||||
|
*/
|
||||||
|
async function copyOverrides() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
gulp
|
||||||
|
.src(upath.join(buildConfig.buildSourceDirectory, overridesFolder, "**/*"))
|
||||||
|
.pipe(gulp.dest(upath.join(sharedDestDirectory, overridesFolder)))
|
||||||
|
.on("end", resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch external dependencies and remove the field from the manifest file.
|
||||||
|
*/
|
||||||
|
async function fetchExternalDependencies() {
|
||||||
|
const dependencies = modpackManifest.externalDependencies;
|
||||||
|
if (dependencies) {
|
||||||
|
const destDirectory = upath.join(sharedDestDirectory, overridesFolder, "mods");
|
||||||
|
|
||||||
|
if (!fs.existsSync(destDirectory)) {
|
||||||
|
await fs.promises.mkdir(destDirectory, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map dependencies to FileDefs.
|
||||||
|
const depDefs: FileDef[] = dependencies.map((dep) => {
|
||||||
|
return {
|
||||||
|
url: dep.url,
|
||||||
|
hashes: [
|
||||||
|
{
|
||||||
|
hashes: [dep.sha],
|
||||||
|
id: "sha1",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
delete modpackManifest.externalDependencies;
|
||||||
|
|
||||||
|
return Bluebird.map(
|
||||||
|
depDefs,
|
||||||
|
async (depDef) => {
|
||||||
|
const dest = upath.join(destDirectory, upath.basename(depDef.url));
|
||||||
|
const cachePath = (await downloadOrRetrieveFileDef(depDef)).cachePath;
|
||||||
|
|
||||||
|
const rel = relative(dest, cachePath);
|
||||||
|
|
||||||
|
await fs.promises.symlink(rel, dest);
|
||||||
|
},
|
||||||
|
{ concurrency: buildConfig.downloaderConcurrency },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a changelog based on environmental variables.
|
||||||
|
*/
|
||||||
|
async function makeChangelog() {
|
||||||
|
let since = getLastGitTag(),
|
||||||
|
to: string;
|
||||||
|
|
||||||
|
// If this is a tagged build, fetch the tag before last.
|
||||||
|
if (process.env.GITHUB_TAG) {
|
||||||
|
since = getLastGitTag(process.env.GITHUB_TAG);
|
||||||
|
to = process.env.GITHUB_TAG;
|
||||||
|
}
|
||||||
|
// Back-compat in case this crap is still around.
|
||||||
|
else if (since == "latest-dev-preview") {
|
||||||
|
since = getLastGitTag(since);
|
||||||
|
}
|
||||||
|
|
||||||
|
const old = JSON.parse(getFileAtRevision("manifest.json", since)) as ModpackManifest;
|
||||||
|
const current = modpackManifest;
|
||||||
|
const commitList = getChangeLog(since, to, [upath.join("..", modpackManifest.overrides)]);
|
||||||
|
|
||||||
|
const builder: string[] = [];
|
||||||
|
// If the UPDATENOTES.md file is present, prepend it verbatim.
|
||||||
|
if (fs.existsSync("../UPDATENOTES.md")) {
|
||||||
|
builder.push((await fs.promises.readFile("../UPDATENOTES.md")).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push the title.
|
||||||
|
builder.push(`# Changes since ${since}`);
|
||||||
|
|
||||||
|
const comparisonResult = await compareAndExpandManifestDependencies(old, current);
|
||||||
|
|
||||||
|
// Push mod update blocks.
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "## New mods",
|
||||||
|
list: comparisonResult.added,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "## Updated mods",
|
||||||
|
list: comparisonResult.modified,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "## Removed mods",
|
||||||
|
list: comparisonResult.removed,
|
||||||
|
},
|
||||||
|
].forEach((block) => {
|
||||||
|
if (block.list.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.push("");
|
||||||
|
builder.push(block.name);
|
||||||
|
builder.push(
|
||||||
|
...block.list
|
||||||
|
// Yeet invalid project names.
|
||||||
|
.filter((project) => !/project-\d*/.test(project))
|
||||||
|
.sort()
|
||||||
|
.map((name) => `* ${name}`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Push the changelog itself.
|
||||||
|
if (commitList) {
|
||||||
|
builder.push("");
|
||||||
|
builder.push("## Commits");
|
||||||
|
builder.push(commitList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the builder only contains the title.
|
||||||
|
if (builder.length == 1) {
|
||||||
|
builder.push("");
|
||||||
|
builder.push("There haven't been any changes.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs.promises.writeFile(upath.join(sharedDestDirectory, "CHANGELOG.md"), builder.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
import transforms from "./transforms";
|
||||||
|
import { ModpackManifest } from "../../types/modpackManifest";
|
||||||
|
export default gulp.series(
|
||||||
|
sharedCleanUp,
|
||||||
|
createSharedDirs,
|
||||||
|
copyOverrides,
|
||||||
|
makeChangelog,
|
||||||
|
fetchExternalDependencies,
|
||||||
|
...transforms,
|
||||||
|
);
|
5
buildtools/tasks/shared/transforms/index.ts
Normal file
5
buildtools/tasks/shared/transforms/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import quest from "./quest_i18n";
|
||||||
|
import scannable from "./scannable";
|
||||||
|
import version from "./version";
|
||||||
|
|
||||||
|
export default [quest, scannable, version];
|
123
buildtools/tasks/shared/transforms/quest_i18n.ts
Normal file
123
buildtools/tasks/shared/transforms/quest_i18n.ts
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import upath from "upath";
|
||||||
|
import { overridesFolder, sharedDestDirectory } from "../../../globals";
|
||||||
|
import { Quest, QuestBook, QuestLines as QuestLine } from "../../../types/bqQuestBook";
|
||||||
|
|
||||||
|
const questLocation = "config/betterquesting/DefaultQuests.json";
|
||||||
|
const langFileLocation = "resources/questbook/lang";
|
||||||
|
|
||||||
|
function escapeString(string: string) {
|
||||||
|
return string.replace(/%/g, "%%").replace(/\n/g, "%n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformKeyPairs(
|
||||||
|
database: { [key: string]: Quest } | { [key: string]: QuestLine },
|
||||||
|
namespace: string,
|
||||||
|
lines: string[],
|
||||||
|
) {
|
||||||
|
Object.keys(database).forEach((key) => {
|
||||||
|
const storeKey = key.replace(/:10/g, "");
|
||||||
|
const item = database[key];
|
||||||
|
|
||||||
|
const properties = item["properties:10"]["betterquesting:10"];
|
||||||
|
if (properties["name:8"] !== "Gap") {
|
||||||
|
const titleKey = `nomifactory.quest.${namespace}.${storeKey}.title`;
|
||||||
|
const descKey = `nomifactory.quest.${namespace}.${storeKey}.desc`;
|
||||||
|
|
||||||
|
// Push lang file lines.
|
||||||
|
lines.push(
|
||||||
|
`# ${namespace} ${storeKey}`,
|
||||||
|
`${titleKey}=${escapeString(properties["name:8"])}`,
|
||||||
|
`${descKey}=${escapeString(properties["desc:8"])}`,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
properties["name:8"] = titleKey;
|
||||||
|
properties["desc:8"] = descKey;
|
||||||
|
} else {
|
||||||
|
properties["name:8"] = "";
|
||||||
|
properties["desc:8"] = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trimming all that results in almost half the size of the original JSON file.
|
||||||
|
*
|
||||||
|
* Interesting, huh?
|
||||||
|
*/
|
||||||
|
const uselessProps = {
|
||||||
|
"simultaneous:1": 0,
|
||||||
|
"ismain:1": 0,
|
||||||
|
"repeat_relative:1": 1,
|
||||||
|
"globalshare:1": 0,
|
||||||
|
"repeattime:3": -1,
|
||||||
|
"issilent:1": 0,
|
||||||
|
"snd_complete:8": "minecraft:entity.player.levelup",
|
||||||
|
"snd_update:8": "minecraft:entity.player.levelup",
|
||||||
|
"tasklogic:8": "AND",
|
||||||
|
"questlogic:8": "AND",
|
||||||
|
"visibility:8": "NORMAL",
|
||||||
|
"partysinglereward:1": 0,
|
||||||
|
"lockedprogress:1": 0,
|
||||||
|
"OreDict:8": "",
|
||||||
|
"Damage:2": 0,
|
||||||
|
"Count:3": 0,
|
||||||
|
"autoclaim:1": 0,
|
||||||
|
"autoConsume:1": 0,
|
||||||
|
"consume:1": 0,
|
||||||
|
"groupDetect:1": 0,
|
||||||
|
"ignoreNBT:1": 0,
|
||||||
|
"index:3": 0,
|
||||||
|
"partialMatch:1": 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
function stripUselessMetadata(object: unknown) {
|
||||||
|
Object.keys(object).forEach((propName) => {
|
||||||
|
const prop = object[propName];
|
||||||
|
if (prop === uselessProps[propName]) {
|
||||||
|
return delete object[propName];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof prop === "object") {
|
||||||
|
if (Array.isArray(prop) && prop.length === 0) {
|
||||||
|
return delete object[propName];
|
||||||
|
}
|
||||||
|
|
||||||
|
stripUselessMetadata(prop);
|
||||||
|
|
||||||
|
if (Object.keys(prop).length === 0) {
|
||||||
|
return delete object[propName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract lang entries from the quest book and transform the database.
|
||||||
|
*/
|
||||||
|
export default async function transformQuestBook(): Promise<void> {
|
||||||
|
const questDatabasePath = upath.join(sharedDestDirectory, overridesFolder, questLocation);
|
||||||
|
const questLangLocation = upath.join(sharedDestDirectory, overridesFolder, langFileLocation);
|
||||||
|
|
||||||
|
const questBook: QuestBook = JSON.parse((await fs.promises.readFile(questDatabasePath)).toString());
|
||||||
|
|
||||||
|
// Traverse through the quest book and rewrite titles/descriptions.
|
||||||
|
// Extract title/desc pairs into a lang file.
|
||||||
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
// Quest lines.
|
||||||
|
transformKeyPairs(questBook["questLines:9"], "line", lines);
|
||||||
|
|
||||||
|
// Quests themselves.
|
||||||
|
transformKeyPairs(questBook["questDatabase:9"], "db", lines);
|
||||||
|
|
||||||
|
// Write lang file.
|
||||||
|
await fs.promises.mkdir(questLangLocation, { recursive: true });
|
||||||
|
await fs.promises.writeFile(upath.join(questLangLocation, "en_us.lang"), lines.join("\n"));
|
||||||
|
|
||||||
|
// Strip useless metadata.
|
||||||
|
stripUselessMetadata(questBook);
|
||||||
|
|
||||||
|
return fs.promises.writeFile(questDatabasePath, JSON.stringify(questBook));
|
||||||
|
}
|
28
buildtools/tasks/shared/transforms/scannable.ts
Normal file
28
buildtools/tasks/shared/transforms/scannable.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { overridesFolder, sharedDestDirectory } from "../../../globals";
|
||||||
|
|
||||||
|
import upath from "upath";
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
const scannableConfigFile = "config/scannable.cfg";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform the scannable config.
|
||||||
|
* Trim excess newlines and remove comments.
|
||||||
|
*/
|
||||||
|
export default async function transformScannable(): Promise<void> {
|
||||||
|
const configPath = upath.join(sharedDestDirectory, overridesFolder, scannableConfigFile);
|
||||||
|
|
||||||
|
const contents = (await fs.promises.readFile(configPath))
|
||||||
|
.toString()
|
||||||
|
// Match arrays (S:array < ... >)
|
||||||
|
.replace(/([ \t]+)(S:\w+) <([^>]+)>/g, (_, g0, g1, g2) => {
|
||||||
|
const body = g2
|
||||||
|
.replace(/#[^\r\n]+/gm, "") // Comments
|
||||||
|
.replace(/^\s+$/gm, "") // Trailing whitespaces
|
||||||
|
.replace(/[\r\n]{2,}/gm, "\n"); // Extra newlines
|
||||||
|
|
||||||
|
return g0 + g1 + " <" + body + (body ? "" : "\n") + g0 + " >";
|
||||||
|
});
|
||||||
|
|
||||||
|
return fs.promises.writeFile(configPath, contents);
|
||||||
|
}
|
49
buildtools/tasks/shared/transforms/version.ts
Normal file
49
buildtools/tasks/shared/transforms/version.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import upath from "path";
|
||||||
|
import mustache from "mustache";
|
||||||
|
import { modpackManifest, overridesFolder, sharedDestDirectory } from "../../../globals";
|
||||||
|
|
||||||
|
const randomPatchesConfigFile = "config/randompatches.cfg";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform the version field of manifest.json.
|
||||||
|
*/
|
||||||
|
export default async function transformManifestVersion(): Promise<void> {
|
||||||
|
let versionTitle;
|
||||||
|
if (process.env.GITHUB_TAG) {
|
||||||
|
const flavorTitle = process.env.BUILD_FLAVOR_TITLE;
|
||||||
|
const tag = process.env.GITHUB_TAG.replace(/^v/, "");
|
||||||
|
|
||||||
|
versionTitle = [modpackManifest.name, tag, flavorTitle].filter(Boolean).join(" - ");
|
||||||
|
|
||||||
|
modpackManifest.version = tag;
|
||||||
|
}
|
||||||
|
// If SHA is provided and the build isn't tagged, append both the branch and short SHA.
|
||||||
|
else if (process.env.GITHUB_SHA && process.env.GITHUB_REF && process.env.GITHUB_REF.startsWith("refs/heads/")) {
|
||||||
|
const shortCommit = process.env.GITHUB_SHA.substr(0, 7);
|
||||||
|
const branch = /refs\/heads\/(.+)/.exec(process.env.GITHUB_REF)?.[1];
|
||||||
|
if (!branch) {
|
||||||
|
throw new Error(`Invalid git ref: ${process.env.GITHUB_REF}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
versionTitle = `${modpackManifest.name} (${branch} branch, ${shortCommit})`;
|
||||||
|
|
||||||
|
modpackManifest.version = `${branch}-${shortCommit}`;
|
||||||
|
} else {
|
||||||
|
versionTitle = `${modpackManifest.name} (manual build)`;
|
||||||
|
|
||||||
|
modpackManifest.version = "manual-build";
|
||||||
|
}
|
||||||
|
|
||||||
|
modpackManifest.name = versionTitle;
|
||||||
|
|
||||||
|
const randomPatchesConfigFilePath = upath.join(sharedDestDirectory, overridesFolder, randomPatchesConfigFile);
|
||||||
|
const randomPatchesFile = (await fs.promises.readFile(randomPatchesConfigFilePath)).toString();
|
||||||
|
|
||||||
|
return fs.promises.writeFile(
|
||||||
|
randomPatchesConfigFilePath,
|
||||||
|
mustache.render(randomPatchesFile, {
|
||||||
|
title: versionTitle,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
10
buildtools/tsconfig.json
Normal file
10
buildtools/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"sourceMap": true,
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es5",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"downlevelIteration": true,
|
||||||
|
}
|
||||||
|
}
|
118
buildtools/types/bqQuestBook.ts
Normal file
118
buildtools/types/bqQuestBook.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
export interface QuestBook {
|
||||||
|
"format:8": string;
|
||||||
|
"questDatabase:9": { [key: string]: Quest };
|
||||||
|
"questLines:9": { [key: string]: QuestLines };
|
||||||
|
"questSettings:10": QuestSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Quest {
|
||||||
|
"preRequisites:11": number[];
|
||||||
|
"properties:10": QuestProperties;
|
||||||
|
"questID:3": number;
|
||||||
|
"rewards:9": Rewards9;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuestProperties {
|
||||||
|
"betterquesting:10": QuestPropertiesBQ;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuestPropertiesBQ {
|
||||||
|
"autoclaim:1": number;
|
||||||
|
"desc:8": string;
|
||||||
|
"globalshare:1": number;
|
||||||
|
"icon:10": Icon;
|
||||||
|
"ismain:1": number;
|
||||||
|
"issilent:1": number;
|
||||||
|
"lockedprogress:1": number;
|
||||||
|
"name:8": string;
|
||||||
|
"partysinglereward:1"?: number;
|
||||||
|
"questlogic:8": QuestLogic;
|
||||||
|
"repeat_relative:1": number;
|
||||||
|
"repeattime:3": number;
|
||||||
|
"simultaneous:1": number;
|
||||||
|
"snd_complete:8": string;
|
||||||
|
"snd_update:8": string;
|
||||||
|
"tasklogic:8": QuestLogic;
|
||||||
|
"visibility:8": QuestVisibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Icon {
|
||||||
|
"Count:3": number;
|
||||||
|
"Damage:2": number;
|
||||||
|
"OreDict:8": string;
|
||||||
|
"id:8": string;
|
||||||
|
"tag:10"?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum QuestLogic {
|
||||||
|
And = "AND",
|
||||||
|
Or = "OR",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum QuestVisibility {
|
||||||
|
Always = "ALWAYS",
|
||||||
|
Chain = "CHAIN",
|
||||||
|
Hidden = "HIDDEN",
|
||||||
|
Normal = "NORMAL",
|
||||||
|
Unlocked = "UNLOCKED",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Rewards9 {
|
||||||
|
"0:10"?: Rewards9_010;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Rewards9_010 {
|
||||||
|
"index:3": number;
|
||||||
|
"rewardID:8": RewardID8;
|
||||||
|
"rewards:9"?: { [key: string]: Icon };
|
||||||
|
"choices:9"?: { [key: string]: Icon };
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum RewardID8 {
|
||||||
|
BqStandardChoice = "bq_standard:choice",
|
||||||
|
BqStandardItem = "bq_standard:item",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TaskID8 {
|
||||||
|
BqStandardCheckbox = "bq_standard:checkbox",
|
||||||
|
BqStandardRetrieval = "bq_standard:retrieval",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuestLines {
|
||||||
|
"lineID:3": number;
|
||||||
|
"order:3": number;
|
||||||
|
"properties:10": QuestLines9_Properties10;
|
||||||
|
"quests:9": { [key: string]: { [key: string]: number } };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuestLines9_Properties10 {
|
||||||
|
"betterquesting:10": FluffyBetterquesting10;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FluffyBetterquesting10 {
|
||||||
|
"bg_image:8": string;
|
||||||
|
"bg_size:3": number;
|
||||||
|
"desc:8": string;
|
||||||
|
"icon:10": Icon;
|
||||||
|
"name:8": string;
|
||||||
|
"visibility:8": QuestVisibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuestSettings {
|
||||||
|
"betterquesting:10": QuestSettings10_Betterquesting10;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuestSettings10_Betterquesting10 {
|
||||||
|
"editmode:1": number;
|
||||||
|
"hardcore:1": number;
|
||||||
|
"home_anchor_x:5": number;
|
||||||
|
"home_anchor_y:5": number;
|
||||||
|
"home_image:8": string;
|
||||||
|
"home_offset_x:3": number;
|
||||||
|
"home_offset_y:3": number;
|
||||||
|
"livesdef:3": number;
|
||||||
|
"livesmax:3": number;
|
||||||
|
"pack_name:8": string;
|
||||||
|
"pack_version:3": number;
|
||||||
|
"party_enable:1": number;
|
||||||
|
}
|
160
buildtools/types/curseForge.ts
Normal file
160
buildtools/types/curseForge.ts
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
interface Author {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
projectId: number;
|
||||||
|
id: number;
|
||||||
|
projectTitleId?: number;
|
||||||
|
projectTitleTitle: string;
|
||||||
|
userId: number;
|
||||||
|
twitchId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Attachment {
|
||||||
|
id: number;
|
||||||
|
projectId: number;
|
||||||
|
description: string;
|
||||||
|
isDefault: boolean;
|
||||||
|
thumbnailUrl: string;
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
status: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Dependency {
|
||||||
|
id: number;
|
||||||
|
addonId: number;
|
||||||
|
type: number;
|
||||||
|
fileId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Module {
|
||||||
|
foldername: string;
|
||||||
|
type: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SortableGameVersion {
|
||||||
|
gameVersionPadded: string;
|
||||||
|
gameVersion: string;
|
||||||
|
gameVersionReleaseDate: Date;
|
||||||
|
gameVersionName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CurseForgeFileInfo {
|
||||||
|
id: number;
|
||||||
|
displayName: string;
|
||||||
|
fileName: string;
|
||||||
|
fileDate: Date;
|
||||||
|
fileLength: number;
|
||||||
|
releaseType: number;
|
||||||
|
fileStatus: number;
|
||||||
|
downloadUrl: string;
|
||||||
|
isAlternate: boolean;
|
||||||
|
alternateFileId: number;
|
||||||
|
dependencies: Dependency[];
|
||||||
|
isAvailable: boolean;
|
||||||
|
modules: Module[];
|
||||||
|
gameVersion: string[];
|
||||||
|
sortableGameVersion: SortableGameVersion[];
|
||||||
|
hasInstallScript: boolean;
|
||||||
|
isCompatibleWithClient: boolean;
|
||||||
|
categorySectionPackageType: number;
|
||||||
|
restrictProjectFileAccess: number;
|
||||||
|
projectStatus: number;
|
||||||
|
renderCacheId: number;
|
||||||
|
packageFingerprintId: number;
|
||||||
|
gameVersionDateReleased: Date;
|
||||||
|
gameVersionMappingId: number;
|
||||||
|
gameVersionId: number;
|
||||||
|
gameId: number;
|
||||||
|
isServerPack: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Category {
|
||||||
|
categoryId: number;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
parentId: number;
|
||||||
|
rootId: number;
|
||||||
|
projectId: number;
|
||||||
|
avatarId: number;
|
||||||
|
gameId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CategorySection {
|
||||||
|
id: number;
|
||||||
|
gameId: number;
|
||||||
|
name: string;
|
||||||
|
packageType: number;
|
||||||
|
path: string;
|
||||||
|
initialInclusionPattern: string;
|
||||||
|
gameCategoryId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GameVersionLatestFile {
|
||||||
|
gameVersion: string;
|
||||||
|
projectFileId: number;
|
||||||
|
projectFileName: string;
|
||||||
|
fileType: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CurseForgeModInfo {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
authors: Author[];
|
||||||
|
attachments: Attachment[];
|
||||||
|
websiteUrl: string;
|
||||||
|
gameId: number;
|
||||||
|
summary: string;
|
||||||
|
defaultFileId: number;
|
||||||
|
downloadCount: number;
|
||||||
|
latestFiles: CurseForgeFileInfo[];
|
||||||
|
categories: Category[];
|
||||||
|
status: number;
|
||||||
|
primaryCategoryId: number;
|
||||||
|
categorySection: CategorySection;
|
||||||
|
slug: string;
|
||||||
|
gameVersionLatestFiles: GameVersionLatestFile[];
|
||||||
|
isFeatured: boolean;
|
||||||
|
popularityScore: number;
|
||||||
|
gamePopularityRank: number;
|
||||||
|
primaryLanguage: string;
|
||||||
|
gameSlug: string;
|
||||||
|
gameName: string;
|
||||||
|
portalName: string;
|
||||||
|
dateModified: Date;
|
||||||
|
dateCreated: Date;
|
||||||
|
dateReleased: Date;
|
||||||
|
isAvailable: boolean;
|
||||||
|
isExperiemental: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Dependency {
|
||||||
|
addonId: number;
|
||||||
|
type: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Module {
|
||||||
|
foldername: string;
|
||||||
|
fingerprint: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CurseForgeFetchedFileInfo {
|
||||||
|
id: number;
|
||||||
|
displayName: string;
|
||||||
|
fileName: string;
|
||||||
|
fileDate: Date;
|
||||||
|
fileLength: number;
|
||||||
|
releaseType: number;
|
||||||
|
fileStatus: number;
|
||||||
|
downloadUrl: string;
|
||||||
|
isAlternate: boolean;
|
||||||
|
alternateFileId: number;
|
||||||
|
dependencies: Dependency[];
|
||||||
|
isAvailable: boolean;
|
||||||
|
modules: Module[];
|
||||||
|
packageFingerprint: number;
|
||||||
|
gameVersion: string[];
|
||||||
|
hasInstallScript: boolean;
|
||||||
|
gameVersionDateReleased: Date;
|
||||||
|
}
|
6
buildtools/types/fileDef.ts
Normal file
6
buildtools/types/fileDef.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { HashDef } from "./hashDef";
|
||||||
|
|
||||||
|
export type FileDef = {
|
||||||
|
url: string;
|
||||||
|
hashes?: HashDef[];
|
||||||
|
};
|
27
buildtools/types/forgeProfile.ts
Normal file
27
buildtools/types/forgeProfile.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
interface Artifact {
|
||||||
|
path: string;
|
||||||
|
url: string;
|
||||||
|
sha1: string;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Downloads {
|
||||||
|
artifact: Artifact;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Library {
|
||||||
|
name: string;
|
||||||
|
downloads: Downloads;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ForgeProfile {
|
||||||
|
_comment_: string[];
|
||||||
|
id: string;
|
||||||
|
time: Date;
|
||||||
|
releaseTime: Date;
|
||||||
|
type: string;
|
||||||
|
mainClass: string;
|
||||||
|
inheritsFrom: string;
|
||||||
|
minecraftArguments: string;
|
||||||
|
libraries: Library[];
|
||||||
|
}
|
4
buildtools/types/hashDef.ts
Normal file
4
buildtools/types/hashDef.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export type HashDef = {
|
||||||
|
id: string;
|
||||||
|
hashes: unknown | Array<unknown>;
|
||||||
|
};
|
19
buildtools/types/mainMenuConfig.ts
Normal file
19
buildtools/types/mainMenuConfig.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// Bare minimum.
|
||||||
|
interface Slideshow {
|
||||||
|
displayDuration: number;
|
||||||
|
fadeDuration: number;
|
||||||
|
images: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Background {
|
||||||
|
image: string;
|
||||||
|
slideshow: Slideshow;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Other {
|
||||||
|
background: Background;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MainMenuConfig {
|
||||||
|
other: Other;
|
||||||
|
}
|
35
buildtools/types/modpackManifest.ts
Normal file
35
buildtools/types/modpackManifest.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
interface ModLoader {
|
||||||
|
id: string;
|
||||||
|
primary: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Minecraft {
|
||||||
|
version: string;
|
||||||
|
modLoaders?: ModLoader[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExternalDependency {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
sha: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModpackManifestFile {
|
||||||
|
projectID: number;
|
||||||
|
fileID: number;
|
||||||
|
required: boolean;
|
||||||
|
sides?: ("client" | "server")[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModpackManifest {
|
||||||
|
minecraft: Minecraft;
|
||||||
|
manifestType: string;
|
||||||
|
manifestVersion: number;
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
author: string;
|
||||||
|
projectID: number;
|
||||||
|
externalDependencies?: ExternalDependency[];
|
||||||
|
files: ModpackManifestFile[];
|
||||||
|
overrides: string;
|
||||||
|
}
|
120
buildtools/types/versionManifest.ts
Normal file
120
buildtools/types/versionManifest.ts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
interface AssetIndex {
|
||||||
|
id: string;
|
||||||
|
sha1: string;
|
||||||
|
size: number;
|
||||||
|
totalSize: number;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Client {
|
||||||
|
sha1: string;
|
||||||
|
size: number;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Server {
|
||||||
|
sha1: string;
|
||||||
|
size: number;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Downloads {
|
||||||
|
client: Client;
|
||||||
|
server: Server;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Artifact {
|
||||||
|
path: string;
|
||||||
|
sha1: string;
|
||||||
|
size: number;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NativesLinux {
|
||||||
|
path: string;
|
||||||
|
sha1: string;
|
||||||
|
size: number;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NativesOsx {
|
||||||
|
path: string;
|
||||||
|
sha1: string;
|
||||||
|
size: number;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NativesWindows {
|
||||||
|
path: string;
|
||||||
|
sha1: string;
|
||||||
|
size: number;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Sources {
|
||||||
|
path: string;
|
||||||
|
sha1: string;
|
||||||
|
size: number;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Javadoc {
|
||||||
|
path: string;
|
||||||
|
sha1: string;
|
||||||
|
size: number;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Classifiers {
|
||||||
|
["natives-linux"]: NativesLinux;
|
||||||
|
["natives-osx"]: NativesOsx;
|
||||||
|
["natives-windows"]: NativesWindows;
|
||||||
|
sources: Sources;
|
||||||
|
javadoc: Javadoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Downloads2 {
|
||||||
|
artifact: Artifact;
|
||||||
|
classifiers: Classifiers;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Os {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Rule {
|
||||||
|
action: string;
|
||||||
|
os: Os;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Extract {
|
||||||
|
exclude: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Natives {
|
||||||
|
linux: string;
|
||||||
|
osx: string;
|
||||||
|
windows: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Library {
|
||||||
|
downloads: Downloads2;
|
||||||
|
name: string;
|
||||||
|
rules: Rule[];
|
||||||
|
extract: Extract;
|
||||||
|
natives: Natives;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VersionManifest {
|
||||||
|
assetIndex: AssetIndex;
|
||||||
|
assets: string;
|
||||||
|
downloads: Downloads;
|
||||||
|
id: string;
|
||||||
|
libraries: Library[];
|
||||||
|
mainClass: string;
|
||||||
|
minecraftArguments: string;
|
||||||
|
minimumLauncherVersion: number;
|
||||||
|
releaseTime: Date;
|
||||||
|
time: Date;
|
||||||
|
type: string;
|
||||||
|
}
|
17
buildtools/types/versionsManifest.ts
Normal file
17
buildtools/types/versionsManifest.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
interface Latest {
|
||||||
|
release: string;
|
||||||
|
snapshot: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Version {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
url: string;
|
||||||
|
time: Date;
|
||||||
|
releaseTime: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VersionsManifest {
|
||||||
|
latest: Latest;
|
||||||
|
versions: Version[];
|
||||||
|
}
|
20
buildtools/util/buildConfig.default.json
Normal file
20
buildtools/util/buildConfig.default.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"downloaderMaxRetries": 5,
|
||||||
|
"downloaderConcurrency": 50,
|
||||||
|
"downloaderCheckHashes": true,
|
||||||
|
"downloaderCacheDirectory": "../.cache",
|
||||||
|
"launchscriptsMinRAM": "2048M",
|
||||||
|
"launchscriptsMaxRAM": "2048M",
|
||||||
|
"launchscriptsJVMArgs": "",
|
||||||
|
"copyFromSharedServerGlobs": [
|
||||||
|
"overrides/**/*",
|
||||||
|
"!overrides/resources/**/*"
|
||||||
|
],
|
||||||
|
"copyFromSharedClientGlobs": [
|
||||||
|
"overrides/**/*"
|
||||||
|
],
|
||||||
|
"buildDestinationDirectory": "../build",
|
||||||
|
"buildSourceDirectory": "../",
|
||||||
|
"nightlyHookAvatar": "",
|
||||||
|
"nightlyHookName": ""
|
||||||
|
}
|
157
buildtools/util/curseForgeAPI.ts
Normal file
157
buildtools/util/curseForgeAPI.ts
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import bluebird from "bluebird";
|
||||||
|
import { CurseForgeFetchedFileInfo, CurseForgeModInfo as CurseForgeProject } from "../types/curseForge";
|
||||||
|
import log from "fancy-log";
|
||||||
|
import request from "requestretry";
|
||||||
|
import { ModpackManifestFile } from "../types/modpackManifest";
|
||||||
|
import Bluebird from "bluebird";
|
||||||
|
import buildConfig from "../buildConfig";
|
||||||
|
import upath from "upath";
|
||||||
|
import fs from "fs";
|
||||||
|
import { FileDef } from "../types/fileDef";
|
||||||
|
import { downloadOrRetrieveFileDef, relative, RetrievedFileDefReason } from "./util";
|
||||||
|
|
||||||
|
const curseForgeProjectCache: { [key: number]: CurseForgeProject } = {};
|
||||||
|
export async function fetchProject(toFetch: number): Promise<CurseForgeProject> {
|
||||||
|
if (curseForgeProjectCache[toFetch]) {
|
||||||
|
return curseForgeProjectCache[toFetch];
|
||||||
|
}
|
||||||
|
|
||||||
|
const project: CurseForgeProject = await request({
|
||||||
|
uri: `https://addons-ecs.forgesvc.net/api/v2/addon/${toFetch}`,
|
||||||
|
json: true,
|
||||||
|
fullResponse: false,
|
||||||
|
maxAttempts: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (project) {
|
||||||
|
curseForgeProjectCache[toFetch] = project;
|
||||||
|
}
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchedFileInfoCache: { [key: string]: CurseForgeFetchedFileInfo } = {};
|
||||||
|
export async function fetchFileInfo(projectID: number, fileID: number): Promise<CurseForgeFetchedFileInfo> {
|
||||||
|
const slug = `${projectID}/${fileID}`;
|
||||||
|
|
||||||
|
if (fetchedFileInfoCache[slug]) {
|
||||||
|
return fetchedFileInfoCache[slug];
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileInfo: CurseForgeFetchedFileInfo = await request({
|
||||||
|
uri: `https://addons-ecs.forgesvc.net/api/v2/addon/${projectID}/file/${fileID}`,
|
||||||
|
json: true,
|
||||||
|
fullResponse: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fileInfo) {
|
||||||
|
fetchedFileInfoCache[slug] = fileInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches multiple CurseForge projects.
|
||||||
|
* Falls back to fetchModInfo in case it's impossible to bulk-fetch some projects.
|
||||||
|
*
|
||||||
|
* @param toFetch Project IDs to fetch.
|
||||||
|
* @returns CurseForge project infos.
|
||||||
|
*/
|
||||||
|
export async function fetchProjectsBulk(toFetch: number[]): Promise<CurseForgeProject[]> {
|
||||||
|
const modInfos: CurseForgeProject[] = [];
|
||||||
|
const unfetched: number[] = [];
|
||||||
|
|
||||||
|
// Determine projects that have been fetched already.
|
||||||
|
toFetch.forEach((id) => {
|
||||||
|
const cached = curseForgeProjectCache[id];
|
||||||
|
if (cached) {
|
||||||
|
modInfos.push(cached);
|
||||||
|
} else {
|
||||||
|
unfetched.push(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (unfetched.length > 0) {
|
||||||
|
// Augment the array of known projects with new info.
|
||||||
|
const fetched: CurseForgeProject[] = await request.post({
|
||||||
|
uri: "https://addons-ecs.forgesvc.net/api/v2/addon/",
|
||||||
|
json: unfetched,
|
||||||
|
fullResponse: false,
|
||||||
|
maxAttempts: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
modInfos.push(...fetched);
|
||||||
|
|
||||||
|
// Cache fetched stuff.
|
||||||
|
fetched.forEach((mi) => {
|
||||||
|
curseForgeProjectCache[mi.id] = mi;
|
||||||
|
});
|
||||||
|
|
||||||
|
// In case we haven't received the proper amount of mod infos,
|
||||||
|
// try requesting them individually.
|
||||||
|
if (unfetched.length !== toFetch.length) {
|
||||||
|
const modInfoIDs = new Set(modInfos.map((mi) => mi.id));
|
||||||
|
const toFetchMissing = [...new Set(toFetch.filter((x) => !modInfoIDs.has(x)))];
|
||||||
|
|
||||||
|
log.warn(`Couldn't fetch next project IDs in bulk: ${toFetchMissing.join(", ")}`);
|
||||||
|
|
||||||
|
// Try fetching mods individually, in case they've been deleted.
|
||||||
|
let count = 0;
|
||||||
|
const missingModInfos: CurseForgeProject[] = await bluebird.map(toFetchMissing, async (id) => {
|
||||||
|
log.info(`Fetching project ID ${id} directly... (${++count} / ${toFetchMissing.length})`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// In case something fails to download; catch, rewrite, rethrow.
|
||||||
|
return await fetchProject(id);
|
||||||
|
} catch (err) {
|
||||||
|
err.message = `Couldn't fetch project ID ${id}. ${err.message || "Unknown error"}`;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// The code above is expected to throw and terminate the further execution,
|
||||||
|
// so we can just do this.
|
||||||
|
modInfos.push(...missingModInfos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return modInfos;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchMods(toFetch: ModpackManifestFile[], destination: string): Promise<void[]> {
|
||||||
|
if (toFetch.length > 0) {
|
||||||
|
log(`Fetching ${toFetch.length} mods...`);
|
||||||
|
|
||||||
|
const modsPath = upath.join(destination, "mods");
|
||||||
|
await fs.promises.mkdir(modsPath, { recursive: true });
|
||||||
|
|
||||||
|
let fetched = 0;
|
||||||
|
return Bluebird.map(
|
||||||
|
toFetch,
|
||||||
|
async (file) => {
|
||||||
|
const fileInfo = await fetchFileInfo(file.projectID, file.fileID);
|
||||||
|
|
||||||
|
const fileDef: FileDef = {
|
||||||
|
url: fileInfo.downloadUrl,
|
||||||
|
hashes: [{ id: "murmurhash", hashes: fileInfo.packageFingerprint }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const modFile = await downloadOrRetrieveFileDef(fileDef);
|
||||||
|
fetched += 1;
|
||||||
|
|
||||||
|
if (modFile.reason == RetrievedFileDefReason.Downloaded) {
|
||||||
|
log(`Downloaded ${upath.basename(fileInfo.downloadUrl)}... (${fetched} / ${toFetch.length})`);
|
||||||
|
} else if (modFile.reason == RetrievedFileDefReason.CacheHit) {
|
||||||
|
log(`Fetched ${upath.basename(fileInfo.downloadUrl)} from cache... (${fetched} / ${toFetch.length})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dest = upath.join(destination, "mods", fileInfo.fileName);
|
||||||
|
|
||||||
|
await fs.promises.symlink(relative(dest, modFile.cachePath), dest);
|
||||||
|
},
|
||||||
|
{ concurrency: buildConfig.downloaderConcurrency },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log("No mods to fetch.");
|
||||||
|
}
|
||||||
|
}
|
110
buildtools/util/hashes.ts
Normal file
110
buildtools/util/hashes.ts
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* Bytes to exclude from hashing.
|
||||||
|
*
|
||||||
|
* Why? I dunno.
|
||||||
|
*/
|
||||||
|
const MURMUR_SKIP_BYTES: { [key: number]: boolean } = {
|
||||||
|
9: true,
|
||||||
|
10: true,
|
||||||
|
13: true,
|
||||||
|
32: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
import _sha1 from "sha1";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JS Implementation of MurmurHash2
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:gary.court@gmail.com">Gary Court</a>
|
||||||
|
* @see http://github.com/garycourt/murmurhash-js
|
||||||
|
* @author <a href="mailto:aappleby@gmail.com">Austin Appleby</a>
|
||||||
|
* @see http://sites.google.com/site/murmurhash/
|
||||||
|
*/
|
||||||
|
function murmurhash2_32_gc(arr, seed, len = arr.length) {
|
||||||
|
let l = len,
|
||||||
|
h = seed ^ l,
|
||||||
|
i = 0,
|
||||||
|
k;
|
||||||
|
|
||||||
|
while (l >= 4) {
|
||||||
|
k = (arr[i] & 0xff) | ((arr[++i] & 0xff) << 8) | ((arr[++i] & 0xff) << 16) | ((arr[++i] & 0xff) << 24);
|
||||||
|
|
||||||
|
k = (k & 0xffff) * 0x5bd1e995 + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16);
|
||||||
|
k ^= k >>> 24;
|
||||||
|
k = (k & 0xffff) * 0x5bd1e995 + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16);
|
||||||
|
|
||||||
|
h = ((h & 0xffff) * 0x5bd1e995 + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)) ^ k;
|
||||||
|
|
||||||
|
l -= 4;
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (l) {
|
||||||
|
case 3:
|
||||||
|
h ^= (arr[i + 2] & 0xff) << 16;
|
||||||
|
case 2:
|
||||||
|
h ^= (arr[i + 1] & 0xff) << 8;
|
||||||
|
case 1:
|
||||||
|
h ^= arr[i] & 0xff;
|
||||||
|
h = (h & 0xffff) * 0x5bd1e995 + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
h ^= h >>> 13;
|
||||||
|
h = (h & 0xffff) * 0x5bd1e995 + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16);
|
||||||
|
h ^= h >>> 15;
|
||||||
|
|
||||||
|
return h >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the hash sum of bytes of given bytes using MurmurHash v2.
|
||||||
|
*
|
||||||
|
* This is what Twitch is using to fingerprint mod files.
|
||||||
|
*/
|
||||||
|
export const murmurhash = (inputBuffer: Buffer, seed = 1): string => {
|
||||||
|
const output = new Uint8Array(inputBuffer.length);
|
||||||
|
|
||||||
|
let pos = 0;
|
||||||
|
for (let i = 0; i < inputBuffer.length; i++) {
|
||||||
|
const byte = inputBuffer.readUInt8(i);
|
||||||
|
|
||||||
|
if (!MURMUR_SKIP_BYTES[byte]) {
|
||||||
|
output[pos++] = byte;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(murmurhash2_32_gc(output, seed, pos));
|
||||||
|
};
|
||||||
|
|
||||||
|
import { HashDef } from "../types/hashDef";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the hash sum of bytes of given bytes using SHA1.
|
||||||
|
*
|
||||||
|
* This is what Forge is using to check files.
|
||||||
|
*/
|
||||||
|
export const sha1 = (inputBuffer: Buffer): string => {
|
||||||
|
return _sha1(inputBuffer);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hashFuncs: { [key: string]: (buffer: Buffer) => string } = {
|
||||||
|
murmurhash: murmurhash,
|
||||||
|
sha1: sha1,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare buffer to the given HashDef.
|
||||||
|
*
|
||||||
|
* @param {Buffer} buffer
|
||||||
|
* @param {HashDef} hashDef
|
||||||
|
*
|
||||||
|
* @throws {Error} Throws a generic error if hashes don't match.
|
||||||
|
*/
|
||||||
|
export const compareBufferToHashDef = (buffer: Buffer, hashDef: HashDef): boolean => {
|
||||||
|
if (!hashFuncs[hashDef.id]) {
|
||||||
|
throw new Error(`No hash function found for ${hashDef.id}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sum = hashFuncs[hashDef.id](buffer);
|
||||||
|
return (Array.isArray(hashDef.hashes) && hashDef.hashes.includes(sum)) || hashDef.hashes == sum;
|
||||||
|
};
|
365
buildtools/util/util.ts
Normal file
365
buildtools/util/util.ts
Normal file
@ -0,0 +1,365 @@
|
|||||||
|
import sha1 from "sha1";
|
||||||
|
import { FileDef } from "../types/fileDef";
|
||||||
|
import fs from "fs";
|
||||||
|
import buildConfig from "../buildConfig";
|
||||||
|
import upath from "upath";
|
||||||
|
import requestretry from "requestretry";
|
||||||
|
import http from "http";
|
||||||
|
import { compareBufferToHashDef } from "./hashes";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import { ModpackManifest, ModpackManifestFile, ExternalDependency } from "../types/modpackManifest";
|
||||||
|
import { fetchProject, fetchProjectsBulk } from "./curseForgeAPI";
|
||||||
|
import Bluebird from "bluebird";
|
||||||
|
import { VersionManifest } from "../types/versionManifest";
|
||||||
|
import { VersionsManifest } from "../types/versionsManifest";
|
||||||
|
import request from "requestretry";
|
||||||
|
import log from "fancy-log";
|
||||||
|
|
||||||
|
const LIBRARY_REG = /^(.+?):(.+?):(.+?)$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the library name into path following the standard package naming convention.
|
||||||
|
*
|
||||||
|
* Turns `package:name:version` into `package/name/version/name-version`.
|
||||||
|
*/
|
||||||
|
export const libraryToPath = (library: string): string => {
|
||||||
|
const parsedLibrary = LIBRARY_REG.exec(library);
|
||||||
|
if (parsedLibrary) {
|
||||||
|
const pkg = parsedLibrary[1].replace(/\./g, "/");
|
||||||
|
const name = parsedLibrary[2];
|
||||||
|
const version = parsedLibrary[3];
|
||||||
|
|
||||||
|
const newURL = `${pkg}/${name}/${version}/${name}-${version}`;
|
||||||
|
|
||||||
|
return newURL;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if given environmental variables are set. Throws otherwise.
|
||||||
|
*/
|
||||||
|
export const checkEnvironmentalVariables = (vars: string[]): void => {
|
||||||
|
vars.forEach((vari) => {
|
||||||
|
if (!process.env[vari] || process.env[vari] == "") {
|
||||||
|
throw new Error(`Environmental variable ${vari} is unset.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum RetrievedFileDefReason {
|
||||||
|
Downloaded,
|
||||||
|
CacheHit,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RetrievedFileDef {
|
||||||
|
reason: RetrievedFileDefReason;
|
||||||
|
cachePath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads/fetches files from the Interwebs.
|
||||||
|
*
|
||||||
|
* Internally hashes the URL of the provided FileDef and looks it up in the cache directory.
|
||||||
|
* In case of no cache hit, downloads the file and stores within the cache directory for later use.
|
||||||
|
*/
|
||||||
|
export async function downloadOrRetrieveFileDef(fileDef: FileDef): Promise<RetrievedFileDef> {
|
||||||
|
const fileNameSha = sha1(fileDef.url);
|
||||||
|
|
||||||
|
const cachedFilePath = upath.join(buildConfig.downloaderCacheDirectory, fileNameSha);
|
||||||
|
if (fs.existsSync(cachedFilePath)) {
|
||||||
|
const file = await fs.promises.readFile(cachedFilePath);
|
||||||
|
|
||||||
|
if (file.length !== 0) {
|
||||||
|
const rFileDef = {
|
||||||
|
reason: RetrievedFileDefReason.CacheHit,
|
||||||
|
cachePath: cachedFilePath,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check hashes.
|
||||||
|
if (fileDef.hashes) {
|
||||||
|
if (
|
||||||
|
fileDef.hashes.every((hashDef) => {
|
||||||
|
return compareBufferToHashDef(file, hashDef);
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return rFileDef;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return rFileDef;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(buildConfig.downloaderCacheDirectory)) {
|
||||||
|
await fs.promises.mkdir(buildConfig.downloaderCacheDirectory, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
let handle: fs.promises.FileHandle;
|
||||||
|
try {
|
||||||
|
handle = await fs.promises.open(cachedFilePath, "w");
|
||||||
|
|
||||||
|
let hashFailed = false;
|
||||||
|
const retryStrategy = (err: Error, response: http.IncomingMessage, body: unknown) => {
|
||||||
|
// Verify hashes.
|
||||||
|
if (!err && fileDef.hashes && body) {
|
||||||
|
const success = fileDef.hashes.every((hashDef) => {
|
||||||
|
return compareBufferToHashDef(body as Buffer, hashDef);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
if (hashFailed) {
|
||||||
|
throw new Error(`Couldn't verify checksums of ${upath.basename(fileDef.url)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
hashFailed = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return requestretry.RetryStrategies.HTTPOrNetworkError(err, response, body);
|
||||||
|
};
|
||||||
|
|
||||||
|
const data: Buffer = Buffer.from(
|
||||||
|
await requestretry({
|
||||||
|
url: fileDef.url,
|
||||||
|
fullResponse: false,
|
||||||
|
encoding: null,
|
||||||
|
retryStrategy: retryStrategy,
|
||||||
|
maxAttempts: 5,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await handle.write(data);
|
||||||
|
await handle.close();
|
||||||
|
|
||||||
|
return {
|
||||||
|
reason: RetrievedFileDefReason.Downloaded,
|
||||||
|
cachePath: cachedFilePath,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
if (handle && (await handle.stat()).isFile()) {
|
||||||
|
log(`Couldn't download ${upath.basename(fileDef.url)}, cleaning up ${fileNameSha}...`);
|
||||||
|
|
||||||
|
await handle.close();
|
||||||
|
await fs.promises.unlink(cachedFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns artifact name body depending on environment variables.
|
||||||
|
* Mostly intended to be called by CI/CD.
|
||||||
|
*/
|
||||||
|
export function makeArtifactNameBody(baseName: string): string {
|
||||||
|
// If the tag is provided by CI, simply just glue it to the base name.
|
||||||
|
if (process.env.GITHUB_TAG) {
|
||||||
|
return `${baseName}-${process.env.GITHUB_TAG}`;
|
||||||
|
}
|
||||||
|
// If SHA is provided and the build isn't tagged, append both the branch and short SHA.
|
||||||
|
else if (process.env.GITHUB_SHA && process.env.GITHUB_REF && process.env.GITHUB_REF.startsWith("refs/heads/")) {
|
||||||
|
const shortCommit = process.env.GITHUB_SHA.substr(0, 7);
|
||||||
|
const branch = /refs\/heads\/(.+)/.exec(process.env.GITHUB_REF);
|
||||||
|
return `${baseName}-${branch[1]}-${shortCommit}`;
|
||||||
|
} else {
|
||||||
|
return baseName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the last tag known to Git using the current branch.
|
||||||
|
* @param {string | nil} before Tag to get the tag before.
|
||||||
|
* @returns string Git tag.
|
||||||
|
* @throws
|
||||||
|
*/
|
||||||
|
export function getLastGitTag(before?: string): string {
|
||||||
|
if (before) {
|
||||||
|
before = `"${before}^"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return execSync(`git describe --abbrev=0 --tags ${before || ""}`)
|
||||||
|
.toString()
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a changelog based on the two provided Git refs.
|
||||||
|
* @param {string} since Lower boundary Git ref.
|
||||||
|
* @param {string} to Upper boundary Git ref.
|
||||||
|
* @param {string[]} dirs Optional scopes.
|
||||||
|
*/
|
||||||
|
export function getChangeLog(since = "HEAD", to = "HEAD", dirs: string[] = undefined): string {
|
||||||
|
const command = [
|
||||||
|
"git log",
|
||||||
|
"--no-merges",
|
||||||
|
'--date="format:%d %b %Y"',
|
||||||
|
'--pretty="* %s - **%an** (%ad)"',
|
||||||
|
`${since}..${to}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (dirs) {
|
||||||
|
command.push("--", dirs.join(" -- "));
|
||||||
|
}
|
||||||
|
|
||||||
|
return execSync(command.join(" ")).toString().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a changelog based on the two provided Git refs.
|
||||||
|
* @param {string} since Lower boundary Git ref.
|
||||||
|
* @param {string} to Upper boundary Git ref.
|
||||||
|
* @param {string[]} dirs Optional scopes.
|
||||||
|
*/
|
||||||
|
export function getFileAtRevision(path: string, revision = "HEAD"): string {
|
||||||
|
return execSync(`git show ${revision}:"${path}"`).toString().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ManifestFileListComparisonResult {
|
||||||
|
removed: string[];
|
||||||
|
modified: string[];
|
||||||
|
added: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function compareAndExpandManifestDependencies(
|
||||||
|
oldFiles: ModpackManifest,
|
||||||
|
newFiles: ModpackManifest,
|
||||||
|
): Promise<ManifestFileListComparisonResult> {
|
||||||
|
// Map inputs for efficient joining.
|
||||||
|
const oldFileMap: { [key: number]: ModpackManifestFile } = oldFiles.files.reduce(
|
||||||
|
(map, file) => ((map[file.projectID] = file), map),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
const newFileMap: { [key: number]: ModpackManifestFile } = newFiles.files.reduce(
|
||||||
|
(map, file) => ((map[file.projectID] = file), map),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const removed: string[] = [],
|
||||||
|
modified: string[] = [],
|
||||||
|
added: string[] = [];
|
||||||
|
|
||||||
|
// Create a distinct map of project IDs.
|
||||||
|
const projectIDs = Array.from(
|
||||||
|
new Set([...oldFiles.files.map((f) => f.projectID), ...newFiles.files.map((f) => f.projectID)]),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fetch projects in bulk and discard the result.
|
||||||
|
// Future calls to fetchProject() and fetchProjectsBulk() will hit the cache.
|
||||||
|
await fetchProjectsBulk(projectIDs);
|
||||||
|
|
||||||
|
await Bluebird.map(
|
||||||
|
projectIDs,
|
||||||
|
async (projectID) => {
|
||||||
|
const oldFileInfo = oldFileMap[projectID];
|
||||||
|
const newFileInfo = newFileMap[projectID];
|
||||||
|
|
||||||
|
// Doesn't exist in new, but exists in old. Removed. Left outer join.
|
||||||
|
if (!newFileInfo && oldFileInfo) {
|
||||||
|
removed.push((await fetchProject(oldFileInfo.projectID)).name);
|
||||||
|
}
|
||||||
|
// Doesn't exist in old, but exists in new. Added. Right outer join.
|
||||||
|
else if (newFileMap[projectID] && !oldFileMap[projectID]) {
|
||||||
|
added.push((await fetchProject(newFileInfo.projectID)).name);
|
||||||
|
}
|
||||||
|
// Exists in both. Modified? Inner join.
|
||||||
|
else if (oldFileInfo.fileID != newFileInfo.fileID) {
|
||||||
|
modified.push((await fetchProject(newFileInfo.projectID)).name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ concurrency: buildConfig.downloaderConcurrency },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Compare external dependencies the same way.
|
||||||
|
const oldExternalMap: { [key: string]: ExternalDependency } = (oldFiles.externalDependencies || []).reduce(
|
||||||
|
(map, file) => ((map[file.name] = file), map),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
const newExternalMap: { [key: string]: ExternalDependency } = (newFiles.externalDependencies || []).reduce(
|
||||||
|
(map, file) => ((map[file.name] = file), map),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const externalNames = Array.from(
|
||||||
|
new Set([
|
||||||
|
...(oldFiles.externalDependencies || []).map((dep) => dep.name),
|
||||||
|
...(newFiles.externalDependencies || []).map((dep) => dep.name),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
externalNames.forEach(async (name) => {
|
||||||
|
const oldDep = oldExternalMap[name];
|
||||||
|
const newDep = newExternalMap[name];
|
||||||
|
|
||||||
|
// Doesn't exist in new, but exists in old. Removed. Left outer join.
|
||||||
|
if (!newDep && oldDep) {
|
||||||
|
removed.push(oldDep.name);
|
||||||
|
}
|
||||||
|
// Doesn't exist in old, but exists in new. Added. Right outer join.
|
||||||
|
else if (newDep && !oldDep) {
|
||||||
|
added.push(newDep.name);
|
||||||
|
}
|
||||||
|
// Exists in both. Modified? Inner join.
|
||||||
|
else if (oldDep.url != newDep.url || oldDep.name != newDep.name) {
|
||||||
|
modified.push(newDep.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
removed: removed,
|
||||||
|
modified: modified,
|
||||||
|
added: added,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const LAUNCHERMETA_VERSION_MANIFEST = "https://launchermeta.mojang.com/mc/game/version_manifest.json";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the version manifest associated with the provided Minecraft version.
|
||||||
|
*
|
||||||
|
* @param minecraftVersion Minecraft version. (e. g., "1.12.2")
|
||||||
|
*/
|
||||||
|
export async function getVersionManifest(minecraftVersion: string): Promise<VersionManifest> {
|
||||||
|
/**
|
||||||
|
* Fetch the manifest file of all Minecraft versions.
|
||||||
|
*/
|
||||||
|
const manifest: VersionsManifest = await request({
|
||||||
|
uri: LAUNCHERMETA_VERSION_MANIFEST,
|
||||||
|
json: true,
|
||||||
|
fullResponse: false,
|
||||||
|
maxAttempts: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
const version = manifest.versions.find((x) => x.id == minecraftVersion);
|
||||||
|
if (!version) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the version manifest file.
|
||||||
|
*/
|
||||||
|
const versionManifest: VersionManifest = await request({
|
||||||
|
uri: version.url,
|
||||||
|
json: true,
|
||||||
|
fullResponse: false,
|
||||||
|
maxAttempts: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
return versionManifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a relative posix path from the first argument to the second.
|
||||||
|
*/
|
||||||
|
export function relative(from: string, to: string): string {
|
||||||
|
const broken = [from.split(upath.sep), to.split(upath.sep)];
|
||||||
|
|
||||||
|
while (broken.every((x) => x.length > 0) && broken[0][0] == broken[1][0]) {
|
||||||
|
broken.forEach((x) => x.shift());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (broken.some((x) => x.length === 0)) {
|
||||||
|
throw new Error("Paths are not relative.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return upath.join(...Array(broken[0].length - 1).fill(".."), ...broken[1]);
|
||||||
|
}
|
36
launchscripts/launch.bat
Normal file
36
launchscripts/launch.bat
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
:: Server Launch Script
|
||||||
|
::
|
||||||
|
:: Thrown together by Neeve in under five minutes, Public Domain
|
||||||
|
:: https://github.com/Neeve01
|
||||||
|
::
|
||||||
|
:: Added java version check by t0suj4, Public Domain
|
||||||
|
:: https://github.com/t0su4
|
||||||
|
|
||||||
|
:: DO NOT EDIT UNLESS YOU KNOW WHAT YOU'RE DOING
|
||||||
|
@ECHO OFF
|
||||||
|
SET FORGEJAR={{forgeJar}}
|
||||||
|
SET JAVA_PARAMETERS={{jvmArgs}}
|
||||||
|
|
||||||
|
:: these you can edit
|
||||||
|
SET MIN_RAM={{minRAM}}
|
||||||
|
SET MAX_RAM={{maxRAM}}
|
||||||
|
|
||||||
|
:: DO NOT EDIT ANYTHING PAST THIS LINE
|
||||||
|
SET LAUNCHPARAMS=-server -Xms%MIN_RAM% -Xmx%MAX_RAM% %JAVA_PARAMETERS% -Dlog4j.configurationFile=log4j2_112-116.xml -jar %FORGEJAR% nogui
|
||||||
|
echo Checking java version...
|
||||||
|
echo.
|
||||||
|
java -version
|
||||||
|
echo.
|
||||||
|
echo The expected java version is 1.8. Not higher, not lower.
|
||||||
|
echo.
|
||||||
|
|
||||||
|
echo Launching the server...
|
||||||
|
echo.
|
||||||
|
echo ^> java %LAUNCHPARAMS%
|
||||||
|
echo.
|
||||||
|
java %LAUNCHPARAMS%
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ^> The server has stopped. If it's a crash, please read the output above.
|
||||||
|
echo.
|
||||||
|
pause
|
38
launchscripts/launch.sh
Normal file
38
launchscripts/launch.sh
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Server Launch Script
|
||||||
|
#
|
||||||
|
# Thrown together by Neeve in under five minutes, Public Domain
|
||||||
|
# https://github.com/Neeve01
|
||||||
|
#
|
||||||
|
# Fixed and added java version check by t0suj4, Public Domain
|
||||||
|
# https://github.com/t0suj4
|
||||||
|
|
||||||
|
# DO NOT EDIT UNLESS YOU KNOW WHAT YOU'RE DOING
|
||||||
|
FORGEJAR='{{forgeJar}}'
|
||||||
|
JAVA_PARAMETERS='{{jvmArgs}}'
|
||||||
|
|
||||||
|
# these you can edit
|
||||||
|
MIN_RAM='{{minRAM}}'
|
||||||
|
MAX_RAM='{{maxRAM}}'
|
||||||
|
|
||||||
|
# DO NOT EDIT ANYTHING PAST THIS LINE
|
||||||
|
LAUNCHPARAMS="-server -Xms$MIN_RAM -Xmx$MAX_RAM $JAVA_PARAMETERS -Dlog4j.configurationFile=log4j2_112-116.xml -jar $FORGEJAR nogui"
|
||||||
|
|
||||||
|
echo "Checking java version..."
|
||||||
|
echo
|
||||||
|
java -version
|
||||||
|
echo
|
||||||
|
echo "The expected java version is 1.8. Not higher, not lower."
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "Launching the server..."
|
||||||
|
echo
|
||||||
|
echo "> java $LAUNCHPARAMS"
|
||||||
|
|
||||||
|
java $LAUNCHPARAMS
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "- The server has stopped. If it's a crash, please read the output above."
|
||||||
|
echo
|
||||||
|
read -p "- Press Return to exit..." _
|
28
serverfiles/log4j2_112-116.xml
Normal file
28
serverfiles/log4j2_112-116.xml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Configuration status="WARN">
|
||||||
|
<Appenders>
|
||||||
|
<Console name="SysOut" target="SYSTEM_OUT">
|
||||||
|
<PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg{nolookups}%n" />
|
||||||
|
</Console>
|
||||||
|
<Queue name="ServerGuiConsole">
|
||||||
|
<PatternLayout pattern="[%d{HH:mm:ss} %level]: %msg{nolookups}%n" />
|
||||||
|
</Queue>
|
||||||
|
<RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz">
|
||||||
|
<PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg{nolookups}%n" />
|
||||||
|
<Policies>
|
||||||
|
<TimeBasedTriggeringPolicy />
|
||||||
|
<OnStartupTriggeringPolicy />
|
||||||
|
</Policies>
|
||||||
|
</RollingRandomAccessFile>
|
||||||
|
</Appenders>
|
||||||
|
<Loggers>
|
||||||
|
<Root level="info">
|
||||||
|
<filters>
|
||||||
|
<MarkerFilter marker="NETWORK_PACKETS" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||||
|
</filters>
|
||||||
|
<AppenderRef ref="SysOut"/>
|
||||||
|
<AppenderRef ref="File"/>
|
||||||
|
<AppenderRef ref="ServerGuiConsole"/>
|
||||||
|
</Root>
|
||||||
|
</Loggers>
|
||||||
|
</Configuration>
|
32
serverfiles/server.properties
Normal file
32
serverfiles/server.properties
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
#Minecraft server properties
|
||||||
|
op-permission-level=4
|
||||||
|
level-name=world
|
||||||
|
allow-flight=true
|
||||||
|
prevent-proxy-connections=false
|
||||||
|
server-port=25565
|
||||||
|
max-world-size=29999984
|
||||||
|
level-seed=
|
||||||
|
force-gamemode=false
|
||||||
|
server-ip=
|
||||||
|
network-compression-threshold=256
|
||||||
|
max-build-height=256
|
||||||
|
spawn-npcs=true
|
||||||
|
white-list=false
|
||||||
|
spawn-animals=true
|
||||||
|
hardcore=false
|
||||||
|
snooper-enabled=true
|
||||||
|
resource-pack-sha1=
|
||||||
|
online-mode=true
|
||||||
|
resource-pack=
|
||||||
|
pvp=true
|
||||||
|
difficulty=1
|
||||||
|
enable-command-block=false
|
||||||
|
gamemode=0
|
||||||
|
player-idle-timeout=0
|
||||||
|
max-players=20
|
||||||
|
spawn-monsters=true
|
||||||
|
view-distance=10
|
||||||
|
generate-structures=true
|
||||||
|
motd=Nomifactory Server
|
||||||
|
level-type=lostcities
|
||||||
|
generator-settings=
|
Loading…
x
Reference in New Issue
Block a user