[EXPAND] [[messages]] messageTitle = "QB Update for GT 2.8 (#681)" messageBody = """ [QB] [DETAILS] details = ["Fixes many Quest Book issues", "Updates QB with changes in GT 2.8"] [DETAILS] """ [[messages]] messageTitle = "Buildscript Refactor (#681)" messageBody = """ [INTERNAL] [DETAILS] details = ["**Important: Buildscript has changed from `npx gulp...` or `gulp...` to `npm run gulp...`**!", "Moves to Node 16 Package Management + Typescript Strict Mode", "New Port QB, Check QB and Fix QB Tasks"] [DETAILS] """ [EXPAND] Co-authored-by: Integer Limit <103940576+IntegerLimit@users.noreply.github.com> Co-authored-by: Ghzdude <44148655+ghzdude@users.noreply.github.com> Co-authored-by: SparkedTheorem <162088357+SparkedTheorem@users.noreply.github.com>
432 lines
11 KiB
TypeScript
432 lines
11 KiB
TypeScript
import { QuestBook } from "#types/bqQuestBook.ts";
|
|
import fs from "fs";
|
|
import {
|
|
cfgExpertPath,
|
|
cfgNormalPath,
|
|
cfgOverrideExpertPath,
|
|
cfgOverrideNormalPath,
|
|
emptyQuest,
|
|
id,
|
|
name,
|
|
stringifyQB,
|
|
stripRewards,
|
|
} from "#tasks/helpers/actionQBUtils.ts";
|
|
import { input, select } from "@inquirer/prompts";
|
|
import { SourceOption } from "#types/actionQBTypes.ts";
|
|
import logInfo, { logWarn } from "#utils/log.ts";
|
|
import upath from "upath";
|
|
import { rootDirectory } from "#globals";
|
|
import colors from "colors";
|
|
import { isEnvVariableSet } from "#utils/util.ts";
|
|
import * as core from "@actions/core";
|
|
import lodash from "lodash";
|
|
|
|
const isAvailableForFormatting = /[0-9a-ek-or]/;
|
|
|
|
export const check = async () => {
|
|
try {
|
|
await checkAndFix(true);
|
|
} catch (e) {
|
|
if (isEnvVariableSet("GITHUB_STEP_SUMMARY")) {
|
|
const summary = core.summary
|
|
.addHeading("Quest Book Format Error!", 2)
|
|
.addRaw(
|
|
"Run the below Command in your Local Clone to Format the Quest Book:",
|
|
true,
|
|
)
|
|
.addCodeBlock("npm run gulp fixQB");
|
|
|
|
if (e instanceof Error) summary.addDetails("Details...", e.message);
|
|
|
|
await summary.write();
|
|
}
|
|
throw e;
|
|
}
|
|
};
|
|
export const fix = () => checkAndFix(false);
|
|
|
|
async function checkAndFix(shouldCheck: boolean) {
|
|
logInfo(colors.bold(`${shouldCheck ? "Checking" : "Fixing"} QB...`));
|
|
let checkNormalQB: QuestBook;
|
|
let checkExpertQB: QuestBook;
|
|
|
|
if (shouldCheck) {
|
|
const nml1 = await fs.promises.readFile(
|
|
upath.join(rootDirectory, cfgNormalPath),
|
|
"utf-8",
|
|
);
|
|
const nml2 = await fs.promises.readFile(
|
|
upath.join(rootDirectory, cfgOverrideNormalPath),
|
|
"utf-8",
|
|
);
|
|
if (nml1 !== nml2) throw new Error("Normal Quest Books are not the Same!");
|
|
|
|
const exp1 = await fs.promises.readFile(
|
|
upath.join(rootDirectory, cfgExpertPath),
|
|
"utf-8",
|
|
);
|
|
const exp2 = await fs.promises.readFile(
|
|
upath.join(rootDirectory, cfgOverrideExpertPath),
|
|
"utf-8",
|
|
);
|
|
if (exp1 !== exp2) throw new Error("Normal Quest Books are not the Same!");
|
|
|
|
checkNormalQB = JSON.parse(nml1) as QuestBook;
|
|
checkExpertQB = JSON.parse(exp1) as QuestBook;
|
|
} else {
|
|
const normalSrc = (await select({
|
|
message: "Which version should we use, for the Normal Source File?",
|
|
choices: [
|
|
{
|
|
name: "Main Config Dir",
|
|
value: "CFG" as SourceOption,
|
|
},
|
|
{
|
|
name: "Config Overrides",
|
|
value: "CFG-OVERRIDE" as SourceOption,
|
|
},
|
|
],
|
|
})) as SourceOption;
|
|
|
|
const expertSrc = (await select({
|
|
message: "Which version should we use, for the Expert Source File?",
|
|
choices: [
|
|
{
|
|
name: "Main Config Dir",
|
|
value: "CFG" as SourceOption,
|
|
},
|
|
{
|
|
name: "Config Overrides",
|
|
value: "CFG-OVERRIDE" as SourceOption,
|
|
},
|
|
],
|
|
})) as SourceOption;
|
|
|
|
checkNormalQB = JSON.parse(
|
|
await fs.promises.readFile(
|
|
upath.join(
|
|
rootDirectory,
|
|
normalSrc === "CFG" ? cfgNormalPath : cfgOverrideNormalPath,
|
|
),
|
|
"utf-8",
|
|
),
|
|
);
|
|
|
|
checkExpertQB = JSON.parse(
|
|
await fs.promises.readFile(
|
|
upath.join(
|
|
rootDirectory,
|
|
expertSrc === "CFG" ? cfgExpertPath : cfgOverrideExpertPath,
|
|
),
|
|
"utf-8",
|
|
),
|
|
);
|
|
}
|
|
|
|
logInfo(colors.bold("Processing Normal QB..."));
|
|
await checkAndFixQB(shouldCheck, checkNormalQB, false);
|
|
logInfo(colors.bold("Processing Expert QB..."));
|
|
await checkAndFixQB(shouldCheck, checkExpertQB, true);
|
|
|
|
if (!shouldCheck) {
|
|
logInfo("Saving...");
|
|
const normal = stringifyQB(checkNormalQB);
|
|
const expert = stringifyQB(checkExpertQB);
|
|
await Promise.all([
|
|
fs.promises.writeFile(upath.join(rootDirectory, cfgNormalPath), normal),
|
|
fs.promises.writeFile(
|
|
upath.join(rootDirectory, cfgOverrideNormalPath),
|
|
normal,
|
|
),
|
|
fs.promises.writeFile(upath.join(rootDirectory, cfgExpertPath), expert),
|
|
fs.promises.writeFile(
|
|
upath.join(rootDirectory, cfgOverrideExpertPath),
|
|
expert,
|
|
),
|
|
]);
|
|
} else logInfo(colors.green("Successful. No Formatting Errors!"));
|
|
}
|
|
|
|
async function checkAndFixQB(
|
|
shouldCheck: boolean,
|
|
qb: QuestBook,
|
|
isExpert: boolean,
|
|
) {
|
|
let index = 0;
|
|
// Use if Should Check is false, so we don't modify the underlying object.
|
|
const newQB = shouldCheck
|
|
? qb["questDatabase:9"]
|
|
: lodash.cloneDeep(qb["questDatabase:9"]);
|
|
|
|
// Checks for Quests
|
|
logInfo("Checking Quests...");
|
|
for (const questKey of Object.keys(qb["questDatabase:9"])) {
|
|
// Copy Quest if Should Check is false (So we don't modify the underlying object)
|
|
const quest = shouldCheck
|
|
? qb["questDatabase:9"][questKey]
|
|
: { ...qb["questDatabase:9"][questKey] };
|
|
|
|
const foundID = id(quest);
|
|
|
|
// Check for Missing Quests
|
|
while (foundID > index) {
|
|
if (shouldCheck) throw new Error(`Missing Quest at Index ${index}!`);
|
|
logWarn(`Adding Empty Quest at Index ${index}...`);
|
|
const newQuest = { ...emptyQuest };
|
|
newQuest["questID:3"] = index;
|
|
newQB[`${index}:10`] = newQuest;
|
|
index++;
|
|
}
|
|
|
|
index++;
|
|
|
|
// Check Name Formatting
|
|
quest["properties:10"]["betterquesting:10"]["name:8"] =
|
|
stripOrThrowExcessFormatting(
|
|
shouldCheck,
|
|
name(quest),
|
|
foundID,
|
|
"Quest",
|
|
"Name",
|
|
);
|
|
|
|
// Check for Empty Descriptions (Trim first, might be a space)
|
|
if (!quest["properties:10"]["betterquesting:10"]["desc:8"].trim()) {
|
|
if (shouldCheck)
|
|
throw new Error(`Quest with ID ${foundID} has Empty Description!`);
|
|
|
|
quest["properties:10"]["betterquesting:10"]["desc:8"] = await input({
|
|
message: `Quest with ID ${foundID} and Name ${quest["properties:10"]["betterquesting:10"]["name:8"]} has an Empty Description! What should we Replace it With?`,
|
|
default: "No Description",
|
|
validate: (value) => Boolean(value.trim()),
|
|
});
|
|
}
|
|
// Check Desc Formatting (Still check if after, as user may have entered dupe formatting)
|
|
quest["properties:10"]["betterquesting:10"]["desc:8"] =
|
|
stripOrThrowExcessFormatting(
|
|
shouldCheck,
|
|
quest["properties:10"]["betterquesting:10"]["desc:8"],
|
|
foundID,
|
|
"Quest",
|
|
"Description",
|
|
);
|
|
|
|
const trimmed =
|
|
quest["properties:10"]["betterquesting:10"]["desc:8"].trim();
|
|
|
|
// Check if Description is Trimmed (Still check if after, as user may have entered new lines)
|
|
if (quest["properties:10"]["betterquesting:10"]["desc:8"] !== trimmed) {
|
|
if (shouldCheck)
|
|
throw new Error(
|
|
`Quest with ID ${foundID} has Excess Spaces/New Lines in the Description!`,
|
|
);
|
|
|
|
logWarn(`Trimming Description of Quest with ID ${foundID}!`);
|
|
quest["properties:10"]["betterquesting:10"]["desc:8"] = trimmed;
|
|
}
|
|
|
|
// Visibility Check
|
|
if (
|
|
quest["properties:10"]["betterquesting:10"]["visibility:8"] === "NORMAL"
|
|
) {
|
|
if (shouldCheck)
|
|
throw new Error(`Quest with ID ${foundID} has Visibility Normal!`);
|
|
|
|
quest["properties:10"]["betterquesting:10"]["visibility:8"] =
|
|
await select({
|
|
message: `Quest with ID ${foundID} has Visibility Normal! What should we Replace it With?`,
|
|
choices: [
|
|
{
|
|
name: "Always",
|
|
value: "ALWAYS",
|
|
},
|
|
{
|
|
name: "Chain",
|
|
value: "CHAIN",
|
|
},
|
|
{
|
|
name: "Hidden",
|
|
value: "HIDDEN",
|
|
},
|
|
{
|
|
name: "Unlocked",
|
|
value: "UNLOCKED",
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
// Check the Order of Prerequisites
|
|
const oldPrerequisites = shouldCheck
|
|
? quest["preRequisites:11"]
|
|
: [...quest["preRequisites:11"]]; // Copy if Changing
|
|
|
|
let rightOrder = true;
|
|
let prev: number = -1; // Smallest ID is 0
|
|
for (let i = 0; i < oldPrerequisites.length; i++) {
|
|
const pre = oldPrerequisites[i];
|
|
if (prev < pre) {
|
|
prev = pre;
|
|
continue;
|
|
}
|
|
if (prev === pre) {
|
|
if (shouldCheck)
|
|
throw new Error(
|
|
`Duplicate Prerequisites in Quest with ID ${foundID}!`,
|
|
);
|
|
logWarn(
|
|
`Removing Duplicate Prerequisite in Quest with ID ${foundID}...`,
|
|
);
|
|
quest["preRequisites:11"].splice(i, 1);
|
|
}
|
|
rightOrder = false;
|
|
break;
|
|
}
|
|
|
|
// Sort Prerequisites if Needed
|
|
if (!rightOrder) {
|
|
if (shouldCheck)
|
|
throw new Error(
|
|
`Prerequisites in Quest with ID ${foundID} is in the Wrong Order!`,
|
|
);
|
|
logWarn(`Sorting Prerequisites in Quest with ID ${foundID}...`);
|
|
|
|
const types = quest["preRequisiteTypes:7"];
|
|
if (!types) quest["preRequisites:11"].sort((a, b) => a - b);
|
|
else {
|
|
const preRequisites = new Map<number, number>();
|
|
quest["preRequisites:11"].forEach((pre, index) =>
|
|
preRequisites.set(pre, types[index]),
|
|
);
|
|
|
|
quest["preRequisites:11"].sort((a, b) => a - b);
|
|
for (let i = 0; i < quest["preRequisites:11"].length; i++) {
|
|
types[i] = preRequisites.get(quest["preRequisites:11"][i]) ?? 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for Rewards that have Nomicoins
|
|
if (isExpert) stripRewards(quest, isExpert, true);
|
|
|
|
if (!shouldCheck) newQB[`${foundID}:10`] = quest;
|
|
}
|
|
|
|
// Check for Redundant Formatting in Quest Lines
|
|
logInfo("Checking Quest Lines...");
|
|
for (const lineKey of Object.keys(qb["questLines:9"])) {
|
|
const line = qb["questLines:9"][lineKey];
|
|
line["properties:10"]["betterquesting:10"]["name:8"] =
|
|
stripOrThrowExcessFormatting(
|
|
shouldCheck,
|
|
line["properties:10"]["betterquesting:10"]["name:8"],
|
|
line["lineID:3"],
|
|
"Quest Line",
|
|
"Name",
|
|
);
|
|
line["properties:10"]["betterquesting:10"]["desc:8"] =
|
|
stripOrThrowExcessFormatting(
|
|
shouldCheck,
|
|
line["properties:10"]["betterquesting:10"]["desc:8"],
|
|
line["lineID:3"],
|
|
"Quest Line",
|
|
"Description",
|
|
);
|
|
}
|
|
if (!shouldCheck) qb["questDatabase:9"] = newQB;
|
|
|
|
logInfo("Checking Properties...");
|
|
// Check Edit Mode
|
|
if (qb["questSettings:10"]["betterquesting:10"]["editmode:1"] !== 0) {
|
|
if (shouldCheck) throw new Error("Edit Mode is On!");
|
|
logWarn("Turning off Edit Mode...");
|
|
qb["questSettings:10"]["betterquesting:10"]["editmode:1"] = 0;
|
|
}
|
|
}
|
|
|
|
function stripOrThrowExcessFormatting(
|
|
shouldCheck: boolean,
|
|
value: string,
|
|
id: number,
|
|
name: string,
|
|
key: string,
|
|
): string {
|
|
if (!value.includes("§")) return value;
|
|
|
|
let builder: string[] = [];
|
|
|
|
for (let i = 0; i < value.length; i++) {
|
|
const char = value.charAt(i);
|
|
|
|
if (builder.at(-1) === "§") {
|
|
if (char === "f") {
|
|
if (shouldCheck)
|
|
throw new Error(
|
|
`${name} with ID ${id} at ${key} has Formatting Code 'f'!`,
|
|
);
|
|
logWarn(
|
|
`Replacing Formatting Code 'f' with 'r' in ${name} with ID ${id} at ${key}...`,
|
|
);
|
|
builder.push("r");
|
|
continue;
|
|
}
|
|
|
|
if (!isAvailableForFormatting.test(char)) {
|
|
if (shouldCheck)
|
|
throw new Error(
|
|
`${name} with ID ${id} at ${key} has Lone Formatting Signal!`,
|
|
);
|
|
|
|
logWarn(
|
|
`Removing Lone Formatting Signal in ${name} with ID ${id} at ${key}...`,
|
|
);
|
|
|
|
// Remove Last Element
|
|
builder = builder.slice(0, -1);
|
|
continue;
|
|
}
|
|
|
|
// Start of String, Remove Formatting is NOT Needed
|
|
if (builder.length === 1 && char === "r") {
|
|
if (shouldCheck)
|
|
throw new Error(
|
|
`${name} with ID ${id} at ${key} has Redundant Formatting!`,
|
|
);
|
|
|
|
logWarn(
|
|
`Removing Redundant Formatting from ${name} with ID ${id} at ${key}...`,
|
|
);
|
|
|
|
// Remove Previous
|
|
builder = [];
|
|
continue;
|
|
}
|
|
builder.push(char);
|
|
continue;
|
|
}
|
|
|
|
if (char === "§") {
|
|
// If two characters before was not § (if builder length < 2, `.at` returns undefined)
|
|
if (builder.at(-2) !== "§") {
|
|
builder.push(char);
|
|
continue;
|
|
}
|
|
|
|
if (shouldCheck)
|
|
throw new Error(
|
|
`${name} with ID ${id} at ${key} has Redundant Formatting!`,
|
|
);
|
|
|
|
logWarn(
|
|
`Removing Redundant Formatting from ${name} with ID ${id} at ${key}...`,
|
|
);
|
|
|
|
// Remove Previous
|
|
builder = builder.slice(0, -2);
|
|
}
|
|
|
|
builder.push(char);
|
|
}
|
|
return builder.join("");
|
|
}
|