diff --git a/js/003-data/gameVariableData.js b/js/003-data/gameVariableData.js index 2746b34ae4bf16e857610de827bede78fc20b8b3..2baa9a734b1055a93b0184970bbdd2258a1c41aa 100644 --- a/js/003-data/gameVariableData.js +++ b/js/003-data/gameVariableData.js @@ -104,6 +104,8 @@ App.Data.defaultGameStateVariables = { debugMode: 0, debugModeCustomFunction: 0, debugModeEventSelection: 0, + /** @type {FC.Bool} If true then the patching system will report any changes that happen */ + reportVerificationChanges: 0, difficultySwitch: 0, disableLisping: 0, displayAssignments: 1, diff --git a/src/data/patches/patch.js b/src/data/patches/patch.js index 12995ef6a75330d24787e27d2196257199f7c4df..3abc2d7c05925f0b6ac86e5d6a472e06a3214596 100644 --- a/src/data/patches/patch.js +++ b/src/data/patches/patch.js @@ -1,4 +1,4 @@ -// cSpell:words TLDR +// cSpell:words divs /** * Need to know how to apply a patch? Go to the line with `====== How to apply a patch ======`. @@ -76,6 +76,7 @@ */ // TODO:@franklygeorge add a way to force apply a list of patch versions and types in order +// TODO:@franklygeorge move the "Open All Reports" code from App.EndWeek.slaveAssignmentReport to App.UI.DOM and then utilize it in the patch system /** * registers a patch to the database @@ -123,216 +124,297 @@ App.Patch.log = (message, type="info") => { * @returns {DocumentFragment} a document fragment with the results of the patching */ App.Patch.applyAll = () => { - // console.time('Patching All'); - // TODO:@franklygeorge on error, provide a human parsable patch log with valid markdown formatting for directly pasteing into a gitlab issue - // TODO:@franklygeorge include a save in the above in text form (do we need to change active passage?) - // TODO:@franklygeorge put each patches info in a collapsed div with a number for how many times `App.Patch.log` was called. `Log (call times)` - // TODO:@franklygeorge remove said collapsed div if it has no content - const f = document.createDocumentFragment(); - let header = App.UI.DOM.appendNewElement("div", f); - - const patchVersions = Object.keys(App.Patch.Patches).sort(); - - // check that the last patch (largest releaseID) is equal to App.Version.release - if (Number(patchVersions.at(-1)) > App.Version.release) { - throw new Error(`Patch exists for release ${patchVersions.at(-1)}, but App.Version.release is at ${App.Version.release}! Did you forget to increment release in '/src/002-config/fc-version.js'?`); - } else if (!(App.Version.release in App.Patch.Patches)) { + // create a loading screen + App.Patch.Utils.loadingScreen.start(); + + // create our document fragment and our main divs + const frag = document.createDocumentFragment(); + const head = App.UI.DOM.appendNewElement("div", frag); + head.id = "patch-header"; + const cDiv = App.UI.DOM.appendNewElement("div", frag); + cDiv.id = "patch-content"; + + // get all available patches in reverse order so that when we call .pop we will get the smallest patch version + let patchVersions = Object.keys(App.Patch.Patches).sort((a, b)=> +b - +a); + + // check that the first patch in patchVersions (largest releaseID) is equal to App.Version.release + if (Number(patchVersions[0]) > App.Version.release) { + throw new Error(`Patch exists for release ${patchVersions[0]}, but App.Version.release is at ${App.Version.release}! Did you forget to increment release in '/src/002-config/fc-version.js'?`); + } else if (Number(patchVersions[0]) < App.Version.release) { throw new Error(`No patch exists for releaseID ${App.Version.release}! Did you make a patch? Instructions are in '/src/data/patches/patch.js'.`); } - /** - * @typedef {object} App.Patch.applyAllWombsObj - * @property {string} identifier - * @property {FC.HumanState} actor - * @property {HTMLDivElement} div - */ - - let i; - /** @type {number} */ - let patchID; - /** @type {App.Patch.applyAllWombsObj[]} */ - let wombs = []; - - try { - for (const ID in patchVersions) { - patchID = Number(patchVersions[ID]); - App.Patch.Utils.current.patch = patchID; - if (patchID <= V.releaseID) { continue; } - console.time(`Patch ${patchID}`); - - App.UI.DOM.appendNewElement("h3", f, `Applying Patch ${patchID}`); - // console.log(`Applying Patch ${patchID}`); - - App.Patch.Utils.current.div = App.UI.DOM.appendNewElement("div", f); - - // pre patch - // console.time(`Patch ${patchID} Pre`); - App.Patch.Utils.gameVariables("pre", App.Patch.Utils.current.div, patchID); // This should always be the first patch - // console.timeEnd(`Patch ${patchID} Pre`); - - // PlayerState - // console.time(`Patch ${patchID} PlayerState`); - App.Patch.Utils.playerState("V.PC", V.PC, "V.PC", App.Patch.Utils.current.div, patchID); - wombs.push({identifier: `V.PC`, actor: V.PC, div: App.Patch.Utils.current.div}); - // console.timeEnd(`Patch ${patchID} PlayerState`); - - // SlaveState - // console.time(`Patch ${patchID} SlaveState`); - for (i in V.slaves ?? []) { - App.Patch.Utils.slaveState( - `V.slaves[${i}]`, V.slaves[i], "V.slaves", App.Patch.Utils.current.div, patchID - ); - wombs.push({identifier: `V.slaves[${i}]`, actor: V.slaves[i], div: App.Patch.Utils.current.div}); - } + // remove all unneeded patch versions from the array + patchVersions = patchVersions.filter((patchID) => { return Number(patchID) > V.releaseID; }); + // get the amount of the patches we are going to apply for later use + const patchCount = patchVersions.length; - if (V.hostage) { - App.Patch.Utils.slaveState( - `V.hostage`, V.hostage, "V.hostage", App.Patch.Utils.current.div, patchID - ); - wombs.push({identifier: `V.hostage`, actor: V.hostage, div: App.Patch.Utils.current.div}); - } + // We use setTimeout here to give the App.Patch.applyAll() function time to return frag and SugarCube the time to change passages + // The execution flow skips this setTimeout call temporarily + setTimeout((() => { + // get the elements we created earlier + let f = document.getElementById("patch-content"); + let header = document.getElementById("patch-header"); + // this element was created by the App.Patch.Utils.loadingScreen.start() call + let message = document.getElementById("patch-message"); + + let verificationStart; - if (V.boomerangSlave) { - App.Patch.Utils.slaveState( - `V.boomerangSlave`, V.boomerangSlave, "V.boomerangSlave", - App.Patch.Utils.current.div, patchID - ); - wombs.push({identifier: `V.boomerangSlave`, actor: V.boomerangSlave, div: App.Patch.Utils.current.div}); + /** This is called by App.Verify.everything() when it is done */ + const finalize = () => { + if (getProp(V, "reportVerificationChanges", 0) === 0) { + App.Patch.Utils.current.div.append(App.UI.DOM.makeElement('div', "Change detection was skipped because it is disabled")); } - if (V.shelterSlave) { - App.Patch.Utils.slaveState( - `V.shelterSlave`, V.shelterSlave, "V.shelterSlave", - App.Patch.Utils.current.div, patchID - ); - wombs.push({identifier: `V.shelterSlave`, actor: V.shelterSlave, div: App.Patch.Utils.current.div}); + // report the time taken and the amount of content added to current div + const verificationTime = (new Date().getTime() - verificationStart).toLocaleString(); + const divLength = App.Patch.Utils.current.div.childNodes.length; + + f.append(App.UI.DOM.accordion( + `Cleanup and Verification (${divLength}) (${verificationTime}ms)`, + App.Patch.Utils.current.div, + (divLength === 0 || getProp(V, "useAccordion", 1) > 0) + )); + + // handle App.Verify.Utils.verificationError + if (App.Verify.Utils.verificationError) { + header.innerHTML += `<span class="error">Verification failed!</span>`; + console.error("Verification failed!"); + // @ts-ignore + State.restore(); // restore the state to before patching + } else { + if (patchCount !== 0) { + header.innerHTML += `<span class="green">${patchCount} patches applied! You are on release ${V.releaseID}</span>`; + } else { + header.innerHTML += `<span class="green">No patches needed to be applied! You are on release ${V.releaseID}</span>`; + } + App.UI.DOM.appendNewElement("h3", f, "Done!"); + console.log("Done!"); } - if (V.traitor) { - App.Patch.Utils.slaveState( - `V.traitor`, V.traitor, "V.traitor", App.Patch.Utils.current.div, patchID - ); - wombs.push({identifier: `V.traitor`, actor: V.traitor, div: App.Patch.Utils.current.div}); + header.innerHTML += `<br><span>See below for details</span><hr>`; + + // update the sidebar to remove any potential errors that were cause by being on an outdated save + App.Utils.scheduleSidebarRefresh(); + + // remove the loading screen so the user can see the results + App.Patch.Utils.loadingScreen.end(); + + App.Patch.Utils.current.div = undefined; + // this is where the execution actually stops for the patching system + }; + + /** + * This function is called later in the code. + * It does verification and final cleanup after the patching is done + * @returns {undefined} + */ + const verifyAndCleanup = () => { + // make a new div + App.Patch.Utils.current.div = App.UI.DOM.makeElement("div"); + + // Cleanup + console.log(`Cleanup`); + + V.NaNArray = findNaN(); // reset NaNArray + App.UI.SlaveSummary.settingsChanged(); // let slave summary know that settings may have changed + + // verification + console.log(`Verification`); + + App.Verify.Utils.verificationError = false; + try { + verificationStart = new Date().getTime(); + // does the actual verification + App.Verify.everything(App.Patch.Utils.current.div, finalize); + } catch (e) { + header.innerHTML += `<span class="error">Verification failed!</span>`; + console.error("Verification failed!"); + App.Patch.Utils.current.div.append(App.UI.DOM.formatException(e)); + // @ts-ignore + State.restore(); // restore the state to before patching + header.innerHTML += `<br><span>See below for details</span><hr>`; + // close the loading screen so that the user can see the error + App.Patch.Utils.loadingScreen.end(); + return; } - // console.timeEnd(`Patch ${patchID} SlaveState`); - - // TankSlaveState - // console.time(`Patch ${patchID} TankSlaveState`); - for (i in V.incubator.tanks ?? []) { - App.Patch.Utils.tankSlaveState( - `V.incubator.tanks[${i}]`, V.incubator.tanks[i], App.Patch.Utils.current.div, patchID - ); - wombs.push({ - identifier: `V.incubator.tanks[${i}]`, - actor: V.incubator.tanks[i], - div: App.Patch.Utils.current.div + }; + + const patchingStart = new Date().getTime(); + let i; + + /** + * This function is called later in the code. + * It applies a single patch. + * If there are more patches to apply it calls itself, if not then it calls verifyAndCleanup(). + * @returns {undefined} + */ + const applyPatch = () => { + const patchID = Number(patchVersions.pop()); + + /** @type {{identifier: string, actor: FC.HumanState, div: HTMLDivElement}[]} */ + const wombs = []; + + const patchStart = new Date().getTime(); + + // patch + try { + // make a new div + App.Patch.Utils.current.div = App.UI.DOM.makeElement("div"); + + // Pre patch; This should always be the first patch + App.Patch.Utils.gameVariables("pre", App.Patch.Utils.current.div, patchID); + + // PlayerState + App.Patch.Utils.playerState("V.PC", V.PC, "V.PC", App.Patch.Utils.current.div, patchID); + wombs.push({identifier: `V.PC`, actor: V.PC, div: App.Patch.Utils.current.div}); + + // SlaveState + for (i in V.slaves ?? []) { + App.Patch.Utils.slaveState( + `V.slaves[${i}]`, V.slaves[i], "V.slaves", App.Patch.Utils.current.div, patchID + ); + wombs.push({identifier: `V.slaves[${i}]`, actor: V.slaves[i], div: App.Patch.Utils.current.div}); + } + + if (V.hostage) { + App.Patch.Utils.slaveState( + `V.hostage`, V.hostage, "V.hostage", App.Patch.Utils.current.div, patchID + ); + wombs.push({identifier: `V.hostage`, actor: V.hostage, div: App.Patch.Utils.current.div}); + } + + if (V.boomerangSlave) { + App.Patch.Utils.slaveState( + `V.boomerangSlave`, V.boomerangSlave, "V.boomerangSlave", + App.Patch.Utils.current.div, patchID + ); + wombs.push({identifier: `V.boomerangSlave`, actor: V.boomerangSlave, div: App.Patch.Utils.current.div}); + } + + if (V.shelterSlave) { + App.Patch.Utils.slaveState( + `V.shelterSlave`, V.shelterSlave, "V.shelterSlave", + App.Patch.Utils.current.div, patchID + ); + wombs.push({identifier: `V.shelterSlave`, actor: V.shelterSlave, div: App.Patch.Utils.current.div}); + } + + if (V.traitor) { + App.Patch.Utils.slaveState( + `V.traitor`, V.traitor, "V.traitor", App.Patch.Utils.current.div, patchID + ); + wombs.push({identifier: `V.traitor`, actor: V.traitor, div: App.Patch.Utils.current.div}); + } + + // TankSlaveState + for (i in V.incubator.tanks ?? []) { + App.Patch.Utils.tankSlaveState( + `V.incubator.tanks[${i}]`, V.incubator.tanks[i], App.Patch.Utils.current.div, patchID + ); + wombs.push({ + identifier: `V.incubator.tanks[${i}]`, + actor: V.incubator.tanks[i], + div: App.Patch.Utils.current.div + }); + } + + // InfantState + for (i in V.cribs ?? []) { + App.Patch.Utils.infantState( + `V.cribs[${i}]`, V.cribs[i], "V.cribs", App.Patch.Utils.current.div, patchID + ); + wombs.push({identifier: `V.cribs[${i}]`, actor: V.cribs[i], div: App.Patch.Utils.current.div}); // shouldn't be needed, but InfantStates are HumanStates so might as well make sure it is right + } + + // ChildState + // TODO:@franklygeorge this is waiting on FC.ChildState to be implemented + const children = []; + for (i in children) { + App.Patch.Utils.childState( + // @ts-ignore + `children[${i}]`, children[i], "children", App.Patch.Utils.current.div, patchID + ); + wombs.push({identifier: `children[${i}]`, actor: children[i], div: App.Patch.Utils.current.div}); + } + + // Wombs/Fetuses; This should always be below all the HumanState patches + wombs.forEach((womb) => { + App.Patch.Utils.patchWomb(womb.identifier, womb.actor, womb.div, patchID); }); + + // CustomOrderSlave + if (V.customSlave) { + App.Patch.Utils.customSlaveOrder( + "V.customSlave", V.customSlave, App.Patch.Utils.current.div, patchID + ); + } + if (V.huskSlave) { + App.Patch.Utils.customSlaveOrder( + "V.huskSlave", V.huskSlave, App.Patch.Utils.current.div, patchID + ); + } + + // Post patch; This should always be the last patch + App.Patch.Utils.gameVariables("post", App.Patch.Utils.current.div, patchID); + } catch (e) { + header.innerHTML += `<span class="error">Patching Failed when applying patch ${App.Patch.Utils.current.patch}["${App.Patch.Utils.current.type}"] to ${App.Patch.Utils.current.identifier}!</span>`; + App.Patch.Utils.current.div.append(App.UI.DOM.formatException(e)); + // @ts-ignore + State.restore(); // restore the state to before patching + header.innerHTML += `<br><span>See below for details</span><hr>`; + // close the loading screen so that the user can see the error + App.Patch.Utils.loadingScreen.end(); + return; } - // console.timeEnd(`Patch ${patchID} TankSlaveState`); - - // InfantState - // console.time(`Patch ${patchID} InfantState`); - for (i in V.cribs ?? []) { - App.Patch.Utils.infantState( - `V.cribs[${i}]`, V.cribs[i], "V.cribs", App.Patch.Utils.current.div, patchID - ); - wombs.push({identifier: `V.cribs[${i}]`, actor: V.cribs[i], div: App.Patch.Utils.current.div}); // shouldn't be needed, but InfantStates are HumanStates so might as well make sure it is right - } - // console.timeEnd(`Patch ${patchID} InfantState`); - - // ChildState - // console.time(`Patch ${patchID} ChildState`); - // TODO:@franklygeorge this is waiting on FC.ChildState to be implemented - const children = []; - for (i in children) { - App.Patch.Utils.childState( - // @ts-ignore - `children[${i}]`, children[i], "children", App.Patch.Utils.current.div, patchID - ); - wombs.push({identifier: `children[${i}]`, actor: children[i], div: App.Patch.Utils.current.div}); - } - // console.timeEnd(`Patch ${patchID} ChildState`); - - // Wombs - // console.time(`Patch ${patchID} Fetuses`); - wombs.forEach((womb) => { - App.Patch.Utils.patchWomb(womb.identifier, womb.actor, womb.div, patchID); - }); - // console.timeEnd(`Patch ${patchID} Fetuses`); - - // CustomOrderSlave - // console.time(`Patch ${patchID} CustomOrderSlave`); - if (V.customSlave) { - App.Patch.Utils.customSlaveOrder( - "V.customSlave", V.customSlave, App.Patch.Utils.current.div, patchID - ); - } - if (V.huskSlave) { - App.Patch.Utils.customSlaveOrder( - "V.huskSlave", V.huskSlave, App.Patch.Utils.current.div, patchID - ); + + // report the time taken and the amount of content added to current div + const patchTime = (new Date().getTime() - patchStart).toLocaleString(); + const divLength = App.Patch.Utils.current.div.childNodes.length; + + f.append(App.UI.DOM.accordion( + `Patch ${patchID} (${divLength}) (${patchTime}ms)`, + App.Patch.Utils.current.div, + (divLength === 0 || getProp(V, "useAccordion", 1) > 0) + )); + + // continue + if (patchVersions.length !== 0) { + // let the user know what we are doing, by changing the loading screen message + message.innerHTML = `Applying Patch ${patchVersions.last()}...`; + // give the DOM time to update and then call applyPatch() again + setTimeout(applyPatch, 0); + } else { + const patchingTime = (new Date().getTime() - patchingStart).toLocaleString(); + f.append(App.UI.DOM.accordion( + `Patching results (${patchCount} patches) (${patchingTime}ms)`, + App.UI.DOM.makeElement('div', `Patching completed successfully, applying ${patchCount} patches in ${patchingTime}ms`), + true, + )); + // let the user know what we are doing, by changing the loading screen message + message.innerHTML = `Cleaning up and verifying...`; + // give the DOM time to update and then call verifyAndCleanup() + setTimeout(verifyAndCleanup, 0); } - // console.timeEnd(`Patch ${patchID} CustomOrderSlave`); + }; - // post patch - // console.time(`Patch ${patchID} Post`); - App.Patch.Utils.gameVariables("post", App.Patch.Utils.current.div, patchID); // This should always be the last patch - // console.timeEnd(`Patch ${patchID} Post`); - console.timeEnd(`Patch ${patchID}`); + // start the patching + if (patchVersions.length !== 0) { + // let the user know what we are doing, by changing the loading screen message + message.innerHTML = `Applying Patch ${patchVersions.last()}...`; + // give the DOM time to update and then call applyPatch() + setTimeout(applyPatch, 0); + } else { + // we didn't have any patches to apply but we still run verification + App.UI.DOM.appendNewElement('div', f, `No patches needed to be applied`); + // let the user know what we are doing, by changing the loading screen message + message.innerHTML = `Cleaning up and verifying...`; + // give the DOM time to update and then call verifyAndCleanup() + setTimeout(verifyAndCleanup, 0); } - } catch (e) { - header.innerHTML += `<span class="error">Patching Failed when applying patch ${App.Patch.Utils.current.patch}["${App.Patch.Utils.current.type}"] to ${App.Patch.Utils.current.identifier}!</span>`; - App.Patch.Utils.current.div.append(App.UI.DOM.formatException(e)); - // @ts-ignore - State.restore(); // restore the state to before patching - header.innerHTML += `<br><span>See below for details</span><hr>`; - // report the results - return f; - } - - App.Patch.Utils.current.div = App.UI.DOM.appendNewElement("div", f); - - // verification - App.UI.DOM.appendNewElement("h3", App.Patch.Utils.current.div, `Verification`); - console.log(`Verification`); - // console.time(`Patching verification`); - App.Verify.Utils.verificationError = false; - try { - App.Verify.everything(App.Patch.Utils.current.div); - } catch (e) { - header.innerHTML += `<span class="error">Verification failed!</span>`; - console.error("Verification failed!"); - App.Patch.Utils.current.div.append(App.UI.DOM.formatException(e)); - // @ts-ignore - State.restore(); // restore the state to before patching - header.innerHTML += `<br><span>See below for details</span><hr>`; - // report the results - return f; - } - // console.timeEnd(`Patching verification`); - - // Cleanup - // console.time(`Patching cleanup`); - App.UI.DOM.appendNewElement("h3", App.Patch.Utils.current.div, `Cleanup`); - console.log(`Cleanup`); - V.NaNArray = findNaN(); // reset NaNArray - App.UI.SlaveSummary.settingsChanged(); // let slave summary know that settings may have changed - // console.timeEnd(`Patching cleanup`); - - // handle App.Verify.Utils.verificationError - if (App.Verify.Utils.verificationError) { - header.innerHTML += `<span class="error">Verification failed!</span>`; - console.error("Verification failed!"); - // @ts-ignore - State.restore(); // restore the state to before patching - } else { - header.innerHTML += `<span class="green">Patches applied! You are on release ${V.releaseID}</span>`; - App.UI.DOM.appendNewElement("h3", f, "Patches Applied!"); - console.log("Patches Applied!"); - } + }), 0); - header.innerHTML += `<br><span>See below for details</span><hr>`; - // console.timeEnd('Patching All'); - // report the results - return f; + // return the empty document + // the first setTimeout above will execute soon after the passage change happens + return frag; }; diff --git a/src/data/patches/patchUtils.js b/src/data/patches/patchUtils.js index 5b81a8e9cac06a65ff7cdf2c3356d7aff5ced5d6..3fefb1a596084e2f2b5f56746a9e6284a3ad583f 100644 --- a/src/data/patches/patchUtils.js +++ b/src/data/patches/patchUtils.js @@ -41,7 +41,7 @@ App.Patch.Utils.patch = (patchType, patchUpTo, identifier, div=undefined, obj, e console.log(`\\/ \\/ \\/ Patching of '${patchType}' started for '${identifier}' \\/ \\/ \\/`); } const patchVersions = Object.keys(App.Patch.Patches).sort().filter((version) => { - return Number(version) <= patchUpTo; + return (Number(version) <= patchUpTo && Number(version) > obj.releaseID); }); /** @type {number} */ @@ -375,3 +375,43 @@ App.Patch.Utils.current = { * @property {(div: HTMLDivElement, child: FC.ChildState) => FC.ChildState} [childState] * @property {(div: HTMLDivElement, order: FC.CustomSlaveOrder, location: "V.customSlave"|"V.huskSlave") => FC.CustomSlaveOrder} [customSlaveOrder] */ + +// put on a loading screen +// this was copied from endWeekAnim.js and modified +App.Patch.Utils.loadingScreen = (function() { + let loadLockID = -1; + let infoDiv = null; + + function makeInfoDiv() { + infoDiv = $(` + <div class="endweek-titleblock"> + <div class="endweek-maintitle">Applying patches...</div> + <div class="endweek-subtitle" id="patch-message"></div> + </div> + `); + } + + function start() { + if (loadLockID === -1) { + makeInfoDiv(); + $("#init-screen").append(infoDiv); + loadLockID = LoadScreen.lock(); + } + } + + function end() { + if (loadLockID !== -1) { + setTimeout(() => { + LoadScreen.unlock(loadLockID); + infoDiv.remove(); + infoDiv = null; + loadLockID = -1; + }, 0); + } + } + + return { + start, + end + }; +})(); diff --git a/src/data/verification/verifyUtils.js b/src/data/verification/verifyUtils.js index 5f3d138b1b7f3bb28c4cc1dd0912ab4533fd5fa9..c0d867715224a77d410ddc7ebb7562902bc12fdc 100644 --- a/src/data/verification/verifyUtils.js +++ b/src/data/verification/verifyUtils.js @@ -187,38 +187,41 @@ App.Verify.Utils.verify = (setKey, identifier, obj, extra, div) => { return original; } } - // log any changes - const changes = App.Verify.Utils.changeSummary(original, obj, setKey, instructionID, identifier); - if (changes) { - changes.forEach((record) => { - if (record.path === "V.IDNumber") { - return; - } - let level = "error"; - if ( - record.description.includes(`'App.Verify.instructions.gameVariables.genePool'`) || - record.type === "key" - ) { - // The genePool verification is one of the (maybe the only) times that properties should actually be deleted/created - level = "orange"; - } - if (["key", "type"].includes(record.type)) { - if (div) { - $(div).append(`<div><span class="${level}">${record.description}</span></div>`); - } else if (level === "error") { - console.error(record.description); - } else { - console.warn(record.description); + + // report changes if V.logVerificationChanges is set to 1 + if (getProp(V, "reportVerificationChanges", 0) === 1) { + const changes = App.Verify.Utils.changeSummary(original, obj, setKey, instructionID, identifier); + if (changes) { + changes.forEach((record) => { + if (record.path === "V.IDNumber") { + return; + } + let level = "error"; + if ( + record.description.includes(`'App.Verify.instructions.gameVariables.genePool'`) || + record.type === "key" + ) { + // The genePool verification is one of the (maybe the only) times that properties should actually be deleted/created + level = "orange"; } - } else { - if (div) { - $(div).append(`<div><span>${record.description}</span></div>`); + if (["key", "type"].includes(record.type)) { + if (div) { + $(div).append(`<div><span class="${level}">${record.description}</span></div>`); + } else if (level === "error") { + console.error(record.description); + } else { + console.warn(record.description); + } } else { - console.warn(record.description); - console.warn("original val:", record.origVal, "new val:", record.newVal); + if (div) { + $(div).append(`<div><span>${record.description}</span></div>`); + } else { + console.warn(record.description); + console.warn("original val:", record.origVal, "new val:", record.newVal); + } } - } - }); + }); + } } } if (DEBUG) { diff --git a/src/data/verification/zVerify.js b/src/data/verification/zVerify.js index ac5e256927baad54ca9b8966d17c359c27e70db0..5972d8589c1e6095c2fdcb24c43eae3791b208c4 100644 --- a/src/data/verification/zVerify.js +++ b/src/data/verification/zVerify.js @@ -243,14 +243,67 @@ App.Verify.instructions = { * Runs verification for everything. * This is resource intensive. If you only need to verify a single data structure, use its verification function(s) instead. * @param {HTMLDivElement} [div] if provided then info may be appended to this div + * @param {Function} [callback] Called when verification finishes if provided */ -App.Verify.everything = (div) => { - App.Verify.slaveStates(div); - App.Verify.tankSlaveStates(div); - App.Verify.childStates(div); - App.Verify.infantStates(div); - App.Verify.fetuses(div); - App.Verify.playerState(V.PC, "V.PC", div); - App.Verify.customSlaveOrders(div); - App.Verify.gameVariables(div); // game variables should always be verified last +App.Verify.everything = (div, callback) => { + // a list of verifications to run + const funcs = [ + {func: App.Verify.slaveStates, params: [div], message: "Verifying SlaveStates..."}, + {func: App.Verify.tankSlaveStates, params: [div], message: "Verifying TankSlaveStates..."}, + {func: App.Verify.childStates, params: [div], message: "Verifying ChildStates..."}, + {func: App.Verify.infantStates, params: [div], message: "Verifying InfantStates..."}, + {func: App.Verify.fetuses, params: [div], message: `Verifying ${V.seePreg === 1 ? 'Fetuses' : 'FStates'}...`}, + {func: App.Verify.playerState, params: [V.PC, "V.PC", div], message: "Verifying PlayerState..."}, + {func: App.Verify.customSlaveOrders, params: [div], message: "Verifying CustomSlaveOrders..."}, + {func: App.Verify.gameVariables, params: [div], message: "Verifying GameVariables (V)..."}, // game variables should always be verified last + ].reverse(); + + // attempt to get the element, use a dummy if it doesn't exist + const message = document.getElementById("patch-message") ?? App.UI.DOM.makeElement("div"); + + const verify = () => { + // get the verification to run + const func = funcs.pop(); + // run the verification + try { + func.func.apply(null, func.params); + } catch (e) { + if (div) { + // attempt to get the element, use a dummy if it doesn't exist + const header = document.getElementById("patch-header") ?? App.UI.DOM.makeElement("div"); + header.innerHTML += `<span class="error">Verification failed!</span>`; + console.error("Verification failed!"); + if (App.Patch.Utils.current.div) { + App.Patch.Utils.current.div.append(App.UI.DOM.formatException(e)); + } else { + div.append(App.UI.DOM.formatException(e)); + } + header.innerHTML += `<br><span>See below for details</span><hr>`; + } else { + console.error(e); + console.error("Verification failed!"); + } + // @ts-ignore + State.restore(); // restore the state to before patching + // close the loading screen (if it is open) so that the user can see the error + App.Patch.Utils.loadingScreen.end(); + return; + } + + // check if there are more to run + if (funcs.length !== 0) { + // set the message the player sees + message.innerHTML = funcs.last().message; + // wait for the DOM to update then call verify + setTimeout(verify, 0); + } else if (callback) { + callback(); + } + }; + if (funcs.length !== 0) { + // set the message the player sees + message.innerHTML = funcs.last().message; + // wait for the DOM to update then call verify + setTimeout(verify, 0); + } }; diff --git a/src/gui/options/options.js b/src/gui/options/options.js index 62142773afdfe313de22cf4d97fb5d5eef0da5d6..8e8d310e0bb1e61a3e873c0364fee0ad8e06e876 100644 --- a/src/gui/options/options.js +++ b/src/gui/options/options.js @@ -517,6 +517,10 @@ App.UI.optionsPassage = function() { .addValue("Manual", 1).on().addValue("Automatic", 0).off(); } + options.addOption("Reporting changes caused by verification is", "reportVerificationChanges") + .addValue("Enabled", 1).on().addValue("Disabled", 0).off() + .addComment("This will report most changes caused by the verification system. While helpful it does make verification (and patching by extension) slower"); + option = options.addCustomOption("Genetics array"); if (V.cheatMode === 1) { option.addButton("Edit Genetics", () => { }, "Edit Genetics"); diff --git a/src/js/utilsDOM.js b/src/js/utilsDOM.js index 63e33b3d7d351747da37548e9ca86b9287544cd1..98a7c3de853cc9719cef87a16666f317f9f1e412 100644 --- a/src/js/utilsDOM.js +++ b/src/js/utilsDOM.js @@ -527,9 +527,11 @@ App.UI.DOM.formatException = function formatException(ex, recursion = false) { const header = document.createElement("p"); header.classList.add("error"); + App.UI.DOM.appendNewElement("div", header, `Please provide a text copy (or screenshot) of this error message starting with \`\`\` and ending with 'End of error message' below, a save file, and any other relevant information.`); + fragment.append(App.UI.DOM.makeElement("br")); App.UI.DOM.appendNewElement("div", header, `\`\`\``); + fragment.append(App.UI.DOM.makeElement("br")); App.UI.DOM.appendNewElement("div", header, `Apologies! An error has occurred. Please report this.`, ["bold"]); - App.UI.DOM.appendNewElement("div", header, `Please provide a screenshot of the error message, a save file and any other relevant information.`); fragment.append(header); const eventLabel = () => { @@ -564,7 +566,29 @@ App.UI.DOM.formatException = function formatException(ex, recursion = false) { App.UI.DOM.appendNewElement("div", error, ex.toString(), ["bold"]); } - fragment.append(error, `\`\`\``); + fragment.append(error); + + fragment.append(App.UI.DOM.makeElement("br")); + + fragment.append(`\`\`\``); + + fragment.append(App.UI.DOM.makeElement("br")); + + fragment.append(App.UI.DOM.makeElement("div", `End of error message`)); + + fragment.append(App.UI.DOM.makeElement("br")); + + const reportDiv = App.UI.DOM.makeElement("div", `Please report this error `); + + const reportLink = App.UI.DOM.makeElement("a", "here"); + + reportLink.classList.add("link-external"); + reportLink.href = "https://gitgud.io/pregmodfan/fc-pregmod/issues/"; + reportLink.target = "_blank"; + + reportDiv.append(reportLink); + + fragment.append(reportDiv); fragment.append(App.UI.DOM.makeElement("br"));