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