[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>
729 lines
19 KiB
TypeScript
729 lines
19 KiB
TypeScript
import { Icon, Quest, QuestBook, QuestVisibility } from "#types/bqQuestBook.ts";
|
|
import { diff } from "just-diff";
|
|
import {
|
|
Changed,
|
|
YesIgnoreNo,
|
|
QuestChange,
|
|
Replacements,
|
|
SavedPorter,
|
|
SpecialModifierHandler,
|
|
} from "#types/actionQBTypes.ts";
|
|
import upath from "upath";
|
|
import fs from "fs";
|
|
import PortQBData from "./questPorting/portQBData.ts";
|
|
import { input, select } from "@inquirer/prompts";
|
|
import {
|
|
configFolder,
|
|
configOverridesFolder,
|
|
rootDirectory,
|
|
storageFolder,
|
|
} from "#globals";
|
|
import logInfo, { logError, logWarn } from "#utils/log.ts";
|
|
import colors from "colors";
|
|
import { getUniqueToArray } from "#utils/util.ts";
|
|
import sortKeys from "sort-keys";
|
|
import lodash from "lodash";
|
|
|
|
let data: PortQBData;
|
|
|
|
export const emptyQuestName = "Gap";
|
|
export const emptyQuestDescription =
|
|
"Unused Gap Quest. Prevents Overriding Of IDs.";
|
|
export const emptyQuestVisibility: QuestVisibility = "HIDDEN";
|
|
export const emptyQuestIconId = "minecraft:air";
|
|
export const emptyQuestTaskId = "bq_standard:checkbox";
|
|
|
|
export const emptyQuest: Quest = {
|
|
"preRequisites:11": [],
|
|
"properties:10": {
|
|
"betterquesting:10": {
|
|
"autoclaim:1": 0,
|
|
"desc:8": "Unused Gap Quest. Prevents Overriding Of IDs.",
|
|
"globalshare:1": 0,
|
|
"icon:10": {
|
|
"Count:3": 0,
|
|
"Damage:2": 0,
|
|
"OreDict:8": "",
|
|
"id:8": "minecraft:air",
|
|
},
|
|
"ignoresview:1": 0,
|
|
"ismain:1": 0,
|
|
"issilent:1": 1,
|
|
"lockedprogress:1": 0,
|
|
"name:8": "Gap",
|
|
"partysinglereward:1": 0,
|
|
"questlogic:8": "AND",
|
|
"repeat_relative:1": 1,
|
|
"repeattime:3": -1,
|
|
"simultaneous:1": 0,
|
|
"snd_complete:8": "minecraft:entity.player.levelup",
|
|
"snd_update:8": "minecraft:entity.player.levelup",
|
|
"tasklogic:8": "AND",
|
|
"visibility:8": "HIDDEN",
|
|
},
|
|
},
|
|
"questID:3": 0,
|
|
"rewards:9": {},
|
|
"tasks:9": {
|
|
"0:10": {
|
|
"index:3": 0,
|
|
"taskID:8": "bq_standard:checkbox",
|
|
},
|
|
},
|
|
};
|
|
|
|
export const defaultPorter = {
|
|
savedQuestMap: [],
|
|
ignoreQuestsNormal: [],
|
|
ignoreQuestsExpert: [],
|
|
alwaysAskQuestsNormal: [],
|
|
alwaysAskQuestsExpert: [],
|
|
} as SavedPorter;
|
|
|
|
/* Paths */
|
|
export const cfgNormalPath = upath.join(
|
|
configFolder,
|
|
"betterquesting",
|
|
"DefaultQuests.json",
|
|
);
|
|
export const cfgExpertPath = upath.join(
|
|
configFolder,
|
|
"betterquesting",
|
|
"saved_quests",
|
|
"ExpertQuests.json",
|
|
);
|
|
|
|
export const cfgOverrideNormalPath = upath.join(
|
|
configOverridesFolder,
|
|
"normal",
|
|
"betterquesting",
|
|
"DefaultQuests.json",
|
|
);
|
|
export const cfgOverrideExpertPath = upath.join(
|
|
configOverridesFolder,
|
|
"expert",
|
|
"betterquesting",
|
|
"DefaultQuests.json",
|
|
);
|
|
|
|
export const savedQuestPorter = upath.join(storageFolder, "savedQBPorter.json");
|
|
|
|
const nomiCoinMatcher = /^nomilabs:nomicoin[0-9]*$/;
|
|
|
|
export function setupUtils(dataIn: PortQBData): void {
|
|
data = dataIn;
|
|
}
|
|
|
|
export function removeFormatting(input: string): string {
|
|
if (!input.includes("§")) return input;
|
|
|
|
const builder: string[] = [];
|
|
for (let i = 0; i < input.length; i++) {
|
|
const char = input.charAt(i);
|
|
if (char === "§") {
|
|
i++; // Skip Next Character
|
|
continue;
|
|
}
|
|
builder.push(char);
|
|
}
|
|
return builder.join("");
|
|
}
|
|
|
|
export function stripRewards(quest: Quest, shouldCheck = false, log = false) {
|
|
for (const rewardKey of Object.keys(quest["rewards:9"])) {
|
|
const reward = quest["rewards:9"][rewardKey];
|
|
if (
|
|
!reward ||
|
|
reward["rewardID:8"] !== "bq_standard:item" ||
|
|
!reward["rewards:9"]
|
|
)
|
|
continue;
|
|
|
|
for (const itemKey of Object.keys(reward["rewards:9"])) {
|
|
const item: Icon = reward["rewards:9"][itemKey];
|
|
if (item && item["id:8"] && nomiCoinMatcher.test(item["id:8"])) {
|
|
if (shouldCheck)
|
|
throw new Error(
|
|
`Expert Quest with ID ${quest["questID:3"]} has Nomi Coin Reward!`,
|
|
);
|
|
if (log)
|
|
logWarn(
|
|
`Removing Nomi Coin Reward for Expert Quest with ID ${quest["questID:3"]}...`,
|
|
);
|
|
delete reward["rewards:9"][itemKey];
|
|
}
|
|
}
|
|
if (Object.keys(reward["rewards:9"]).length === 0)
|
|
delete quest["rewards:9"][rewardKey];
|
|
else quest["rewards:9"][rewardKey] = reward;
|
|
}
|
|
}
|
|
|
|
let cachedQuestByName: Map<string, Quest>;
|
|
|
|
/**
|
|
* Finds the corresponding quest on the qb to change, using the data cache. If object is not found in the data cache, asks the client questions to determine the quest.
|
|
* @param sourceId The id of the quest on the source qb.
|
|
* @param sourceQuest The Source Quest, if it is not just `data.currentIDsToQuests.get(sourceId)`.
|
|
* @return Returns the quest that is found, or undefined if the quest should be skipped.
|
|
*/
|
|
export async function findQuest(
|
|
sourceId: number,
|
|
sourceQuest?: Quest,
|
|
): Promise<Quest | undefined> {
|
|
if (data.ignoreQuests.has(sourceId)) return undefined;
|
|
if (data.foundQuests.has(sourceId)) return data.foundQuests.get(sourceId);
|
|
|
|
// If no source quest, default behaviour
|
|
if (!sourceQuest) sourceQuest = data.currentIDsToQuests.get(sourceId);
|
|
// If still no source quest, throw
|
|
if (!sourceQuest)
|
|
throw new Error(
|
|
`Request Find Quest for id ${sourceId}, which is not in IDs to Quests!`,
|
|
);
|
|
|
|
logInfo(
|
|
colors.magenta(
|
|
`Finding Corresponding Quest for Source Quest with ID ${sourceId} and Name ${name(sourceQuest)}...`,
|
|
),
|
|
);
|
|
|
|
// Try Find by ID
|
|
const questById = data.toChangeIDsToQuests.get(sourceId);
|
|
if (questById && !isEmptyQuest(questById)) {
|
|
// Ask the client if the corresponding id on the corresponding qb is correct
|
|
const correctQuestById = await isRightQuest(
|
|
`Does the Corresponding Quest have ID ${sourceId} and Name ${name(questById)}?`,
|
|
);
|
|
if (correctQuestById === "YES") {
|
|
logInfo("Using Quest...");
|
|
await finalizeFoundQuest(sourceId, () =>
|
|
data.foundQuests.set(sourceId, questById),
|
|
);
|
|
return questById;
|
|
}
|
|
if (correctQuestById === "IGNORE") {
|
|
logInfo("Ignoring...");
|
|
await finalizeFoundQuest(sourceId, () => data.ignoreQuests.add(sourceId));
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
// Generate Quest By Name if Needed
|
|
if (!cachedQuestByName) {
|
|
cachedQuestByName = new Map<string, Quest>();
|
|
[...data.toChangeIDsToQuests.values()].forEach((item) =>
|
|
cachedQuestByName.set(removeFormatting(name(item)), item),
|
|
);
|
|
}
|
|
|
|
// Try Find by Name
|
|
const removeFormattedName = removeFormatting(name(sourceQuest));
|
|
const questByName = cachedQuestByName.get(removeFormattedName);
|
|
if (questByName && !isEmptyQuest(questByName)) {
|
|
// Ask the client if the corresponding id on the corresponding qb is correct
|
|
const correctQuestByName = await isRightQuest(
|
|
`Does the Corresponding Quest have ID ${id(questByName)} and Name ${name(questByName)}?`,
|
|
);
|
|
if (correctQuestByName === "YES") {
|
|
logInfo("Using Quest...");
|
|
await finalizeFoundQuest(sourceId, () =>
|
|
data.foundQuests.set(sourceId, questByName),
|
|
);
|
|
return questByName;
|
|
}
|
|
if (correctQuestByName === "IGNORE") {
|
|
logInfo("Cancelling...");
|
|
await finalizeFoundQuest(sourceId, () => data.ignoreQuests.add(sourceId));
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
// Finally, ask for the specific ID
|
|
let foundBySpecificID: YesIgnoreNo = "NO";
|
|
let questBySpecificID: Quest | undefined = undefined;
|
|
while (foundBySpecificID === "NO") {
|
|
const specID = parseInt(
|
|
await input({
|
|
message:
|
|
"Please Provide a Specific Quest ID to be used as the Corresponding Quest. Enter -1 to Skip/Cancel this Quest!",
|
|
validate: (value) => {
|
|
const numValue = parseInt(value);
|
|
if (numValue === -1) return true; // Allow Cancelling
|
|
if (isNaN(numValue) || numValue < 0) {
|
|
return "Please Enter a Number Value >= 0!";
|
|
}
|
|
return true;
|
|
},
|
|
}),
|
|
);
|
|
if (specID === -1) {
|
|
logInfo("Cancelling...");
|
|
foundBySpecificID = "IGNORE";
|
|
break;
|
|
}
|
|
questBySpecificID = data.toChangeIDsToQuests.get(specID);
|
|
if (!questBySpecificID) {
|
|
logError(`${specID} is not a Quest ID in the Quest Book being Changed!`);
|
|
continue;
|
|
}
|
|
if (isEmptyQuest(questBySpecificID)) {
|
|
logError(
|
|
`${specID} is a Empty Quest! Enter -1 to Skip/Cancel this Quest, not the ID of an Empty Quest!`,
|
|
);
|
|
continue;
|
|
}
|
|
foundBySpecificID = await isRightQuest(
|
|
`Are You Sure you Would Like to use Quest with ID ${specID} and Name ${name(questBySpecificID)}?`,
|
|
);
|
|
}
|
|
|
|
if (foundBySpecificID === "IGNORE" || !questBySpecificID)
|
|
await finalizeFoundQuest(sourceId, () => data.ignoreQuests.add(sourceId));
|
|
else
|
|
await finalizeFoundQuest(sourceId, () =>
|
|
data.foundQuests.set(sourceId, questBySpecificID),
|
|
);
|
|
|
|
return questBySpecificID;
|
|
}
|
|
|
|
async function finalizeFoundQuest(sourceID: number, addToList: () => void) {
|
|
if (data.alwaysAskQuests.has(sourceID)) {
|
|
logInfo(
|
|
"This Quest is set to Ask Each Time. If this is not Desirable, Change this in the Saved Porter!",
|
|
);
|
|
return;
|
|
}
|
|
const askEachTime = await booleanSelect(
|
|
"Should we Ask the Corresponding ID for this Quest Every Time?",
|
|
"Yes",
|
|
"No",
|
|
false,
|
|
);
|
|
if (askEachTime) data.alwaysAskQuests.add(sourceID);
|
|
else addToList();
|
|
}
|
|
|
|
async function isRightQuest(message: string): Promise<YesIgnoreNo> {
|
|
return (await select({
|
|
message: message,
|
|
choices: [
|
|
{
|
|
name: "Yes",
|
|
value: "YES",
|
|
},
|
|
{
|
|
name: "No",
|
|
value: "NO",
|
|
},
|
|
{
|
|
name: "Skip/Ignore this Quest",
|
|
value: "IGNORE",
|
|
},
|
|
],
|
|
})) as YesIgnoreNo;
|
|
}
|
|
|
|
export async function booleanSelect(
|
|
message: string,
|
|
trueMsg = "Yes",
|
|
falseMsg = "No",
|
|
defaultTo = true,
|
|
): Promise<boolean> {
|
|
return (
|
|
await select({
|
|
message: message,
|
|
choices: [
|
|
{
|
|
name: trueMsg,
|
|
value: true,
|
|
},
|
|
{
|
|
name: falseMsg,
|
|
value: false,
|
|
},
|
|
],
|
|
default: defaultTo,
|
|
})
|
|
).valueOf();
|
|
}
|
|
|
|
export function id(quest: Quest): number {
|
|
return quest["questID:3"];
|
|
}
|
|
|
|
export function name(quest: Quest): string {
|
|
return quest["properties:10"]["betterquesting:10"]["name:8"];
|
|
}
|
|
|
|
export function dependencies(quest: Quest): number[] {
|
|
return quest["preRequisites:11"];
|
|
}
|
|
|
|
/**
|
|
* Paths to Ignore in Quest Change Calculation.
|
|
* Prerequisites handled separately.
|
|
* Prerequisites Types handled by Prerequisites.
|
|
* Rewards not ported (too different across Quest Books)
|
|
**/
|
|
const ignoreRootPaths = new Set<string>([
|
|
"preRequisites:11",
|
|
"preRequisiteTypes:7",
|
|
"rewards:9",
|
|
]);
|
|
|
|
/**
|
|
* Special Handling in Modified. The Path that is added to the changes list should have -CUSTOM appended to the end, to distinguish this from other changes.
|
|
*/
|
|
const specialModifierHandlers: SpecialModifierHandler[] = [
|
|
(old, current, changes) => {
|
|
const diff = getUniqueToArray(
|
|
old["preRequisites:11"],
|
|
current["preRequisites:11"],
|
|
);
|
|
// Unique to old array: Removed
|
|
if (diff.arr1Unique.length > 0 || diff.arr2Unique.length > 0) {
|
|
changes.push({
|
|
path: ["preRequisites-CUSTOM"],
|
|
op: "replace",
|
|
value: diff,
|
|
});
|
|
}
|
|
},
|
|
];
|
|
|
|
export function getChanged(
|
|
currentQuests: Quest[],
|
|
oldQuests: Quest[],
|
|
): Changed {
|
|
// i is current iter, j is old iter
|
|
let i = 0;
|
|
let j = 0;
|
|
const changed: Changed = { added: [], modified: [], removed: [] };
|
|
while (i < currentQuests.length && j < oldQuests.length) {
|
|
const currentQuestID = id(currentQuests[i]);
|
|
const oldQuestID = id(oldQuests[j]);
|
|
if (currentQuestID == oldQuestID) {
|
|
let questDiff = diff(oldQuests[j], currentQuests[i]) as QuestChange[];
|
|
if (questDiff.length !== 0) {
|
|
questDiff = questDiff.filter(
|
|
(change) =>
|
|
typeof change.path[0] !== "string" ||
|
|
!ignoreRootPaths.has(change.path[0]),
|
|
);
|
|
for (const handler of specialModifierHandlers) {
|
|
handler(oldQuests[j], currentQuests[i], questDiff);
|
|
}
|
|
if (isEmptyQuest(currentQuests[i])) changed.removed.push(oldQuests[j]);
|
|
else
|
|
changed.modified.push({
|
|
currentQuest: currentQuests[i],
|
|
oldQuest: oldQuests[j],
|
|
change: questDiff,
|
|
});
|
|
}
|
|
i++;
|
|
j++;
|
|
continue;
|
|
}
|
|
if (!data.currentIDsToQuests.has(oldQuestID)) {
|
|
logWarn(
|
|
`A quest has been removed directly! (ID ${id(oldQuests[j])}, Name '${name(
|
|
oldQuests[j],
|
|
)}') This is NOT recommended! IDs may overlay in the future! Replace quests with empty ones instead!`,
|
|
);
|
|
changed.removed.push(oldQuests[j]);
|
|
j++;
|
|
continue;
|
|
}
|
|
changed.added.push(currentQuests[i]);
|
|
i++;
|
|
}
|
|
if (i < currentQuests.length) {
|
|
changed.added.push(...currentQuests.slice(i));
|
|
} else if (j < currentQuests.length) {
|
|
changed.removed.push(...currentQuests.slice(i));
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
export function isEmptyQuest(quest: Quest): boolean {
|
|
return (
|
|
questIsSilent(quest) &&
|
|
emptyName(quest) &&
|
|
emptyDesc(quest) &&
|
|
emptyVisibility(quest) &&
|
|
emptyIcon(quest) &&
|
|
questHasNoRewards(quest) &&
|
|
emptyTasks(quest)
|
|
);
|
|
}
|
|
|
|
function emptyName(quest: Quest): boolean {
|
|
const questName = name(quest);
|
|
return questName === emptyQuestName || !questName;
|
|
}
|
|
|
|
function emptyDesc(quest: Quest): boolean {
|
|
const questDesc = quest["properties:10"]["betterquesting:10"]["desc:8"];
|
|
return questDesc === emptyQuestDescription || !questDesc;
|
|
}
|
|
|
|
function emptyVisibility(quest: Quest): boolean {
|
|
const questVisibility =
|
|
quest["properties:10"]["betterquesting:10"]["visibility:8"];
|
|
return questVisibility === emptyQuestVisibility;
|
|
}
|
|
|
|
function emptyIcon(quest: Quest): boolean {
|
|
const questIcon = quest["properties:10"]["betterquesting:10"]["icon:10"];
|
|
return (
|
|
!questIcon || questIcon["id:8"] === emptyQuestIconId || !questIcon["id:8"]
|
|
);
|
|
}
|
|
|
|
function questIsSilent(quest: Quest): boolean {
|
|
return quest["properties:10"]["betterquesting:10"]["issilent:1"] !== 0;
|
|
}
|
|
|
|
function questHasNoRewards(quest: Quest): boolean {
|
|
return !quest["rewards:9"] || Object.keys(quest["rewards:9"]).length === 0;
|
|
}
|
|
|
|
function emptyTasks(quest: Quest): boolean {
|
|
return (
|
|
!quest["tasks:9"] ||
|
|
Object.keys(quest["tasks:9"]).length === 0 ||
|
|
(Object.keys(quest["tasks:9"]).length === 1 &&
|
|
(!quest["tasks:9"]["0:10"] ||
|
|
!quest["tasks:9"]["0:10"]["taskID:8"] ||
|
|
quest["tasks:9"]["0:10"]["taskID:8"] === emptyQuestTaskId))
|
|
);
|
|
}
|
|
|
|
export function stringifyQB(qb: QuestBook): string {
|
|
// Formatting Changes
|
|
const replacements: Replacements[] = [
|
|
{
|
|
search: /</g,
|
|
replacement: "\\u003c",
|
|
},
|
|
{
|
|
search: />/g,
|
|
replacement: "\\u003e",
|
|
},
|
|
{
|
|
search: /&/g,
|
|
replacement: "\\u0026",
|
|
},
|
|
{
|
|
search: /=/g,
|
|
replacement: "\\u003d",
|
|
},
|
|
{
|
|
search: /'/g,
|
|
replacement: "\\u0027",
|
|
},
|
|
];
|
|
qb = sortKeysRecursiveIgnoreArray(qb, (key1, key2): number => {
|
|
const defaultVal = key2 < key1 ? 1 : -1;
|
|
|
|
if (!key1.includes(":") || !key2.includes(":")) return defaultVal;
|
|
|
|
const num1 = Number.parseInt(key1.split(":")[0]);
|
|
const num2 = Number.parseInt(key2.split(":")[0]);
|
|
|
|
if (Number.isNaN(num1) || Number.isNaN(num2)) return defaultVal;
|
|
return num1 - num2;
|
|
});
|
|
let parsed = JSON.stringify(qb, null, 2).replace(
|
|
/("[a-zA-Z_]+:[56]":\s)(-?[0-9]+)(,?)$/gm,
|
|
"$1$2.0$3",
|
|
); // Add '.0' to any Float/Double Values that are Integers
|
|
|
|
for (const replacement of replacements) {
|
|
parsed = parsed.replace(replacement.search, replacement.replacement);
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
/**
|
|
* Use our own, instead of sortKeysRecursive, to ignore sorting of arrays.
|
|
*/
|
|
function sortKeysRecursiveIgnoreArray<T extends object>(
|
|
object: T,
|
|
compare: (a: string, b: string) => number,
|
|
): T {
|
|
const result = sortKeys(object as Record<string, unknown>, { compare }) as T;
|
|
|
|
// We can modify results, Object.Keys returns a static array
|
|
Object.keys(result).forEach(function (key) {
|
|
const current = lodash.get(result, key);
|
|
if (current) {
|
|
if (typeof current === "object") {
|
|
lodash.set(result, key, sortKeys(current, { compare }));
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
export async function save(toSave: QuestBook): Promise<void> {
|
|
const save = await booleanSelect("Would you like to Save Changes?");
|
|
if (!save) return;
|
|
|
|
const shouldSavePorter = await booleanSelect(
|
|
"Would you like to Save the Quest Porter?",
|
|
);
|
|
if (shouldSavePorter) await savePorter();
|
|
|
|
const parsed = stringifyQB(toSave);
|
|
|
|
for (const path of data.outputPaths) {
|
|
await fs.promises.writeFile(upath.join(rootDirectory, path), parsed);
|
|
}
|
|
|
|
logInfo(`Saved Files: ${data.outputPaths.join(", ")}!`);
|
|
logInfo(
|
|
colors.green(
|
|
colors.bold(
|
|
"Remember to import the JSON Files into your Instance to format them!",
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
async function savePorter() {
|
|
logInfo("Saving Porter...");
|
|
|
|
let porter: SavedPorter;
|
|
|
|
// Keys of Porter Are Checked on Import
|
|
// Porter Definitely Has a Value for Each Key
|
|
if (!data.savedPorter) {
|
|
if (fs.existsSync(savedQuestPorter)) {
|
|
porter = await readFromPorter(false);
|
|
} else porter = defaultPorter;
|
|
} else porter = data.savedPorter;
|
|
|
|
// Save Map
|
|
porter.savedQuestMap = [];
|
|
for (const sourceID of data.foundQuests.keys()) {
|
|
const sourceQuest = data.foundQuests.get(sourceID);
|
|
if (!sourceQuest) continue;
|
|
const targetID = id(sourceQuest);
|
|
|
|
let normalID: number, expertID: number;
|
|
switch (data.type) {
|
|
case "NORMAL":
|
|
normalID = sourceID;
|
|
expertID = targetID;
|
|
break;
|
|
case "EXPERT":
|
|
normalID = targetID;
|
|
expertID = sourceID;
|
|
break;
|
|
}
|
|
|
|
porter.savedQuestMap.push({
|
|
normal: normalID,
|
|
expert: expertID,
|
|
});
|
|
|
|
porter.savedQuestMap.sort((a, b) => a.normal - b.normal);
|
|
}
|
|
|
|
// Save Ignore
|
|
const ignoreArr = [...data.ignoreQuests];
|
|
if (data.type === "NORMAL") porter.ignoreQuestsNormal = ignoreArr;
|
|
else porter.ignoreQuestsExpert = ignoreArr;
|
|
|
|
// Save Always Ask
|
|
const alwaysAskArr = [...data.alwaysAskQuests];
|
|
if (data.type === "NORMAL") porter.alwaysAskQuestsNormal = alwaysAskArr;
|
|
else porter.alwaysAskQuestsExpert = alwaysAskArr;
|
|
|
|
// Write Porter to File
|
|
return fs.promises.writeFile(
|
|
savedQuestPorter,
|
|
JSON.stringify(porter, null, 2),
|
|
);
|
|
}
|
|
|
|
export async function readFromPorter(
|
|
replaceExisting: boolean,
|
|
): Promise<SavedPorter> {
|
|
const savedPorter = JSON.parse(
|
|
await fs.promises.readFile(savedQuestPorter, "utf-8"),
|
|
) as SavedPorter;
|
|
|
|
// Make Sure Porter has Every Key
|
|
for (const key of Object.keys(defaultPorter)) {
|
|
// @ts-expect-error Cannot use String as Key
|
|
if (!savedPorter[key]) savedPorter[key] = defaultPorter[key];
|
|
}
|
|
|
|
// Add in Map
|
|
if (replaceExisting) data.foundQuests.clear();
|
|
for (const savedQuestPath of savedPorter.savedQuestMap) {
|
|
if (
|
|
Number.isNaN(savedQuestPath.normal) ||
|
|
Number.isNaN(savedQuestPath.expert)
|
|
)
|
|
throw new Error("ID must be a number!");
|
|
|
|
let sourceID: number, targetID: number;
|
|
switch (data.type) {
|
|
case "NORMAL":
|
|
sourceID = savedQuestPath.normal;
|
|
targetID = savedQuestPath.expert;
|
|
break;
|
|
case "EXPERT":
|
|
sourceID = savedQuestPath.expert;
|
|
targetID = savedQuestPath.normal;
|
|
break;
|
|
}
|
|
|
|
if (!data.currentIDsToQuests.has(sourceID))
|
|
throw new Error("ID must be a valid quest!");
|
|
const targetQuest = data.toChangeIDsToQuests.get(targetID);
|
|
if (!targetQuest) throw new Error("ID must be a valid quest!");
|
|
|
|
if (!data.foundQuests.has(sourceID))
|
|
data.foundQuests.set(sourceID, targetQuest);
|
|
}
|
|
|
|
// Ignore & Always Ask
|
|
data.ignoreQuests = addToOrReplaceSet(
|
|
replaceExisting,
|
|
data.type === "NORMAL"
|
|
? savedPorter.ignoreQuestsNormal
|
|
: savedPorter.ignoreQuestsExpert,
|
|
data.ignoreQuests,
|
|
);
|
|
data.alwaysAskQuests = addToOrReplaceSet(
|
|
replaceExisting,
|
|
data.type === "NORMAL"
|
|
? savedPorter.alwaysAskQuestsNormal
|
|
: savedPorter.alwaysAskQuestsExpert,
|
|
data.alwaysAskQuests,
|
|
);
|
|
|
|
data.savedPorter = savedPorter;
|
|
return savedPorter;
|
|
}
|
|
|
|
function addToOrReplaceSet<T>(
|
|
replaceExisting: boolean,
|
|
array: T[],
|
|
set: Set<T>,
|
|
): Set<T> {
|
|
if (replaceExisting) return new Set<T>(array);
|
|
|
|
array.forEach((value) => set.add(value));
|
|
return set;
|
|
}
|