Dead Frontier - Scrapping

Alternate scrapping method.

// ==UserScript==
// @name        Dead Frontier - Scrapping
// @namespace   Dead Frontier - Shrike00
// @match       *://fairview.deadfrontier.com/onlinezombiemmo/index.php?page=24
// @grant       none
// @version     0.0.16
// @author      Shrike00
// @description Alternate scrapping method.
// @require https://update.greasyfork.org/scripts/441829/1470896/Dead%20Frontier%20-%20API.js
// @license MIT
// ==/UserScript==

// Changelog
// 0.0.16 - October 30, 2024
// - Bugfix: Regular click-and-drag scrapping now works after exiting scrap window.
// 0.0.15 - October 25, 2024
// - Bugfix: Improved compatibility with official feature. MutationObserver is now only active when scrap window is open,
//           and removes pageLock added by official click event listener (for the agreement that no longer appears).
// 0.0.14 - October 25, 2024
// - Bugfix: Added support for renamed items.
// 0.0.13 - October 24, 2024
// - Bugfix: Added case for when a modifier (MC/cooked) is on the item to be scrapped, but is not present in the market.
//           Reported and fix suggested by hotrods20.
// 0.0.12 - October 24, 2024
// - Change: Compatibility update to avoid conflicting with official fast scrap feature.
// 0.0.11 - May 2, 2024
// - Change: Updated Dead Frontier API, new tradezone ids.
// 0.0.10 - March 12, 2024
// - Change: Items are added to scrap whitelist if confirmed.
// 0.0.9 - February 24, 2024
// - Change: Updated backpack filters.
// 0.0.8 - February 20, 2024
// - Change: Added treasure items.
// 0.0.7 - February 17, 2024
// - Change: Loosened restrictions, market prices can now be higher and still allow scrapping.
// 0.0.6 - October 24, 2023
// - Improvement: No longer searches market for non-transferable items and locked slots, availability check is more aggressive.
// 0.0.5 - August 10, 2023
// - Change: Now confirms pop-up with enter key.
// 0.0.4 - April 9, 2023
// - Change: Added flag to optionally not do the market search.
// 0.0.3 - October 26, 2022
// - Bugfix: Properly warns if no market entries are available.
// 0.0.2 - September 30, 2022
// - Change: Warns if no market entries are available.
// 0.0.1 - June 4, 2022
// - Initial release

(function() {
	'use strict';

	// User Options
	const use_market_data = true;

	function isEquipment(item) {
		const weapon = item.category === ItemCategory.WEAPON;
		const armour = item.category === ItemCategory.ARMOUR;
		const equippable_item = item.category === ItemCategory.ITEM && (item.subcategory === ItemSubcategory.IMPLANT || item.subcategory === ItemSubcategory.CLOTHING);
		return weapon || armour || equippable_item;
	}

	function isConsumable(item) {
		const medicine = item.category === ItemCategory.ITEM && item.subcategory === ItemSubcategory.MEDICINE;
		const food = item.category === ItemCategory.ITEM && item.subcategory === ItemSubcategory.FOOD;
		const barricading = item.full_type === "woodenplanks" || item.full_type === "nails";
		return medicine || food || barricading;
	}

	function isAmmo(item) {
		const ammo = item.category === ItemCategory.AMMO;
		return ammo;
	}

	const scrap_categories = [
		{name: "Equipment", predicate: isEquipment},
		{name: "Consumables", predicate: isConsumable},
		{name: "Ammo", predicate: isAmmo},
		{name: "Other", predicate: (item) => true}
	];

	function treasureTypes() {
		const globaldata = window.globalData;
		const treasure_types = new Set();
		for (let key in globaldata) {
			const value = globaldata[key];
			if ("description" in value && value.description.indexOf("Scrap at The Yard") !== -1) {
				treasure_types.add(key);
			}
		}
		return treasure_types;
	}

	// Add in item types that are non-lootable or non-transferable but commonly scrapped here.
	const whitelist_types = new Set(["easterimplant", "halloweenimplant", "christmasimplant"]);
	const treasure_types = treasureTypes();
	function scrapItemPredicate(item) {
		const globaldata = window.globalData;
		const data = globaldata[item.base_type];
		const transferable = !("no_transfer" in data) || data.no_transfer !== "1";
		const lootable = !("noloot" in data) || data.noloot !== "1";
		return (transferable && lootable) || whitelist_types.has(item.base_type) || treasure_types.has(item.base_type);
	}

	function warnMarketPrice(scrap_price, market_price) {
		const flat_difference = 20000;
		const percentage_threshold = 0.5;
		const warn = (market_price - scrap_price > flat_difference) || (market_price*percentage_threshold > scrap_price);
		return warn;
	}

	function preventMarketPrice(scrap_price, market_price) {
		const flat_difference = 350000;
		const prevent = market_price - scrap_price > flat_difference;
		return prevent;
	}

	// Imports

	const ItemCategory = DeadFrontier.ItemCategory;
	const ItemSubcategory = DeadFrontier.ItemSubcategory;

	// Helpers

	function promiseWait(dt) {
		const promise = new Promise(function(resolve, reject) {
			setTimeout(function() {
				resolve();
			}, dt);
		});
		return promise;
	}

	function promiseInventoryAvailable(items, dt = 500) {
		const promise = new Promise(function(resolve, reject) {
			const check = setInterval(function() {
				if (items.inventoryAvailable()) {
					clearInterval(check);
					resolve();
				}
			}, dt);
		});
		return promise;
	}

	function isStackable(item) {
		const ammo = item.category === ItemCategory.AMMO;
		return ammo;
	}

	function marketFilter(item) {
		if (DeadFrontier.ItemFilters.Cooked(item)) {
			return DeadFrontier.MarketFilters.Cooked;
		} else if (DeadFrontier.ItemFilters.Godcrafted(item)) {
			return DeadFrontier.MarketFilters.Godcrafted;
		} else if (DeadFrontier.ItemFilters.Mastercrafted(item)) {
			return DeadFrontier.MarketFilters.Mastercrafted;
		} else {
			return (market_entry) => true;
		}
	}

	function updateCacheFromInventory(market_cache, player_items) {
		// Requests all items from inventory.
		return promiseInventoryAvailable(player_items, 100)
		.then(function() {
			const types_to_request = [];
			for (const [slot, item] of player_items.inventoryItems()) {
				const transferable = item.properties.get("transferable");
				const locked_slot = player_items.isLockedSlot(slot);
				const passes_predicate = scrapItemPredicate(item);
				const is_renamed = item.properties.has("rename");
				if (transferable && passes_predicate && !locked_slot && !is_renamed) {
					types_to_request.push(item.base_type);
				}
			}
			return market_cache.requestMultipleItemMarketEntriesByType(types_to_request);
		})
		.then(function() {
			const renames_to_request = [];
			for (const [slot, item] of player_items.inventoryItems()) {
				const transferable = item.properties.get("transferable");
				const locked_slot = player_items.isLockedSlot(slot);
				const passes_predicate = scrapItemPredicate(item);
				const is_renamed = item.properties.has("rename");
				if (transferable && passes_predicate && !locked_slot && is_renamed) {
					renames_to_request.push(item.properties.get("rename"));
				}
			}
			return market_cache.requestMultipleItemMarketEntriesByRename(renames_to_request);
		});
	}

	function updateMissingCacheFromInventory(market_cache, player_items) {
		// Only requests missing items from cache.
		return promiseInventoryAvailable(player_items, 100)
		.then(function() {
			const types_to_request = [];
			for (const [slot, item] of player_items.inventoryItems()) {
				const transferable = item.properties.get("transferable");
				const locked_slot = player_items.isLockedSlot(slot);
				const passes_predicate = scrapItemPredicate(item);
				const cache_contains_type = market_cache.hasItemType(item.base_type);
				const is_renamed = item.properties.has("rename");
				if (transferable && passes_predicate && !locked_slot && !cache_contains_type && !is_renamed) {
					types_to_request.push(item.base_type);
				}
			}
			return market_cache.requestMultipleItemMarketEntriesByType(types_to_request);
		})
		.then(function() {
			const renames_to_request = [];
			for (const [slot, item] of player_items.inventoryItems()) {
				const transferable = item.properties.get("transferable");
				const locked_slot = player_items.isLockedSlot(slot);
				const passes_predicate = scrapItemPredicate(item);
				const is_renamed = item.properties.has("rename");
				if (transferable && passes_predicate && !locked_slot && is_renamed) {
					const cache_contains_rename = market_cache.hasRename(item.properties.get("rename"));
					if (cache_contains_rename) {
						renames_to_request.push(item.properties.get("rename"));
					}
				}
			}
			return market_cache.requestMultipleItemMarketEntriesByRename(types_to_request);
		})
	}

	// UI

	function scrappingContainer() {
		const box = document.createElement("div");
		box.style.setProperty("position", "absolute");
		box.style.setProperty("display", "none");
		box.style.setProperty("width", "99%");
		box.style.setProperty("height", "100%");
		box.style.setProperty("top", "0px");
		box.style.setProperty("left", "0px");
		box.style.setProperty("margin", "0 auto");
		box.style.setProperty("justify-content", "center");
		box.style.setProperty("z-index", 20);
		box.style.setProperty("background-color", "rgba(0, 0, 0, 1)")
		box.style.setProperty("border", "1px solid red");
		return box;
	}

	function cancelButton() {
		const button = document.createElement("button");
		button.innerHTML = "Cancel";
		button.style.setProperty("position", "absolute");
		button.style.setProperty("left", "0px");
		button.style.setProperty("right", "0px");
		button.style.setProperty("bottom", "5px");
		button.style.setProperty("margin", "auto");
		button.style.setProperty("font-size", "16px");
		button.style.setProperty("font-family", "Courier New, Arial");
		return button;
	}

	function scrapIcon() {
		const inventory_holder = document.getElementById("inventoryholder");
		for (let i = 0; inventory_holder.children.length; i++) {
			const child = inventory_holder.children[i];
			if (child.dataset.action === "scrap") {
				return child;
			}
		}
		return undefined;
	}

	function scrapTitle() {
		const title = document.createElement("div");
		title.style.setProperty("font-size", "18px");
		title.style.setProperty("margin-top", "5px");
		title.style.setProperty("margin-bottom", "-30px");
		title.style.setProperty("font-weight", "bold");
		title.style.setProperty("color", "#D0D0D0");
		title.style.setProperty("text-shadow", "0 0 5px red");
		title.style.setProperty("font-family", "Courier New, Arial");
		title.innerHTML = "Scrapping";
		return title;
	}

	function setWarningHidden() {
		// Hides pop-up warning.
		const prompt = document.getElementById("prompt");
		const gamecontent = document.getElementById("gamecontent");
		prompt.removeAttribute("style");
		gamecontent.removeAttribute("class");
		gamecontent.style.setProperty("display", "none");
	}

	function setWarningVisible(text, yesCallback, noCallback) {
		// Shows pop-up warning/prompt.
		const prompt = document.getElementById("prompt");
		const gamecontent = document.getElementById("gamecontent");
		// Warning message
		prompt.style.setProperty("display", "block");
		gamecontent.style.setProperty("display", "block");
		gamecontent.className = "warning";
		gamecontent.style.setProperty("font-family", "\"Courier New CE\", Arial");
		gamecontent.style.setProperty("font-weight", "bold");
		gamecontent.style.setProperty("color", "white");
		gamecontent.style.setProperty("text-align", "center");
		gamecontent.innerHTML = text;
		// Buttons
		const noButton = document.createElement("button");
		noButton.style.position = "absolute";
		noButton.style.top = "72px";
		noButton.style.left = "151px";
		noButton.innerHTML = "No";
		noButton.addEventListener("click", noCallback);
		gamecontent.appendChild(noButton);
		const yesButton = document.createElement("button");
		yesButton.style.position = "absolute";
		yesButton.style.left = "86px";
		yesButton.style.top = "72px";
		yesButton.innerHTML = "Yes";
		yesButton.addEventListener("click", yesCallback);
		gamecontent.appendChild(yesButton);
		// Enter hotkey for confirming.
		gamecontent.focus();
		let debounce = true; // Avoid sending request if one has already been sent.
		gamecontent.onkeydown = function(e) {
			const enter_pressed = e.code === "Enter" || e.code === "NumpadEnter";
			if (debounce && enter_pressed) {
				debounce = false;
				Promise.resolve(yesCallback())
				.then(function() {
					debounce = true;
				});
			}
		}
	}

	function setLoadingHidden() {
		const prompt = document.getElementById("prompt");
		const gamecontent = document.getElementById("gamecontent");
		gamecontent.innerHTML = "";
		prompt.style.setProperty("display", "none");
		gamecontent.style.setProperty("display", "none");
	}

	function setLoadingVisible() {
		const prompt = document.getElementById("prompt");
		const gamecontent = document.getElementById("gamecontent");
		gamecontent.style.setProperty("text-align", "center");
		gamecontent.innerHTML = "Loading...";
		prompt.style.setProperty("display", "block");
		gamecontent.style.setProperty("display", "block");
	}

	function scrapScrollingFrame() {
		const frame = document.createElement("div");
		frame.style.setProperty("overflow-y", "scroll");
		frame.style.setProperty("border", "1px solid #990000");
		frame.style.setProperty("position", "relative");
		frame.style.setProperty("height", "450px");
		frame.style.setProperty("width", "640px");
		frame.style.setProperty("bottom", "5px");
		return frame;
	}

	function headerElement(name) {
		const header = document.createElement("tr");
		header.innerHTML = name;
		header.style.setProperty("font-weight", "bold");
		header.style.setProperty("font-size", "14px");
		return header;
	}

	function displayName(item) {
		const is_renamed = item.properties.has("rename");
		const base = is_renamed ? item.properties.get("rename") : item.base_name;
		if (item.category === DeadFrontier.ItemCategory.WEAPON && DeadFrontier.ItemFilters.Mastercrafted(item)) {
			const accuracy = item.properties.get("accuracy").toString();
			const reloading = item.properties.get("reloading").toString();
			const critical_hit = item.properties.get("critical_hit").toString();
			return base + " (" + accuracy + "/" + reloading + "/" + critical_hit + ")";
		} else if (item.category === DeadFrontier.ItemCategory.ARMOUR && DeadFrontier.ItemFilters.Mastercrafted(item)) {
			const agility = item.properties.get("agility").toString();
			const endurance = item.properties.get("endurance").toString();
			return base + " (" + agility + "/" + endurance + ")";
		} else if (isStackable(item)) {
			const quantity = item.quantity.toString();
			return base + " (" + quantity  + ")";
		} else {
			return base;
		}
	}

	function itemNameDatum(item_name) {
		const name = document.createElement("td");
		name.innerHTML = item_name;
		name.style.setProperty("padding-left", "4px");
		name.style.setProperty("padding-top", "4px");
		name.style.setProperty("padding-bottom", "4px");
		name.style.setProperty("width", "250px");
		return name;
	}

	function scrapPriceDatum(text) {
		const scrap_price = document.createElement("td");
		scrap_price.innerHTML = text;
		scrap_price.style.setProperty("text-align", "right");
		scrap_price.style.setProperty("padding-left", "10px");
		scrap_price.style.setProperty("padding-top", "4px");
		scrap_price.style.setProperty("padding-bottom", "4px");
		scrap_price.style.setProperty("width", "100px");
		return scrap_price;
	}

	function marketPriceDatum(text) {
		const market_price = document.createElement("td");
		market_price.innerHTML = text;
		market_price.style.setProperty("text-align", "right");
		market_price.style.setProperty("padding-left", "10px");
		market_price.style.setProperty("padding-top", "4px");
		market_price.style.setProperty("padding-bottom", "4px");
		market_price.style.setProperty("width", "100px");
		return market_price;
	}

	function scrapButton() {
		const scrap_button = document.createElement("button");
		scrap_button.innerHTML = "Scrap";
		scrap_button.style.setProperty("margin-left", "60px");
		scrap_button.style.setProperty("margin-top", "4px");
		scrap_button.style.setProperty("margin-bottom", "4px");
		scrap_button.style.setProperty("margin-right", "4px");
		return scrap_button;
	}

	function updateScrapItems(table, player_items, market_cache) {
		table.replaceChildren(); // Remove children
		// Create header elements
		const headers = scrap_categories.map((category) => headerElement(category.name));
		headers.forEach((header) => table.appendChild(header));
		// Stores information about each table row
		const item_category_data = new Map();
		scrap_categories.forEach((category) => item_category_data.set(category.name, []));
		for (const [slot, item] of player_items.inventoryItems()) {
			// Skip if item is in locked slot or does not pass predicate (by default, ignores non-transferable and non-lootable items).
			if (window.lockedSlots.includes(slot.toString())) {
				continue;
			}
			if (!scrapItemPredicate(item)) {
				continue;
			}
			// Creates row element
			const tr = document.createElement("tr");
			tr.style.setProperty("font-size", "12px");
			// Item name
			const name = itemNameDatum(displayName(item));
			// Item scrap price
			const scrap_value = scrapValue(item.full_type, item.quantity);
			const scrap_price = scrapPriceDatum("$" + scrap_value.toLocaleString())
			// Item market price
			let market_price = null;
			let cheapest_market_price = null;
			const is_renamed = item.properties.has("rename");
			if (market_cache.hasItemType(item.base_type) || (is_renamed && market_cache.hasRename(item.properties.get("rename")))) {
				const item_market_data = is_renamed ? market_cache.getItemMarketEntriesByRename(item.properties.get("rename")) : market_cache.getItemMarketEntriesByType(item.base_type);
				const filtered_data = item_market_data.filter(marketFilter(item));
				if (filtered_data.length === 0) {
					const cheapest_market_entry = item_market_data[0];
					const cheapest_market_item = cheapest_market_entry.item;
					cheapest_market_price = isStackable(item) ? Math.floor((cheapest_market_entry.price/cheapest_market_entry.item.quantity)*item.quantity) : cheapest_market_entry.price;
					market_price = marketPriceDatum("$" + cheapest_market_price.toLocaleString() + "?");
				} else {
					const cheapest_market_entry = filtered_data[0];
					const cheapest_market_item = cheapest_market_entry.item;
					cheapest_market_price = isStackable(item) ? Math.floor((cheapest_market_entry.price/cheapest_market_entry.item.quantity)*item.quantity) : cheapest_market_entry.price;
					market_price = marketPriceDatum("$" + cheapest_market_price.toLocaleString());
				}
			} else {
				cheapest_market_price = 0;
				market_price = marketPriceDatum("N/A");
			}
			// Item scrap button
			const scrap_button = scrapButton();
			scrap_button.addEventListener("mouseenter", function(e) {
				tr.style.setProperty("font-weight", "bold");
				tr.style.setProperty("box-shadow", "0px 0px 2px 1px white");
			});
			scrap_button.addEventListener("mouseleave", function(e) {
				tr.style.removeProperty("font-weight");
				tr.style.removeProperty("box-shadow");
			})
			// Scrap Button Event Listeners
			const prevent_scrap = preventMarketPrice(scrap_value, cheapest_market_price);
			const warn_scrap = warnMarketPrice(scrap_value, cheapest_market_price);
			const no_market_entries_available = !market_cache.hasItemType(item.base_type) && !(is_renamed && market_cache.hasRename(item.properties.get("rename")));
			const need_market_check = !(whitelist_types.has(item.base_type) || treasure_types.has(item.base_type));
			const force_allow = whitelist_types.has(item.base_type);
			if (prevent_scrap) {
				tr.style.setProperty("color", "red");
				scrap_button.style.setProperty("display", "none");
			} else if (warn_scrap && !no_market_entries_available && !force_allow) {
				tr.style.setProperty("color", "red");
				scrap_button.addEventListener("click", function(e) {
					const text = "This item sells for <span style=\"color: red;\">$" + cheapest_market_price.toLocaleString() + "</span> and scraps for <span style=\"color: red;\">$" + scrap_value.toLocaleString() + "</span>.<br>Are you sure you want to scrap this item?";
					setWarningVisible(text, function() {
						player_items.scrapInventoryItem(slot, item)
						.then(function() {
							whitelist_types.add(item.base_type);
							updateScrapItems(table, player_items, market_cache);
							playSound("shop_buysell");
							setWarningHidden();
						});
					}, setWarningHidden);
				});
			} else if (no_market_entries_available && need_market_check && use_market_data && !force_allow) {
				tr.style.setProperty("color", "red");
				scrap_button.addEventListener("click", function(e) {
					const text = "No market data for this item is available.<br>Are you sure you want to scrap this item?";
					setWarningVisible(text, function() {
						player_items.scrapInventoryItem(slot, item)
						.then(function() {
							whitelist_types.add(item.base_type);
							updateScrapItems(table, player_items, market_cache);
							playSound("shop_buysell");
							setWarningHidden();
						});
					}, setWarningHidden);
				});
			} else {
				scrap_button.addEventListener("click", function(e) {
					setLoadingVisible();
					player_items.scrapInventoryItem(slot, item)
					.then(function() {
						setLoadingHidden();
						updateScrapItems(table, player_items, market_cache);
						playSound("shop_buysell");
					});
				});
			}
			// Adding to arrays (to be sorted and added to table).
			tr.appendChild(name);
			tr.appendChild(market_price);
			tr.appendChild(scrap_price);
			tr.appendChild(scrap_button);
			// Adds data about each table row to map
			const data = {element: tr, full_type: item.full_type, quantity: item.quantity};
			for (let i = 0; i < scrap_categories.length; i++) {
				const category = scrap_categories[i];
				if (category.predicate(item)) {
					item_category_data.get(category.name).push(data);
					break;
				}
			}
		}
		// Sort item table rows and add to table.
		function sortingFunction(a, b) {
			const scrap_a = scrapValue(a.full_type, a.quantity);
			const scrap_b = scrapValue(b.full_type, b.quantity);
			if (scrap_a === scrap_b) {
				return a.full_type > b.full_type;
			} else {
				return scrap_a > scrap_b;
			}
		}
		// Hides header rows if there are no items under them
		headers.forEach((header) => header.style.setProperty("display", item_category_data.get(header.innerHTML).length === 0 ? "none" : "block"));
		// Sorts category data/table rows
		for (const [category_name, category_data] of item_category_data.entries()) {
			category_data.sort(sortingFunction);
		}
		// Puts table row elements after each header
		headers.forEach((header) => header.after(...item_category_data.get(header.innerHTML).map((data) => data.element)));
	}

	function tableKeys() {
		const keys = document.createElement("div");
		keys.style.setProperty("margin-top", "10px");
		keys.style.setProperty("margin-bottom", "-15px");
		keys.style.setProperty("font-size", "14px");
		keys.style.setProperty("font-weight", "bold");
		keys.style.setProperty("font-family", "Courier New, monospace");
		const name = document.createElement("span");
		name.style.setProperty("position", "absolute");
		name.style.setProperty("top", "23px");
		name.style.setProperty("left", "53px");
		name.innerHTML = "Item Name";
		keys.appendChild(name);
		const market_price = document.createElement("span");
		market_price.style.setProperty("position", "absolute");
		market_price.style.setProperty("top", "23px");
		market_price.style.setProperty("left", "322px");
		market_price.innerHTML = "Market Price";
		keys.appendChild(market_price);
		const scrap_price = document.createElement("span");
		scrap_price.style.setProperty("position", "absolute");
		scrap_price.style.setProperty("top", "23px");
		scrap_price.style.setProperty("left", "443px");
		scrap_price.innerHTML = "Scrap Price";
		keys.appendChild(scrap_price);
		return keys;
	}

	// Main

	function main() {
		const player_items = new DeadFrontier.PlayerItems();
		const market_cache = new DeadFrontier.MarketCache(parseInt(window.userVars.DFSTATS_df_tradezone));
		const flash_replace = document.getElementById("inventoryholder").parentNode;
		const prompt = document.getElementById("prompt");
		const gamecontent = document.getElementById("gamecontent");
		let prompt_observer;
		const container = scrappingContainer();
		flash_replace.appendChild(container);
		const title = scrapTitle();
		container.appendChild(title);
		container.appendChild(tableKeys());
		const scrapping_frame = scrapScrollingFrame();
		container.appendChild(scrapping_frame);
		const cancel_button = cancelButton();
		container.appendChild(cancel_button);
		const table = document.createElement("table");
		table.style.setProperty("margin-bottom", "4px");
		let last_request_time = null;
		scrapping_frame.appendChild(table);
		scrapIcon().addEventListener("click", function(e) {
			prompt_observer.observe(prompt, {childList: true});
			setLoadingVisible();
			const now = performance.now();
			if (use_market_data) {
				updateCacheFromInventory(market_cache, player_items)
				.then(function() {
					last_request_time = now;
					updateScrapItems(table, player_items, market_cache);
					promiseWait(100) // Wait for DOM to update.
					.then(function() {
						container.style.setProperty("display", "grid");
						setLoadingHidden();
					});
				});
			} else {
				last_request_time = now;
				updateScrapItems(table, player_items, market_cache);
				promiseWait(100) // Wait for DOM to update.
				.then(function() {
					container.style.setProperty("display", "grid");
					setLoadingHidden();
				});
			}
		});
		cancel_button.addEventListener("click", function(e) {
			window.pageLock = false;
			prompt_observer.disconnect();
			gamecontent.style.removeProperty("display");
			container.style.setProperty("display", "none");
			prompt.style.setProperty("display", "none");
		});
		// Observer to hide genericActionBox that pops up the liability/responsibility agreement for official quick scrapping.
		function prompt_callback(mutation_list, observer) {
			for (const mutation of mutation_list) {
				const added_elements = mutation.addedNodes;
				const is_generic_action_box = added_elements.length == 1 && added_elements[0].className == "genericActionBox opElem"
				if (is_generic_action_box) {
					const action_box = added_elements[0];
					action_box.style.display = "none";
					const nodes_to_remove = [];
					const parent = action_box.parentNode;
					for (const child of parent.children) {
						if (child.className === "genericActionBox opElem") {
							nodes_to_remove.push(child);
						}
					}
					for (const child of nodes_to_remove) {
						parent.removeChild(child);
					}
				}
			}
		}
		prompt_observer = new MutationObserver(prompt_callback);
	};
	main();
})();