tracer4b b526677126 Questbook Update + Buildscript Improvements (#681)
[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>
2024-05-14 21:57:00 +10:00

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("");
}