mirror of
				https://github.com/OpenFusionProject/Client.git
				synced 2025-11-04 04:40:24 +00:00 
			
		
		
		
	Merge 51f0c06092 into ec0fc87dcd
				
					
				
			This commit is contained in:
		
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -8,4 +8,8 @@ version
 | 
			
		||||
resources/app/files/rankurl.txt
 | 
			
		||||
node_modules/
 | 
			
		||||
dist/
 | 
			
		||||
UnityBugReporter.exe
 | 
			
		||||
UnityBugReporter.exe
 | 
			
		||||
cache_handler/*/
 | 
			
		||||
extra/
 | 
			
		||||
*.sh
 | 
			
		||||
yarn.lock
 | 
			
		||||
@@ -80,6 +80,11 @@ body {
 | 
			
		||||
    background-repeat: repeat;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.btn-warning {
 | 
			
		||||
    background-image: url("../../assets/img/btn-warning-bg.png");
 | 
			
		||||
    background-repeat: repeat;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#of-aboutmodal > .modal-dialog > .modal-content {
 | 
			
		||||
    background-color: #093363;
 | 
			
		||||
    border-color: #6699ff;
 | 
			
		||||
@@ -105,6 +110,36 @@ body {
 | 
			
		||||
    border-color: #6699ff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#of-editcacheconfigmodal > .modal-dialog > .modal-content {
 | 
			
		||||
    background-color: #093363;
 | 
			
		||||
    border-color: #6699ff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#of-editconfigmodal > .modal-dialog > .modal-content {
 | 
			
		||||
    background-color: #093363;
 | 
			
		||||
    border-color: #6699ff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#of-addversionmodal > .modal-dialog > .modal-content {
 | 
			
		||||
    background-color: #093363;
 | 
			
		||||
    border-color: #6699ff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#of-editversionmodal > .modal-dialog > .modal-content {
 | 
			
		||||
    background-color: #093363;
 | 
			
		||||
    border-color: #6699ff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#of-deleteversionmodal > .modal-dialog > .modal-content {
 | 
			
		||||
    background-color: #093363;
 | 
			
		||||
    border-color: #6699ff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#of-restoreversionsmodal > .modal-dialog > .modal-content {
 | 
			
		||||
    background-color: #093363;
 | 
			
		||||
    border-color: #6699ff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.form-control,
 | 
			
		||||
.form-control:focus {
 | 
			
		||||
    border-color: #0099ff;
 | 
			
		||||
@@ -118,6 +153,14 @@ select {
 | 
			
		||||
    color: #fff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal {
 | 
			
		||||
    overflow-y: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.invalidinput {
 | 
			
		||||
    border-color: #ff0000;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button:disabled {
 | 
			
		||||
    cursor: not-allowed;
 | 
			
		||||
    pointer-events: all !important;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								assets/img/btn-warning-bg.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/img/btn-warning-bg.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 514 B  | 
@@ -2,14 +2,27 @@ var remote = require("remote");
 | 
			
		||||
var remotefs = remote.require("fs-extra");
 | 
			
		||||
var dns = remote.require("dns");
 | 
			
		||||
var path = remote.require("path");
 | 
			
		||||
var dialog = remote.require("dialog");
 | 
			
		||||
var net = remote.require("net");
 | 
			
		||||
var spawn = require("child_process").spawn;
 | 
			
		||||
 | 
			
		||||
var userData = remote.require("app").getPath("userData");
 | 
			
		||||
var configPath = path.join(userData, "config.json");
 | 
			
		||||
var serversPath = path.join(userData, "servers.json");
 | 
			
		||||
var versionsPath = path.join(userData, "versions.json");
 | 
			
		||||
var cacheRoot = path.join(
 | 
			
		||||
    userData,
 | 
			
		||||
    "/../../LocalLow/Unity/Web Player/Cache"
 | 
			
		||||
);
 | 
			
		||||
var offlineRootDefault = path.join(cacheRoot, "Offline");
 | 
			
		||||
var offlineRoot = offlineRootDefault;
 | 
			
		||||
 | 
			
		||||
var cdnString = "http://cdn.dexlabs.systems/ff/big";
 | 
			
		||||
 | 
			
		||||
var versionArray;
 | 
			
		||||
var serverArray;
 | 
			
		||||
var cacheSizes;
 | 
			
		||||
var defaultHashes;
 | 
			
		||||
var config;
 | 
			
		||||
 | 
			
		||||
function enableServerListButtons() {
 | 
			
		||||
@@ -30,6 +43,30 @@ function disableServerListButtons() {
 | 
			
		||||
    $("#of-deleteserver-button").prop("disabled", true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function enableVersionAddButton() {
 | 
			
		||||
    $("#of-addversion-button").removeClass("disabled");
 | 
			
		||||
    $("#of-addversion-button").prop("disabled", false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function enableVersionListButtons() {
 | 
			
		||||
    $("#of-editversion-button").removeClass("disabled");
 | 
			
		||||
    $("#of-editversion-button").prop("disabled", false);
 | 
			
		||||
    $("#of-deleteversion-button").removeClass("disabled");
 | 
			
		||||
    $("#of-deleteversion-button").prop("disabled", false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function disableVersionAddButton() {
 | 
			
		||||
    $("#of-addversion-button").addClass("disabled");
 | 
			
		||||
    $("#of-addversion-button").prop("disabled", true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function disableVersionListButtons() {
 | 
			
		||||
    $("#of-editversion-button").addClass("disabled");
 | 
			
		||||
    $("#of-editversion-button").prop("disabled", true);
 | 
			
		||||
    $("#of-deleteversion-button").addClass("disabled");
 | 
			
		||||
    $("#of-deleteversion-button").prop("disabled", true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getAppVersion() {
 | 
			
		||||
    appVersion = remote.require("app").getVersion();
 | 
			
		||||
 | 
			
		||||
@@ -47,6 +84,36 @@ function setAppVersionText() {
 | 
			
		||||
    $("#of-versionnumber").text("v" + getAppVersion());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function validateServerSave(modalName) {
 | 
			
		||||
    // works everytime a key is entered into the server save form
 | 
			
		||||
    var descInput = document.getElementById(modalName + "server-descinput");
 | 
			
		||||
    var ipInput = document.getElementById(modalName + "server-ipinput");
 | 
			
		||||
    var button = document.getElementById(modalName + "server-savebutton");
 | 
			
		||||
    var valid = true;
 | 
			
		||||
 | 
			
		||||
    descInput.classList.remove("invalidinput");
 | 
			
		||||
    ipInput.classList.remove("invalidinput");
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        descInput.value.length < parseInt(descInput.getAttribute("minlength")) ||
 | 
			
		||||
        descInput.value.length > parseInt(descInput.getAttribute("maxlength"))
 | 
			
		||||
    ) {
 | 
			
		||||
        descInput.classList.add("invalidinput");
 | 
			
		||||
        valid = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!(new RegExp(ipInput.getAttribute("pattern"))).test(ipInput.value)) {
 | 
			
		||||
        ipInput.classList.add("invalidinput");
 | 
			
		||||
        valid = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (valid) {
 | 
			
		||||
        button.removeAttribute("disabled");
 | 
			
		||||
    } else {
 | 
			
		||||
        button.setAttribute("disabled", "");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function addServer() {
 | 
			
		||||
    var jsonToModify = JSON.parse(remotefs.readFileSync(serversPath));
 | 
			
		||||
 | 
			
		||||
@@ -113,6 +180,154 @@ function restoreDefaultServers() {
 | 
			
		||||
    loadServerList();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function validateVersionSave(modalName) {
 | 
			
		||||
    // works everytime a key is entered into the version save form
 | 
			
		||||
    var nameInput = document.getElementById(modalName + "version-nameinput");
 | 
			
		||||
    var urlInput = document.getElementById(modalName + "version-urlinput");
 | 
			
		||||
    var button = document.getElementById(modalName + "version-savebutton");
 | 
			
		||||
    var valid = true;
 | 
			
		||||
 | 
			
		||||
    nameInput.classList.remove("invalidinput");
 | 
			
		||||
    urlInput.classList.remove("invalidinput");
 | 
			
		||||
 | 
			
		||||
    var matchingVersions = versionArray.filter(function (obj) {
 | 
			
		||||
        return obj.name === nameInput.value;
 | 
			
		||||
    });
 | 
			
		||||
    var allowedMatches = (modalName === "edit") ? 1 : 0;
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        matchingVersions.length > allowedMatches ||
 | 
			
		||||
        !(new RegExp(nameInput.getAttribute("pattern"))).test(nameInput.value)
 | 
			
		||||
    ) {
 | 
			
		||||
        nameInput.classList.add("invalidinput");
 | 
			
		||||
        valid = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!(new RegExp(urlInput.getAttribute("pattern"))).test(urlInput.value)) {
 | 
			
		||||
        urlInput.classList.add("invalidinput");
 | 
			
		||||
        valid = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (valid) {
 | 
			
		||||
        button.removeAttribute("disabled");
 | 
			
		||||
    } else {
 | 
			
		||||
        button.setAttribute("disabled", "");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function addVersion() {
 | 
			
		||||
    var jsonToModify = JSON.parse(remotefs.readFileSync(versionsPath));
 | 
			
		||||
 | 
			
		||||
    var version = {};
 | 
			
		||||
    version["name"] =
 | 
			
		||||
        $("#addversion-nameinput").val().length == 0
 | 
			
		||||
            ? "custom-build-" + uuidv4().substring(0, 8)
 | 
			
		||||
            : $("#addversion-nameinput").val();
 | 
			
		||||
    version["url"] =
 | 
			
		||||
        $("#addversion-urlinput").val().length == 0
 | 
			
		||||
            ? cdnString + "/" + version["name"] + "/"
 | 
			
		||||
            : $("#addversion-urlinput").val();
 | 
			
		||||
 | 
			
		||||
    var matchingVersions = jsonToModify["versions"].filter(function (obj) {
 | 
			
		||||
        return obj.name === version["name"];
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (matchingVersions.length > 0) return;
 | 
			
		||||
 | 
			
		||||
    jsonToModify["versions"].unshift(version);
 | 
			
		||||
 | 
			
		||||
    remotefs.writeFileSync(versionsPath, JSON.stringify(jsonToModify, null, 4));
 | 
			
		||||
    loadCacheList();
 | 
			
		||||
    handleCache("hash-check", version["name"]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function editVersion() {
 | 
			
		||||
    var jsonToModify = JSON.parse(remotefs.readFileSync(versionsPath));
 | 
			
		||||
    var editedVersionString = null;
 | 
			
		||||
 | 
			
		||||
    $.each(jsonToModify["versions"], function (key, value) {
 | 
			
		||||
        if (value["name"] == getSelectedVersion() && !defaultHashes.hasOwnProperty(value["name"])) {
 | 
			
		||||
            value["name"] =
 | 
			
		||||
                $("#editversion-nameinput").val().length == 0
 | 
			
		||||
                    ? value["name"]
 | 
			
		||||
                    : $("#editversion-nameinput").val();
 | 
			
		||||
            value["url"] =
 | 
			
		||||
                $("#editversion-urlinput").val().length == 0
 | 
			
		||||
                    ? value["url"]
 | 
			
		||||
                    : $("#editversion-urlinput").val();
 | 
			
		||||
            editedVersionString = value["name"];
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (!editedVersionString) return;
 | 
			
		||||
 | 
			
		||||
    remotefs.writeFileSync(versionsPath, JSON.stringify(jsonToModify, null, 4));
 | 
			
		||||
    loadCacheList();
 | 
			
		||||
    handleCache("hash-check", editedVersionString);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function deleteVersion() {
 | 
			
		||||
    var jsonToModify = JSON.parse(remotefs.readFileSync(versionsPath));
 | 
			
		||||
 | 
			
		||||
    var result = jsonToModify["versions"].filter(function (obj) {
 | 
			
		||||
        return obj.name === getSelectedVersion();
 | 
			
		||||
    })[0];
 | 
			
		||||
 | 
			
		||||
    if (defaultHashes.hasOwnProperty(result.name)) return;
 | 
			
		||||
 | 
			
		||||
    var resultindex = jsonToModify["versions"].indexOf(result);
 | 
			
		||||
 | 
			
		||||
    jsonToModify["versions"].splice(resultindex, 1);
 | 
			
		||||
 | 
			
		||||
    remotefs.writeFileSync(versionsPath, JSON.stringify(jsonToModify, null, 4));
 | 
			
		||||
    loadCacheList();
 | 
			
		||||
    delete cacheSizes[result.name];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function restoreDefaultVersions() {
 | 
			
		||||
    remotefs.copySync(
 | 
			
		||||
        path.join(__dirname, "/defaults/versions.json"),
 | 
			
		||||
        versionsPath
 | 
			
		||||
    );
 | 
			
		||||
    loadCacheList();
 | 
			
		||||
    handleCache("hash-check");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function editConfig() {
 | 
			
		||||
    var jsonToModify = JSON.parse(remotefs.readFileSync(configPath));
 | 
			
		||||
 | 
			
		||||
    jsonToModify["autoupdate-check"] = $("#editconfig-autoupdate").prop("checked");
 | 
			
		||||
    jsonToModify["cache-swapping"] = $("#editconfig-cacheswapping").prop("checked");
 | 
			
		||||
    jsonToModify["enable-offline-cache"] = $("#editconfig-enableofflinecache").prop("checked");
 | 
			
		||||
    jsonToModify["verify-offline-cache"] = $("#editconfig-verifyofflinecache").prop("checked");
 | 
			
		||||
 | 
			
		||||
    var dirInput = $("#editconfig-offlinecachelocation:text").val();
 | 
			
		||||
    var shouldChangeRoot = (
 | 
			
		||||
        remotefs.existsSync(dirInput) &&
 | 
			
		||||
        remotefs.statSync(dirInput).isDirectory()
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    jsonToModify["offline-cache-location"] = shouldChangeRoot ? dirInput : offlineRoot;
 | 
			
		||||
 | 
			
		||||
    remotefs.writeFileSync(configPath, JSON.stringify(jsonToModify, null, 4));
 | 
			
		||||
 | 
			
		||||
    loadConfig();
 | 
			
		||||
    if (shouldChangeRoot) handleCache("hash-check", null, "offline");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function validateCacheLocation() {
 | 
			
		||||
    var input = document.getElementById("editconfig-offlinecachelocation");
 | 
			
		||||
    var button = document.getElementById("editconfig-savebutton");
 | 
			
		||||
 | 
			
		||||
    input.classList.remove("invalidinput");
 | 
			
		||||
    button.removeAttribute("disabled");
 | 
			
		||||
 | 
			
		||||
    if (!remotefs.existsSync(input.value) || !remotefs.statSync(input.value).isDirectory()) {
 | 
			
		||||
        input.classList.add("invalidinput");
 | 
			
		||||
        button.setAttribute("disabled", "");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function loadGameVersions() {
 | 
			
		||||
    var versionJson = remotefs.readJsonSync(versionsPath);
 | 
			
		||||
    versionArray = versionJson["versions"];
 | 
			
		||||
@@ -125,14 +340,24 @@ function loadGameVersions() {
 | 
			
		||||
function loadConfig() {
 | 
			
		||||
    // Load config object globally
 | 
			
		||||
    config = remotefs.readJsonSync(configPath);
 | 
			
		||||
 | 
			
		||||
    $("#editconfig-autoupdate").prop("checked", config["autoupdate-check"]);
 | 
			
		||||
    $("#editconfig-cacheswapping").prop("checked", config["cache-swapping"]);
 | 
			
		||||
    $("#editconfig-enableofflinecache").prop("checked", config["enable-offline-cache"]);
 | 
			
		||||
    $("#editconfig-verifyofflinecache").prop("checked", config["verify-offline-cache"]);
 | 
			
		||||
 | 
			
		||||
    offlineRoot = config["offline-cache-location"] || offlineRootDefault;
 | 
			
		||||
    $("#editconfig-offlinecachelocation:text").val(offlineRoot);
 | 
			
		||||
 | 
			
		||||
    validateCacheLocation();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function loadServerList() {
 | 
			
		||||
    var serverJson = remotefs.readJsonSync(serversPath);
 | 
			
		||||
    serverArray = serverJson["servers"];
 | 
			
		||||
 | 
			
		||||
    deselectServer(); // Disable buttons until another server is selected
 | 
			
		||||
    $(".server-listing-entry").remove(); // Clear out old stuff, if any
 | 
			
		||||
    disableServerListButtons(); // Disable buttons until another server is selected
 | 
			
		||||
 | 
			
		||||
    if (serverArray.length > 0) {
 | 
			
		||||
        // Servers were found in the JSON
 | 
			
		||||
@@ -158,11 +383,293 @@ function loadServerList() {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function performCacheSwap(newVersion) {
 | 
			
		||||
    var cacheRoot = path.join(
 | 
			
		||||
        userData,
 | 
			
		||||
        "/../../LocalLow/Unity/Web Player/Cache"
 | 
			
		||||
function loadCacheList() {
 | 
			
		||||
    var versionjson = remotefs.readJsonSync(versionsPath);
 | 
			
		||||
    versionArray = versionjson["versions"];
 | 
			
		||||
 | 
			
		||||
    if (!defaultHashes) {
 | 
			
		||||
        defaultHashes = remotefs.readJsonSync(path.join(
 | 
			
		||||
            __dirname,
 | 
			
		||||
            "/defaults/hashes.json"
 | 
			
		||||
        ));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    deselectVersion();
 | 
			
		||||
    $(".cache-listing-entry").remove();
 | 
			
		||||
 | 
			
		||||
    $.each(versionArray, function (key, value) {
 | 
			
		||||
        var row = document.createElement("tr");
 | 
			
		||||
        row.className = "cache-listing-entry"
 | 
			
		||||
        row.setAttribute("id", value.name);
 | 
			
		||||
 | 
			
		||||
        var cellVersion = document.createElement("td");
 | 
			
		||||
        cellVersion.textContent = value.name;
 | 
			
		||||
        cellVersion.className = "text-monospace";
 | 
			
		||||
 | 
			
		||||
        var cellPlayableCache = getCacheInfoCell(value.name, "playable");
 | 
			
		||||
        var cellOfflineCache = getCacheInfoCell(value.name, "offline");
 | 
			
		||||
 | 
			
		||||
        row.appendChild(cellVersion);
 | 
			
		||||
        row.appendChild(cellPlayableCache);
 | 
			
		||||
        row.appendChild(cellOfflineCache);
 | 
			
		||||
 | 
			
		||||
        $("#cache-tablebody").append(row);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    storageLoadingStart();
 | 
			
		||||
    storageLoadingUpdate(cacheSizes);
 | 
			
		||||
    storageLoadingComplete(cacheSizes);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getCacheElemID(versionString, cacheMode, elementName) {
 | 
			
		||||
    return [versionString, cacheMode, "cache", elementName].filter(function (value) {
 | 
			
		||||
        return typeof value !== "undefined";
 | 
			
		||||
    }).join("-");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getCacheButtonID(versionString, cacheMode, buttonMode) {
 | 
			
		||||
    return [getCacheElemID(versionString, cacheMode), buttonMode, "button"].join("-");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getCacheLabelText(sizes) {
 | 
			
		||||
    if (!sizes || sizes.total === 0)
 | 
			
		||||
        return "?.?? GB / ?.?? GB";
 | 
			
		||||
 | 
			
		||||
    var gb = 1 << 30;
 | 
			
		||||
    var labelText = (sizes.intact / gb).toFixed(2) + " / " + (sizes.total / gb).toFixed(2) + " GB";
 | 
			
		||||
 | 
			
		||||
    if (sizes.altered > 0) {
 | 
			
		||||
        labelText += "<br/>(" + (sizes.altered / gb).toFixed(2) + " GB Altered)";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return labelText;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getCacheInfoCell(versionString, cacheMode) {
 | 
			
		||||
    var divID = getCacheElemID(versionString, cacheMode, "div");
 | 
			
		||||
    var labelID = getCacheElemID(versionString, cacheMode, "label");
 | 
			
		||||
 | 
			
		||||
    var settings = {
 | 
			
		||||
        download: {
 | 
			
		||||
            icon: "fas fa-download",
 | 
			
		||||
            class: "btn btn-success mr-1",
 | 
			
		||||
            tooltip: "Download Cache"
 | 
			
		||||
        },
 | 
			
		||||
        fix: {
 | 
			
		||||
            icon: "fas fa-hammer",
 | 
			
		||||
            class: "btn btn-warning mr-1",
 | 
			
		||||
            tooltip: "Fix Altered Files in Cache"
 | 
			
		||||
        },
 | 
			
		||||
        delete: {
 | 
			
		||||
            icon: "fas fa-trash-alt",
 | 
			
		||||
            class: "btn btn-danger mr-1",
 | 
			
		||||
            tooltip: "Delete Cache"
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    var cellCache = document.createElement("td");
 | 
			
		||||
    var divCacheAll = document.createElement("div");
 | 
			
		||||
 | 
			
		||||
    var labelCache = document.createElement("label");
 | 
			
		||||
    labelCache.setAttribute("id", labelID);
 | 
			
		||||
    labelCache.setAttribute("for", divID);
 | 
			
		||||
    labelCache.innerHTML = getCacheLabelText(
 | 
			
		||||
        (cacheSizes && cacheSizes[versionString]) ?
 | 
			
		||||
        cacheSizes[versionString][cacheMode] :
 | 
			
		||||
        null
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    var divCacheButtons = document.createElement("div");
 | 
			
		||||
    divCacheButtons.setAttribute("id", labelID);
 | 
			
		||||
 | 
			
		||||
    $.each(settings, function (buttonMode, config) {
 | 
			
		||||
        if (cacheMode === "playable" && buttonMode !== "delete") {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var buttonID = getCacheButtonID(versionString, cacheMode, buttonMode);
 | 
			
		||||
 | 
			
		||||
        var iconItalic = document.createElement("i");
 | 
			
		||||
        iconItalic.setAttribute("class", config.icon);
 | 
			
		||||
 | 
			
		||||
        var buttonCache = document.createElement("button");
 | 
			
		||||
        buttonCache.setAttribute("id", buttonID);
 | 
			
		||||
        buttonCache.setAttribute("class", config.class);
 | 
			
		||||
        buttonCache.setAttribute("title", config.tooltip);
 | 
			
		||||
        buttonCache.setAttribute("type", "button");
 | 
			
		||||
        buttonCache.setAttribute("onclick", "handleCache(\"" + buttonMode + "\", \"" + versionString + "\", \"" + cacheMode + "\");");
 | 
			
		||||
        buttonCache.appendChild(iconItalic);
 | 
			
		||||
 | 
			
		||||
        divCacheButtons.appendChild(buttonCache);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    divCacheAll.appendChild(labelCache);
 | 
			
		||||
    divCacheAll.appendChild(divCacheButtons);
 | 
			
		||||
    cellCache.appendChild(divCacheAll);
 | 
			
		||||
 | 
			
		||||
    return cellCache;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function storageLoadingStart(vString, cMode) {
 | 
			
		||||
    var versionStrings = [];
 | 
			
		||||
    $.each(versionArray, function (key, value) {
 | 
			
		||||
        if (vString) {
 | 
			
		||||
            if (vString === value.name)
 | 
			
		||||
                versionStrings.push(value.name);
 | 
			
		||||
        } else {
 | 
			
		||||
            versionStrings.push(value.name);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    var cacheModes = (cMode) ? [cMode] : ["offline", "playable"];
 | 
			
		||||
 | 
			
		||||
    deselectVersion();
 | 
			
		||||
    disableVersionAddButton();
 | 
			
		||||
 | 
			
		||||
    $.each(versionStrings, function (vKey, versionString) {
 | 
			
		||||
        $.each(cacheModes, function (cKey, cacheMode) {
 | 
			
		||||
            var buttonDelete = document.getElementById(getCacheButtonID(versionString, cacheMode, "delete"));
 | 
			
		||||
            var buttonDownload = document.getElementById(getCacheButtonID(versionString, cacheMode, "download"));
 | 
			
		||||
            var buttonFix = document.getElementById(getCacheButtonID(versionString, cacheMode, "fix"));
 | 
			
		||||
 | 
			
		||||
            if (!buttonDelete) return;
 | 
			
		||||
 | 
			
		||||
            buttonDelete.setAttribute("disabled", "");
 | 
			
		||||
            buttonDelete.children[0].setAttribute("class", "fas fa-spinner fa-spin fa-fw");
 | 
			
		||||
 | 
			
		||||
            if (cacheMode === "offline") {
 | 
			
		||||
                buttonDownload.setAttribute("disabled", "");
 | 
			
		||||
                buttonDownload.children[0].setAttribute("class", "fas fa-spinner fa-spin fa-fw");
 | 
			
		||||
 | 
			
		||||
                buttonFix.setAttribute("disabled", "");
 | 
			
		||||
                buttonFix.children[0].setAttribute("class", "fas fa-spinner fa-spin fa-fw");
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function storageLoadingUpdate(allSizes) {
 | 
			
		||||
    $.each(allSizes, function (versionString, vSizes) {
 | 
			
		||||
        $.each(vSizes, function (cacheMode, sizes) {
 | 
			
		||||
            var label = document.getElementById(getCacheElemID(versionString, cacheMode, "label"));
 | 
			
		||||
 | 
			
		||||
            cacheSizes = cacheSizes || {};
 | 
			
		||||
            cacheSizes[versionString] = cacheSizes[versionString] || {};
 | 
			
		||||
            cacheSizes[versionString][cacheMode] = sizes || {};
 | 
			
		||||
 | 
			
		||||
            if (!label) return;
 | 
			
		||||
 | 
			
		||||
            label.innerHTML = getCacheLabelText(sizes);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function storageLoadingComplete(allSizes) {
 | 
			
		||||
    $.each(allSizes, function (versionString, vSizes) {
 | 
			
		||||
        $.each(vSizes, function (cacheMode, sizes) {
 | 
			
		||||
            var buttonDelete = document.getElementById(getCacheButtonID(versionString, cacheMode, "delete"));
 | 
			
		||||
            var buttonDownload = document.getElementById(getCacheButtonID(versionString, cacheMode, "download"));
 | 
			
		||||
            var buttonFix = document.getElementById(getCacheButtonID(versionString, cacheMode, "fix"));
 | 
			
		||||
 | 
			
		||||
            if (!buttonDelete) return;
 | 
			
		||||
 | 
			
		||||
            buttonDelete.children[0].setAttribute("class", "fas fa-trash-alt");
 | 
			
		||||
 | 
			
		||||
            if (cacheMode === "offline") {
 | 
			
		||||
                buttonDownload.children[0].setAttribute("class", "fas fa-download");
 | 
			
		||||
                buttonFix.children[0].setAttribute("class", "fas fa-hammer");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (sizes.intact > 0 || sizes.altered > 0) {
 | 
			
		||||
                buttonDelete.removeAttribute("disabled");
 | 
			
		||||
 | 
			
		||||
                if (cacheMode === "offline") {
 | 
			
		||||
                    buttonDownload.setAttribute("disabled", "");
 | 
			
		||||
 | 
			
		||||
                    if (sizes.altered > 0 || sizes.intact < sizes.total) {
 | 
			
		||||
                        buttonFix.removeAttribute("disabled");
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                buttonDelete.setAttribute("disabled", "");
 | 
			
		||||
 | 
			
		||||
                if (cacheMode === "offline") {
 | 
			
		||||
                    buttonDownload.removeAttribute("disabled");
 | 
			
		||||
                    buttonFix.setAttribute("disabled", "");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    enableVersionAddButton();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleCache(operation, versionString, cacheMode, callback) {
 | 
			
		||||
    var versions = versionArray.filter(function (obj) {
 | 
			
		||||
        return obj.name === versionString;
 | 
			
		||||
    });
 | 
			
		||||
    var cdnRoot = (versions.length === 0) ? cdnString : versions[0].url;
 | 
			
		||||
 | 
			
		||||
    var lastSizes = { intact: 0, altered: 0, total: 0 };
 | 
			
		||||
    var buf = "";
 | 
			
		||||
 | 
			
		||||
    storageLoadingStart(versionString, cacheMode);
 | 
			
		||||
 | 
			
		||||
    var server = net.createServer(function (sock) {
 | 
			
		||||
        sock.setEncoding("utf8");
 | 
			
		||||
 | 
			
		||||
        sock.on("data", function (data) {
 | 
			
		||||
            buf += data;
 | 
			
		||||
 | 
			
		||||
            var end = buf.indexOf("\n");
 | 
			
		||||
 | 
			
		||||
            while (end > 0) {
 | 
			
		||||
                var sub = buf.substring(0, end);
 | 
			
		||||
                buf = buf.substring(end + 1);
 | 
			
		||||
 | 
			
		||||
                lastSizes = JSON.parse(sub);
 | 
			
		||||
                storageLoadingUpdate(lastSizes);
 | 
			
		||||
 | 
			
		||||
                end = buf.indexOf("\n");
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    server.listen(0, "localhost", function () {
 | 
			
		||||
        spawn(
 | 
			
		||||
            path.join(__dirname, "lib", "cache_handler.exe"),
 | 
			
		||||
            [
 | 
			
		||||
                "--operation", operation,
 | 
			
		||||
                // roots below contain version-agnostic main directories for caches
 | 
			
		||||
                "--playable-root", cacheRoot,
 | 
			
		||||
                "--offline-root", offlineRoot,
 | 
			
		||||
                "--user-dir", userData,
 | 
			
		||||
                // CDN root contains version-specific directory, unless cacheMode is "all"
 | 
			
		||||
                "--cdn-root", cdnRoot,
 | 
			
		||||
                "--cache-mode", cacheMode || "all",
 | 
			
		||||
                "--cache-version", versionString || "all",
 | 
			
		||||
                "--port", server.address().port,
 | 
			
		||||
                "--official-caches"
 | 
			
		||||
            ].concat(Object.keys(defaultHashes)),
 | 
			
		||||
            {
 | 
			
		||||
                stdio: "inherit"
 | 
			
		||||
            }
 | 
			
		||||
        ).on("exit", function (code, signal) {
 | 
			
		||||
            if (code !== 0 || signal) {
 | 
			
		||||
                dialog.showErrorBox(
 | 
			
		||||
                    "Sorry!",
 | 
			
		||||
                    "Process \"" + operation + "\" failed with code " + code + " and signal " + signal + "."
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            server.close();
 | 
			
		||||
            storageLoadingComplete(lastSizes);
 | 
			
		||||
            if (callback)
 | 
			
		||||
                callback(lastSizes);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function performCacheSwap(newVersion) {
 | 
			
		||||
    var currentCache = path.join(cacheRoot, "FusionFall");
 | 
			
		||||
    var newCache = path.join(cacheRoot, newVersion);
 | 
			
		||||
    var record = path.join(userData, ".lastver");
 | 
			
		||||
@@ -197,7 +704,7 @@ function performCacheSwap(newVersion) {
 | 
			
		||||
 | 
			
		||||
    // Make note of what version we are launching for next launch
 | 
			
		||||
    remotefs.writeFileSync(record, newVersion);
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    if (remotefs.existsSync(newCache) && !skip) {
 | 
			
		||||
        // Rename saved cache to FusionFall
 | 
			
		||||
        remotefs.renameSync(newCache, currentCache);
 | 
			
		||||
@@ -205,19 +712,18 @@ function performCacheSwap(newVersion) {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// For writing loginInfo.php, assetInfo.php, etc.
 | 
			
		||||
function setGameInfo(serverUUID) {
 | 
			
		||||
    var result = serverArray.filter(function (obj) {
 | 
			
		||||
function prepGameInfo(serverUUID) {
 | 
			
		||||
    var serverInfo = serverArray.filter(function (obj) {
 | 
			
		||||
        return obj.uuid === serverUUID;
 | 
			
		||||
    })[0];
 | 
			
		||||
    var gameVersion = versionArray.filter(function (obj) {
 | 
			
		||||
        return obj.name === result.version;
 | 
			
		||||
    var versionInfo = versionArray.filter(function (obj) {
 | 
			
		||||
        return obj.name === serverInfo.version;
 | 
			
		||||
    })[0];
 | 
			
		||||
 | 
			
		||||
    // If cache swapping property exists AND is `true`, run cache swapping logic
 | 
			
		||||
    if (config["cache-swapping"]) {
 | 
			
		||||
        try {
 | 
			
		||||
            performCacheSwap(gameVersion.name);
 | 
			
		||||
            performCacheSwap(versionInfo.name);
 | 
			
		||||
        } catch (ex) {
 | 
			
		||||
            console.log(
 | 
			
		||||
                "Error when swapping cache, it may get overwritten:\n" + ex
 | 
			
		||||
@@ -225,11 +731,39 @@ function setGameInfo(serverUUID) {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    window.assetUrl = gameVersion.url; // game-client.js needs to access this
 | 
			
		||||
    if (!config["enable-offline-cache"]) {
 | 
			
		||||
        // if we always ignore the offline cache, just use the URL
 | 
			
		||||
        setGameInfo(serverInfo, versionInfo.url);
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var offlinePath = path.join(offlineRoot, versionInfo.name);
 | 
			
		||||
    var offlineURL = "file:///" + offlinePath.replace(/\\/g, "/") + "/";
 | 
			
		||||
 | 
			
		||||
    if (config["verify-offline-cache"]) {
 | 
			
		||||
        // if required, do a full hash check, and use the offline cache only if it is fully intact
 | 
			
		||||
        handleCache("hash-check", versionInfo.name, "offline", function (sizes) {
 | 
			
		||||
            var versionURL = (sizes.intact < sizes.total) ? versionInfo.url : offlineURL;
 | 
			
		||||
            setGameInfo(serverInfo, versionURL);
 | 
			
		||||
        });
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // if main.unity3d is present, use the offline cache
 | 
			
		||||
    var mainPath = path.join(offlinePath, "main.unity3d");
 | 
			
		||||
    var versionURL = remotefs.existsSync(mainPath) ? versionInfo.url : offlineURL;
 | 
			
		||||
    setGameInfo(serverInfo, versionURL);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// For writing loginInfo.php, assetInfo.php, etc.
 | 
			
		||||
function setGameInfo(serverInfo, versionURL) {
 | 
			
		||||
    var versionURLRoot = versionURL.endsWith("/") ? versionURL : versionURL + "/";
 | 
			
		||||
    window.assetUrl = versionURLRoot; // game-client.js needs to access this
 | 
			
		||||
    console.log("Cache will expand from " + versionURLRoot);
 | 
			
		||||
 | 
			
		||||
    remotefs.writeFileSync(path.join(__dirname, "assetInfo.php"), assetUrl);
 | 
			
		||||
    if (result.hasOwnProperty("endpoint")) {
 | 
			
		||||
        var httpEndpoint = result.endpoint.replace("https://", "http://");
 | 
			
		||||
    if (serverInfo.hasOwnProperty("endpoint")) {
 | 
			
		||||
        var httpEndpoint = serverInfo.endpoint.replace("https://", "http://");
 | 
			
		||||
        remotefs.writeFileSync(
 | 
			
		||||
            path.join(__dirname, "rankurl.txt"),
 | 
			
		||||
            httpEndpoint + "getranks"
 | 
			
		||||
@@ -261,17 +795,17 @@ function setGameInfo(serverUUID) {
 | 
			
		||||
    // Server address parsing
 | 
			
		||||
    var address;
 | 
			
		||||
    var port;
 | 
			
		||||
    var sepPos = result.ip.indexOf(":");
 | 
			
		||||
    var sepPos = serverInfo.ip.indexOf(":");
 | 
			
		||||
    if (sepPos > -1) {
 | 
			
		||||
        address = result.ip.substr(0, sepPos);
 | 
			
		||||
        port = result.ip.substr(sepPos + 1);
 | 
			
		||||
        address = serverInfo.ip.substr(0, sepPos);
 | 
			
		||||
        port = serverInfo.ip.substr(sepPos + 1);
 | 
			
		||||
    } else {
 | 
			
		||||
        address = result.ip;
 | 
			
		||||
        address = serverInfo.ip;
 | 
			
		||||
        port = 23000; // default
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // DNS resolution. there is no synchronous version for some stupid reason
 | 
			
		||||
    if (!address.match(/^[0-9.]+$/))
 | 
			
		||||
    if (!address.match(/^[0-9.]+$/)) {
 | 
			
		||||
        dns.lookup(address, (family = 4), function (err, resolvedAddress) {
 | 
			
		||||
            if (!err) {
 | 
			
		||||
                console.log("Resolved " + address + " to " + resolvedAddress);
 | 
			
		||||
@@ -281,7 +815,7 @@ function setGameInfo(serverUUID) {
 | 
			
		||||
            }
 | 
			
		||||
            prepConnection(address, port);
 | 
			
		||||
        });
 | 
			
		||||
    else {
 | 
			
		||||
    } else {
 | 
			
		||||
        console.log(address + " is an IP; skipping DNS lookup");
 | 
			
		||||
        prepConnection(address, port);
 | 
			
		||||
    }
 | 
			
		||||
@@ -300,6 +834,10 @@ function getSelectedServer() {
 | 
			
		||||
    return $("#server-tablebody > tr.bg-primary").prop("id");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getSelectedVersion() {
 | 
			
		||||
    return $("#cache-tablebody > tr.bg-primary").prop("id");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function connectToServer() {
 | 
			
		||||
    // Get ID of the selected server, which corresponds to its UUID in the json
 | 
			
		||||
    console.log("Connecting to server with UUID of " + getSelectedServer());
 | 
			
		||||
@@ -310,7 +848,7 @@ function connectToServer() {
 | 
			
		||||
    $("#of-serverselector").fadeOut("slow", function () {
 | 
			
		||||
        setTimeout(function () {
 | 
			
		||||
            $("body,html").css("pointer-events", "");
 | 
			
		||||
            setGameInfo(getSelectedServer());
 | 
			
		||||
            prepGameInfo(getSelectedServer());
 | 
			
		||||
        }, 200);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
@@ -321,21 +859,43 @@ function deselectServer() {
 | 
			
		||||
    $(".server-listing-entry").removeClass("bg-primary");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function deselectVersion() {
 | 
			
		||||
    disableVersionListButtons();
 | 
			
		||||
    $(".cache-listing-entry").removeClass("bg-primary");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$("#server-table").on("click", ".server-listing-entry", function (event) {
 | 
			
		||||
    enableServerListButtons();
 | 
			
		||||
    $(this).addClass("bg-primary").siblings().removeClass("bg-primary");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$("#cache-table").on("click", ".cache-listing-entry", function (event) {
 | 
			
		||||
    // wait for the add button to be re-enabled first
 | 
			
		||||
    if ($("#of-addversion-button").prop("disabled")) return;
 | 
			
		||||
    // do not select default builds
 | 
			
		||||
    if (defaultHashes.hasOwnProperty($(this).attr("id"))) return;
 | 
			
		||||
 | 
			
		||||
    enableVersionListButtons();
 | 
			
		||||
    $(this).addClass("bg-primary").siblings().removeClass("bg-primary");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// QoL feature: if you double click on a server it will connect
 | 
			
		||||
$("#server-table").on("dblclick", ".server-listing-entry", function (event) {
 | 
			
		||||
    $(this).addClass("bg-primary").siblings().removeClass("bg-primary");
 | 
			
		||||
    connectToServer();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$("#of-addservermodal").on("show.bs.modal", function (e) {
 | 
			
		||||
    validateServerSave("add");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$("#of-addversionmodal").on("show.bs.modal", function (e) {
 | 
			
		||||
    validateVersionSave("add");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$("#of-editservermodal").on("show.bs.modal", function (e) {
 | 
			
		||||
    var jsonToModify = remotefs.readJsonSync(
 | 
			
		||||
        path.join(userData, "servers.json")
 | 
			
		||||
    );
 | 
			
		||||
    var jsonToModify = remotefs.readJsonSync(serversPath);
 | 
			
		||||
 | 
			
		||||
    $.each(jsonToModify["servers"], function (key, value) {
 | 
			
		||||
        if (value["uuid"] == getSelectedServer()) {
 | 
			
		||||
            $("#editserver-descinput")[0].value = value["description"];
 | 
			
		||||
@@ -350,6 +910,21 @@ $("#of-editservermodal").on("show.bs.modal", function (e) {
 | 
			
		||||
            $("#editserver-versionselect")[0].selectedIndex = versionIndex;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    validateServerSave("edit");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$("#of-editversionmodal").on("show.bs.modal", function (e) {
 | 
			
		||||
    var jsonToModify = remotefs.readJsonSync(versionsPath);
 | 
			
		||||
 | 
			
		||||
    $.each(jsonToModify["versions"], function (key, value) {
 | 
			
		||||
        if (value["name"] == getSelectedVersion()) {
 | 
			
		||||
            $("#editversion-nameinput")[0].value = value["name"];
 | 
			
		||||
            $("#editversion-urlinput")[0].value = value["url"];
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    validateVersionSave("edit");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$("#of-deleteservermodal").on("show.bs.modal", function (e) {
 | 
			
		||||
@@ -358,3 +933,19 @@ $("#of-deleteservermodal").on("show.bs.modal", function (e) {
 | 
			
		||||
    })[0];
 | 
			
		||||
    $("#deleteserver-servername").html(result.description);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$("#of-deleteversionmodal").on("show.bs.modal", function (e) {
 | 
			
		||||
    var result = versionArray.filter(function (obj) {
 | 
			
		||||
        return obj.name === getSelectedVersion();
 | 
			
		||||
    })[0];
 | 
			
		||||
    $("#deleteversion-versionname").html(result.name);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$("#of-editcacheconfigmodal").on("show.bs.modal", function (e) {
 | 
			
		||||
    if (!cacheSizes) handleCache("hash-check");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$("#of-editconfigmodal").on("show.bs.modal", function (e) {
 | 
			
		||||
    // best to keep this synced on modal show
 | 
			
		||||
    loadConfig();
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										560
									
								
								cache_handler/cache_handler.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										560
									
								
								cache_handler/cache_handler.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,560 @@
 | 
			
		||||
import json
 | 
			
		||||
import random
 | 
			
		||||
import shutil
 | 
			
		||||
import asyncio
 | 
			
		||||
import hashlib
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from dataclasses import dataclass
 | 
			
		||||
from typing import Any, Dict, List, Tuple
 | 
			
		||||
from argparse import Namespace, ArgumentParser
 | 
			
		||||
 | 
			
		||||
import httpx
 | 
			
		||||
import aiofiles
 | 
			
		||||
from bs4 import BeautifulSoup
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# hack to get pyinstaller 3.5 to work
 | 
			
		||||
if False:
 | 
			
		||||
    import anyio._backends._asyncio
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Definitions
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
BUF_SIZE = 1 << 16
 | 
			
		||||
VMDict = Dict[str, Dict[str, Dict[str, Any]]]
 | 
			
		||||
size_dict: VMDict = {}
 | 
			
		||||
hash_dict: VMDict = {}
 | 
			
		||||
hash_dict_updated: bool = False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Helper Classes
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class FileInfo:
 | 
			
		||||
    version: str
 | 
			
		||||
    mode: str
 | 
			
		||||
    local_root: Path
 | 
			
		||||
    url_root: str
 | 
			
		||||
    current_local_path: Path
 | 
			
		||||
    current_url: str
 | 
			
		||||
    sha256: str
 | 
			
		||||
 | 
			
		||||
    def resolve(self, suffix: str, sha256: str = ''):
 | 
			
		||||
        return FileInfo(
 | 
			
		||||
            version=self.version,
 | 
			
		||||
            mode=self.mode,
 | 
			
		||||
            local_root=self.local_root,
 | 
			
		||||
            url_root=self.url_root,
 | 
			
		||||
            current_local_path=(self.current_local_path / suffix),
 | 
			
		||||
            current_url=(self.current_url.rstrip('/') + '/' + suffix.lstrip('/')),
 | 
			
		||||
            sha256=(sha256 or self.sha256),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def resolve_full(self, full_path: Path, sha256: str = ''):
 | 
			
		||||
        return self.resolve(full_path.relative_to(self.local_root).as_posix(), sha256=sha256)
 | 
			
		||||
 | 
			
		||||
    def relative_path(self) -> str:
 | 
			
		||||
        return self.current_local_path.relative_to(self.local_root).as_posix()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class FileInfoGroup:
 | 
			
		||||
    version: str
 | 
			
		||||
    mode: str
 | 
			
		||||
    is_official: bool
 | 
			
		||||
    local_root: Path
 | 
			
		||||
    url_root: str
 | 
			
		||||
    file_info_list: List[FileInfo]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# IPC
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def send_message(writer: asyncio.StreamWriter) -> None:
 | 
			
		||||
    message = (json.dumps(size_dict) + '\n').encode('utf-8')
 | 
			
		||||
    writer.write(message)
 | 
			
		||||
    await writer.drain()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Hash Helpers
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_file_size_and_hash(file_path: Path) -> Tuple[int, str]:
 | 
			
		||||
    size = 0
 | 
			
		||||
    sha256 = hashlib.sha256()
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        async with aiofiles.open(file_path, mode='rb') as rb:
 | 
			
		||||
            while True:
 | 
			
		||||
                data = await rb.read(BUF_SIZE)
 | 
			
		||||
                if not data:
 | 
			
		||||
                    break
 | 
			
		||||
                sha256.update(data)
 | 
			
		||||
                size += len(data)
 | 
			
		||||
    except:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    return size, sha256.hexdigest()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def check_file_hash(file_info: FileInfo) -> bool:
 | 
			
		||||
    size, hash_str = await get_file_size_and_hash(file_info.current_local_path)
 | 
			
		||||
    file_intact = (hash_str == file_info.sha256)
 | 
			
		||||
 | 
			
		||||
    state = 'intact' if file_intact else 'altered'
 | 
			
		||||
    size_dict[file_info.version][file_info.mode][state] += size
 | 
			
		||||
 | 
			
		||||
    return file_intact
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def register_size_and_hash(file_info: FileInfo) -> None:
 | 
			
		||||
    global hash_dict_updated
 | 
			
		||||
 | 
			
		||||
    size, hash_str = await get_file_size_and_hash(file_info.current_local_path)
 | 
			
		||||
 | 
			
		||||
    size_dict[file_info.version][file_info.mode]['intact'] += size
 | 
			
		||||
    size_dict[file_info.version][file_info.mode]['total'] += size
 | 
			
		||||
 | 
			
		||||
    hash_dict[file_info.version][file_info.mode + '_size'] += size
 | 
			
		||||
    hash_dict[file_info.version][file_info.mode][file_info.relative_path()] = hash_str
 | 
			
		||||
 | 
			
		||||
    hash_dict_updated = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def unregister_size_and_hash(file_info: FileInfo) -> None:
 | 
			
		||||
    global hash_dict_updated
 | 
			
		||||
 | 
			
		||||
    size_dict[file_info.version][file_info.mode]['intact'] = 0
 | 
			
		||||
    size_dict[file_info.version][file_info.mode]['altered'] = 0
 | 
			
		||||
    size_dict[file_info.version][file_info.mode]['total'] = 0
 | 
			
		||||
 | 
			
		||||
    hash_dict[file_info.version][file_info.mode + '_size'] = 0
 | 
			
		||||
    hash_dict[file_info.version][file_info.mode].clear()
 | 
			
		||||
 | 
			
		||||
    hash_dict_updated = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Hash High-Level Helpers
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def hash_check_registered(writer: asyncio.StreamWriter, file_info_groups: List[FileInfoGroup], update_freq: int = 50) -> None:
 | 
			
		||||
    file_info_list = [file_info
 | 
			
		||||
                      for file_info_group in file_info_groups
 | 
			
		||||
                      for file_info in file_info_group.file_info_list]
 | 
			
		||||
 | 
			
		||||
    coroutines = [check_file_hash(file_info) for file_info in file_info_list]
 | 
			
		||||
    random.shuffle(coroutines)
 | 
			
		||||
 | 
			
		||||
    for i in range(0, len(coroutines), update_freq):
 | 
			
		||||
        await asyncio.gather(*coroutines[i:i+update_freq])
 | 
			
		||||
        await send_message(writer)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def hash_check_unregistered(writer: asyncio.StreamWriter, file_info_groups: List[FileInfoGroup]) -> None:
 | 
			
		||||
    for file_info_group in file_info_groups:
 | 
			
		||||
        file_info = FileInfo(
 | 
			
		||||
            version=file_info_group.version,
 | 
			
		||||
            mode=file_info_group.mode,
 | 
			
		||||
            local_root=file_info_group.local_root,
 | 
			
		||||
            url_root=file_info_group.url_root,
 | 
			
		||||
            current_local_path=file_info_group.local_root,
 | 
			
		||||
            current_url=file_info_group.url_root,
 | 
			
		||||
            sha256='',
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        path_set = {str(fi.current_local_path.resolve())
 | 
			
		||||
                    for fi in file_info_group.file_info_list}
 | 
			
		||||
 | 
			
		||||
        for file_path in file_info_group.local_root.glob('**/*'):
 | 
			
		||||
            if file_path.is_dir() or str(file_path.resolve()) in path_set:
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            # assume file is intact
 | 
			
		||||
            await register_size_and_hash(file_info.resolve_full(file_path))
 | 
			
		||||
            await send_message(writer)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Download Helpers
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def download_unregistered_file_all(writer: asyncio.StreamWriter, file_info: FileInfo) -> None:
 | 
			
		||||
    remote_path = Path(file_info.current_url.replace('file:', '', 1).lstrip('/'))
 | 
			
		||||
 | 
			
		||||
    for file_path in remote_path.glob('**/*'):
 | 
			
		||||
        if file_path.is_dir():
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
        new_file_info = file_info.resolve(file_path.relative_to(remote_path).as_posix())
 | 
			
		||||
 | 
			
		||||
        new_file_info.current_local_path.parent.mkdir(parents=True, exist_ok=True)
 | 
			
		||||
        shutil.copy(file_path, new_file_info.current_local_path)
 | 
			
		||||
 | 
			
		||||
        # assume file is intact
 | 
			
		||||
        await register_size_and_hash(new_file_info)
 | 
			
		||||
        await send_message(writer)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def download_unregistered_http_all(
 | 
			
		||||
    writer: asyncio.StreamWriter,
 | 
			
		||||
    client: httpx.AsyncClient,
 | 
			
		||||
    file_info: FileInfo,
 | 
			
		||||
    retries: int = 5,
 | 
			
		||||
    depth: int = 3,
 | 
			
		||||
) -> None:
 | 
			
		||||
    if depth == 0:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    file_info.current_local_path.mkdir(exist_ok=True)
 | 
			
		||||
 | 
			
		||||
    response = await client.get(file_info.current_url)
 | 
			
		||||
    response.raise_for_status()
 | 
			
		||||
 | 
			
		||||
    page = response.content
 | 
			
		||||
    bs = BeautifulSoup(page, 'html.parser')
 | 
			
		||||
    links = bs.find_all('a', href=True)
 | 
			
		||||
 | 
			
		||||
    for link in links:
 | 
			
		||||
        file_str = str(link['href'])
 | 
			
		||||
        new_file_info = file_info.resolve(file_str)
 | 
			
		||||
 | 
			
		||||
        if file_str == '../':
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
        if file_str.endswith('/'):
 | 
			
		||||
            await download_unregistered_http_all(
 | 
			
		||||
                writer, client, new_file_info, retries=retries, depth=(depth - 1))
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
        for i in range(retries):
 | 
			
		||||
            try:
 | 
			
		||||
                async with client.stream('GET', new_file_info.current_url) as stream:
 | 
			
		||||
                    stream.raise_for_status()
 | 
			
		||||
 | 
			
		||||
                    async with aiofiles.open(new_file_info.current_local_path, mode='wb') as wb:
 | 
			
		||||
                        async for chunk in stream.aiter_bytes(chunk_size=BUF_SIZE):
 | 
			
		||||
                            await wb.write(chunk)
 | 
			
		||||
                break
 | 
			
		||||
            except:
 | 
			
		||||
                await asyncio.sleep(i + 1)
 | 
			
		||||
 | 
			
		||||
        # assume file is intact
 | 
			
		||||
        await register_size_and_hash(new_file_info)
 | 
			
		||||
        await send_message(writer)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def download_registered_single(writer: asyncio.StreamWriter, client: httpx.AsyncClient, file_info: FileInfo, retries: int = 5) -> None:
 | 
			
		||||
    if (await check_file_hash(file_info)):
 | 
			
		||||
        await send_message(writer)
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    for i in range(retries):
 | 
			
		||||
        try:
 | 
			
		||||
            async with client.stream('GET', file_info.current_url) as stream:
 | 
			
		||||
                stream.raise_for_status()
 | 
			
		||||
 | 
			
		||||
                async with aiofiles.open(file_info.current_local_path, mode='wb') as wb:
 | 
			
		||||
                    async for chunk in stream.aiter_bytes(chunk_size=BUF_SIZE):
 | 
			
		||||
                        await wb.write(chunk)
 | 
			
		||||
        except:
 | 
			
		||||
            await asyncio.sleep(i + 1)
 | 
			
		||||
 | 
			
		||||
        if (await check_file_hash(file_info)):
 | 
			
		||||
            break
 | 
			
		||||
 | 
			
		||||
    await send_message(writer)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Download High-Level Helpers
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def download_unregistered(writer: asyncio.StreamWriter, client: httpx.AsyncClient, file_info_groups: List[FileInfoGroup]) -> None:
 | 
			
		||||
    for file_info_group in file_info_groups:
 | 
			
		||||
        file_info = FileInfo(
 | 
			
		||||
            version=file_info_group.version,
 | 
			
		||||
            mode=file_info_group.mode,
 | 
			
		||||
            local_root=file_info_group.local_root,
 | 
			
		||||
            url_root=file_info_group.url_root,
 | 
			
		||||
            current_local_path=file_info_group.local_root,
 | 
			
		||||
            current_url=file_info_group.url_root,
 | 
			
		||||
            sha256='',
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if file_info_group.url_root.startswith('http'):
 | 
			
		||||
            await download_unregistered_http_all(writer, client, file_info)
 | 
			
		||||
        else:
 | 
			
		||||
            await download_unregistered_file_all(writer, file_info)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def download_registered(writer: asyncio.StreamWriter, client: httpx.AsyncClient, file_info_groups: List[FileInfoGroup]) -> None:
 | 
			
		||||
    coroutines = []
 | 
			
		||||
 | 
			
		||||
    for file_info_group in file_info_groups:
 | 
			
		||||
        for file_info in file_info_group.file_info_list:
 | 
			
		||||
            file_info.current_local_path.parent.mkdir(parents=True, exist_ok=True)
 | 
			
		||||
            coroutines.append(download_registered_single(writer, client, file_info))
 | 
			
		||||
 | 
			
		||||
    random.shuffle(coroutines)
 | 
			
		||||
 | 
			
		||||
    await asyncio.gather(*coroutines)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Delete High-Level Helpers
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def delete_unregistered(writer: asyncio.StreamWriter, file_info_groups: List[FileInfoGroup]) -> None:
 | 
			
		||||
    for file_info_group in file_info_groups:
 | 
			
		||||
        file_info = FileInfo(
 | 
			
		||||
            version=file_info_group.version,
 | 
			
		||||
            mode=file_info_group.mode,
 | 
			
		||||
            local_root=file_info_group.local_root,
 | 
			
		||||
            url_root=file_info_group.url_root,
 | 
			
		||||
            current_local_path=file_info_group.local_root,
 | 
			
		||||
            current_url=file_info_group.url_root,
 | 
			
		||||
            sha256='',
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        shutil.rmtree(file_info.current_local_path)
 | 
			
		||||
 | 
			
		||||
        await unregister_size_and_hash(file_info)
 | 
			
		||||
        await send_message(writer)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def delete_registered(writer: asyncio.StreamWriter, file_info_groups: List[FileInfoGroup]) -> None:
 | 
			
		||||
    roots = set()
 | 
			
		||||
    for file_info_group in file_info_groups:
 | 
			
		||||
        for file_info in file_info_group.file_info_list:
 | 
			
		||||
            if file_info.current_local_path.parent.is_dir():
 | 
			
		||||
                roots.add(file_info.current_local_path.parent)
 | 
			
		||||
            if file_info.current_local_path.is_file():
 | 
			
		||||
                file_info.current_local_path.unlink()
 | 
			
		||||
 | 
			
		||||
    await send_message(writer)
 | 
			
		||||
 | 
			
		||||
    roots_list: List[Path] = sorted(roots, key=lambda p: len(p.parts), reverse=True)
 | 
			
		||||
    for root_dir in roots_list:
 | 
			
		||||
        if not any(root_dir.iterdir()):
 | 
			
		||||
            root_dir.rmdir()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Operations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def hash_check(writer: asyncio.StreamWriter, file_info_groups: List[FileInfoGroup], update_freq: int = 50) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Behavior:
 | 
			
		||||
    - get info group, if permanent, then only check hashes of files in
 | 
			
		||||
    hashes.json (either default or current)
 | 
			
		||||
    - if not permanent but has hashes registered, check the files in hashes.json
 | 
			
		||||
    then run a tree search for more files. If file new, add it into the hashes
 | 
			
		||||
    (assuming intact).
 | 
			
		||||
    - if not permanent and new, run a tree search for more files. If file new, add
 | 
			
		||||
    it into the hashes (assuming intact).
 | 
			
		||||
    """
 | 
			
		||||
    registered_groups = [file_info_group
 | 
			
		||||
                         for file_info_group in file_info_groups
 | 
			
		||||
                         if file_info_group.file_info_list]
 | 
			
		||||
    unregistered_groups = [file_info_group
 | 
			
		||||
                           for file_info_group in file_info_groups
 | 
			
		||||
                           if not file_info_group.is_official]
 | 
			
		||||
 | 
			
		||||
    if registered_groups:
 | 
			
		||||
        await hash_check_registered(writer, registered_groups, update_freq=update_freq)
 | 
			
		||||
    if unregistered_groups:
 | 
			
		||||
        await hash_check_unregistered(writer, unregistered_groups)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def download(writer: asyncio.StreamWriter, file_info_groups: List[FileInfoGroup], max_connections: int = 5) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Behavior:
 | 
			
		||||
    - get info group, if permanent, download checked with hashes.json
 | 
			
		||||
    - if not permanent but has hashes registered, download checked for the registered
 | 
			
		||||
    files. Run a recursive http or file download for the others, skipping registered
 | 
			
		||||
    files. If file new, add it into hashes (assuming intact).
 | 
			
		||||
    - if not permanent and new, run a recursive http or file download for the others.
 | 
			
		||||
    If file new, add it into hashes (assuming intact).
 | 
			
		||||
    """
 | 
			
		||||
    registered_groups = [file_info_group
 | 
			
		||||
                         for file_info_group in file_info_groups
 | 
			
		||||
                         if file_info_group.file_info_list]
 | 
			
		||||
    unregistered_groups = [file_info_group
 | 
			
		||||
                           for file_info_group in file_info_groups
 | 
			
		||||
                           if not file_info_group.is_official]
 | 
			
		||||
 | 
			
		||||
    async with httpx.AsyncClient(limits=httpx.Limits(max_connections=max_connections),
 | 
			
		||||
                                 timeout=httpx.Timeout(None)) as client:
 | 
			
		||||
        if registered_groups:
 | 
			
		||||
            await download_registered(writer, client, registered_groups)
 | 
			
		||||
        if unregistered_groups:
 | 
			
		||||
            await download_unregistered(writer, client, unregistered_groups)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def delete(writer: asyncio.StreamWriter, file_info_groups: List[FileInfoGroup]) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Behavior:
 | 
			
		||||
    - get info group, if permanent, erase files listed in hashes.json, and remove dirs
 | 
			
		||||
    from the innermost dir to the outermost, checking if they're empty.
 | 
			
		||||
    - if not permanent but has hashed registered, tree-remove the local directory, erase
 | 
			
		||||
    the entries in hashes.json
 | 
			
		||||
    - if not permanent and new, tree-remove the local directory
 | 
			
		||||
    """
 | 
			
		||||
    registered_groups = [file_info_group
 | 
			
		||||
                         for file_info_group in file_info_groups
 | 
			
		||||
                         if file_info_group.is_official]
 | 
			
		||||
    unregistered_groups = [file_info_group
 | 
			
		||||
                           for file_info_group in file_info_groups
 | 
			
		||||
                           if not file_info_group.is_official]
 | 
			
		||||
 | 
			
		||||
    if registered_groups:
 | 
			
		||||
        await delete_registered(writer, registered_groups)
 | 
			
		||||
    if unregistered_groups:
 | 
			
		||||
        await delete_unregistered(writer, unregistered_groups)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Main & Helpers
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def swapped_path(local_root: str, user_dir: str, cache_version: str, cache_mode: str) -> Path:
 | 
			
		||||
    current_cache = Path(local_root) / 'FusionFall'
 | 
			
		||||
    named_cache = Path(local_root) / cache_version
 | 
			
		||||
    record_path = Path(user_dir) / '.lastver'
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        cache_mode == 'playable' and
 | 
			
		||||
        not named_cache.is_dir() and
 | 
			
		||||
        current_cache.is_dir() and
 | 
			
		||||
        record_path.is_file() and
 | 
			
		||||
        cache_version == record_path.read_text(encoding='utf-8')
 | 
			
		||||
    ):
 | 
			
		||||
        return current_cache
 | 
			
		||||
 | 
			
		||||
    return named_cache
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def compile_file_lists(args: Namespace) -> List[FileInfoGroup]:
 | 
			
		||||
    global hash_dict_updated
 | 
			
		||||
 | 
			
		||||
    with open(Path(args.user_dir) / 'hashes.json') as r:
 | 
			
		||||
        hash_dict.update(json.load(r))
 | 
			
		||||
 | 
			
		||||
    with open(Path(args.user_dir) / 'versions.json') as r:
 | 
			
		||||
        versions = json.load(r)['versions']
 | 
			
		||||
 | 
			
		||||
    for version in versions:
 | 
			
		||||
        if version['name'] not in hash_dict:
 | 
			
		||||
            hash_dict[version['name']] = {
 | 
			
		||||
                'playable_size': 0,
 | 
			
		||||
                'offline_size': 0,
 | 
			
		||||
                'playable': {},
 | 
			
		||||
                'offline': {},
 | 
			
		||||
            }
 | 
			
		||||
            hash_dict_updated = True
 | 
			
		||||
 | 
			
		||||
    cache_modes = ['offline', 'playable'] if args.cache_mode == 'all' else [args.cache_mode]
 | 
			
		||||
    cache_versions = list(hash_dict) if args.cache_version == 'all' else [args.cache_version]
 | 
			
		||||
 | 
			
		||||
    file_info_groups = []
 | 
			
		||||
 | 
			
		||||
    for cache_version in cache_versions:
 | 
			
		||||
        for cache_mode in cache_modes:
 | 
			
		||||
            file_info_list = []
 | 
			
		||||
 | 
			
		||||
            local_root = args.offline_root if cache_mode == 'offline' else args.playable_root
 | 
			
		||||
            local_dir = swapped_path(local_root, args.user_dir, cache_version, cache_mode)
 | 
			
		||||
            url_dir = (
 | 
			
		||||
                args.cdn_root.rstrip('/') + '/' + cache_version.lstrip('/')
 | 
			
		||||
                if args.cache_version == 'all' else
 | 
			
		||||
                args.cdn_root
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            file_info_version = FileInfo(
 | 
			
		||||
                version=cache_version,
 | 
			
		||||
                mode=cache_mode,
 | 
			
		||||
                local_root=local_dir,
 | 
			
		||||
                url_root=url_dir,
 | 
			
		||||
                current_local_path=local_dir,
 | 
			
		||||
                current_url=url_dir,
 | 
			
		||||
                sha256='',
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            if cache_version not in size_dict:
 | 
			
		||||
                size_dict[cache_version] = {}
 | 
			
		||||
 | 
			
		||||
            size_dict[cache_version][cache_mode] = {
 | 
			
		||||
                'intact': 0,
 | 
			
		||||
                'altered': 0,
 | 
			
		||||
                'total': hash_dict[cache_version][cache_mode + '_size'],
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            file_info_list.extend([
 | 
			
		||||
                file_info_version.resolve(rel_path, sha256=file_hash)
 | 
			
		||||
                for rel_path, file_hash in hash_dict[cache_version][cache_mode].items()
 | 
			
		||||
            ])
 | 
			
		||||
 | 
			
		||||
            file_info_groups.append(FileInfoGroup(
 | 
			
		||||
                version=cache_version,
 | 
			
		||||
                mode=cache_mode,
 | 
			
		||||
                is_official=(cache_version in args.official_caches),
 | 
			
		||||
                local_root=local_dir,
 | 
			
		||||
                url_root=url_dir,
 | 
			
		||||
                file_info_list=file_info_list,
 | 
			
		||||
            ))
 | 
			
		||||
 | 
			
		||||
    return file_info_groups
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def write_hash_updates(args: Namespace) -> None:
 | 
			
		||||
    if not hash_dict_updated:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    for version_name in hash_dict:
 | 
			
		||||
        if version_name in args.official_caches:
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
        for cache_mode in ['playable', 'offline']:
 | 
			
		||||
            hash_dict[version_name][cache_mode] = dict(sorted(hash_dict[version_name][cache_mode].items()))
 | 
			
		||||
 | 
			
		||||
    with open(Path(args.user_dir) / 'hashes.json', 'w') as w:
 | 
			
		||||
        json.dump(hash_dict, w, indent=4)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def prep_and_run_coroutine(args: Namespace) -> None:
 | 
			
		||||
    file_info_groups = compile_file_lists(args)
 | 
			
		||||
 | 
			
		||||
    _, writer = await asyncio.open_connection('localhost', args.port)
 | 
			
		||||
 | 
			
		||||
    coroutines = {
 | 
			
		||||
        'hash-check': hash_check,
 | 
			
		||||
        'download': download,
 | 
			
		||||
        'fix': download,
 | 
			
		||||
        'delete': delete,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        await coroutines[args.operation](writer, file_info_groups)
 | 
			
		||||
    finally:
 | 
			
		||||
        await send_message(writer)
 | 
			
		||||
 | 
			
		||||
    writer.close()
 | 
			
		||||
    await writer.wait_closed()
 | 
			
		||||
 | 
			
		||||
    write_hash_updates(args)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def parse_args() -> Namespace:
 | 
			
		||||
    parser = ArgumentParser('Python executable for tasks relating to OpenFusionClient.')
 | 
			
		||||
    parser.add_argument('--operation', type=str, required=True, choices=['hash-check', 'download', 'delete'])
 | 
			
		||||
    parser.add_argument('--playable-root', dest='playable_root', type=str)
 | 
			
		||||
    parser.add_argument('--offline-root', dest='offline_root', type=str)
 | 
			
		||||
    parser.add_argument('--user-dir', dest='user_dir', type=str, required=True)
 | 
			
		||||
    parser.add_argument('--cdn-root', dest='cdn_root', type=str, default='http://cdn.dexlabs.systems/ff/big')
 | 
			
		||||
    parser.add_argument('--cache-mode', dest='cache_mode', type=str, default='all', choices=['all', 'offline', 'playable'])
 | 
			
		||||
    parser.add_argument('--cache-version', dest='cache_version', type=str, default='all')
 | 
			
		||||
    parser.add_argument('--port', type=str, required=True)
 | 
			
		||||
    parser.add_argument('--official-caches', dest='official_caches', nargs='*', type=str, default=[])
 | 
			
		||||
    return parser.parse_args()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    asyncio.run(prep_and_run_coroutine(parse_args()))
 | 
			
		||||
							
								
								
									
										33
									
								
								cache_handler/cache_handler.spec
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								cache_handler/cache_handler.spec
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
# -*- mode: python ; coding: utf-8 -*-
 | 
			
		||||
 | 
			
		||||
block_cipher = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
a = Analysis(['cache_handler.py'],
 | 
			
		||||
             pathex=['Z:\\src'],
 | 
			
		||||
             binaries=[],
 | 
			
		||||
             datas=[],
 | 
			
		||||
             hiddenimports=[],
 | 
			
		||||
             hookspath=[],
 | 
			
		||||
             runtime_hooks=[],
 | 
			
		||||
             excludes=[],
 | 
			
		||||
             win_no_prefer_redirects=False,
 | 
			
		||||
             win_private_assemblies=False,
 | 
			
		||||
             cipher=block_cipher,
 | 
			
		||||
             noarchive=False)
 | 
			
		||||
pyz = PYZ(a.pure, a.zipped_data,
 | 
			
		||||
             cipher=block_cipher)
 | 
			
		||||
exe = EXE(pyz,
 | 
			
		||||
          a.scripts,
 | 
			
		||||
          a.binaries,
 | 
			
		||||
          a.zipfiles,
 | 
			
		||||
          a.datas,
 | 
			
		||||
          [],
 | 
			
		||||
          name='cache_handler',
 | 
			
		||||
          debug=False,
 | 
			
		||||
          bootloader_ignore_signals=False,
 | 
			
		||||
          strip=False,
 | 
			
		||||
          upx=True,
 | 
			
		||||
          upx_exclude=[],
 | 
			
		||||
          runtime_tmpdir=None,
 | 
			
		||||
          console=True )
 | 
			
		||||
							
								
								
									
										4
									
								
								cache_handler/requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								cache_handler/requirements.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
aiofiles
 | 
			
		||||
httpx
 | 
			
		||||
beautifulsoup4
 | 
			
		||||
pyinstaller==3.5
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
    "autoupdate-check": true,
 | 
			
		||||
    "cache-swapping": true,
 | 
			
		||||
    "last-version-initialized": "1.5"
 | 
			
		||||
    "enable-offline-cache": false,
 | 
			
		||||
    "verify-offline-cache": false,
 | 
			
		||||
    "last-version-initialized": "1.6"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										35695
									
								
								defaults/hashes.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35695
									
								
								defaults/hashes.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										447
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										447
									
								
								index.html
									
									
									
									
									
								
							@@ -108,6 +108,30 @@
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="col-4 d-inline-flex justify-content-end">
 | 
			
		||||
                        <button
 | 
			
		||||
                            class="btn btn-primary mr-1"
 | 
			
		||||
                            data-toggle="modal"
 | 
			
		||||
                            data-bs-tooltip=""
 | 
			
		||||
                            data-placement="bottom"
 | 
			
		||||
                            id="of-editconfig-button"
 | 
			
		||||
                            type="button"
 | 
			
		||||
                            title="Edit Configuration"
 | 
			
		||||
                            data-target="#of-editconfigmodal"
 | 
			
		||||
                        >
 | 
			
		||||
                            <i class="fas fa-cog"></i>
 | 
			
		||||
                        </button>
 | 
			
		||||
                        <button
 | 
			
		||||
                            class="btn btn-primary mr-1"
 | 
			
		||||
                            data-toggle="modal"
 | 
			
		||||
                            data-bs-tooltip=""
 | 
			
		||||
                            data-placement="bottom"
 | 
			
		||||
                            id="of-editcache-button"
 | 
			
		||||
                            type="button"
 | 
			
		||||
                            title="Edit Game Builds"
 | 
			
		||||
                            data-target="#of-editcacheconfigmodal"
 | 
			
		||||
                        >
 | 
			
		||||
                            <i class="fas fa-database"></i>
 | 
			
		||||
                        </button>
 | 
			
		||||
                        <button
 | 
			
		||||
                            class="btn btn-primary disabled"
 | 
			
		||||
                            id="of-connect-button"
 | 
			
		||||
@@ -159,6 +183,13 @@
 | 
			
		||||
                                data-target="#of-restoreserversmodal"
 | 
			
		||||
                                >Reset to Default Servers</a
 | 
			
		||||
                            >
 | 
			
		||||
                            <a
 | 
			
		||||
                                href="#of-restoreversionsmodal"
 | 
			
		||||
                                onclick="$('#of-aboutmodal').modal('toggle')"
 | 
			
		||||
                                data-toggle="modal"
 | 
			
		||||
                                data-target="#of-restoreversionsmodal"
 | 
			
		||||
                                >Reset to Default Game Builds</a
 | 
			
		||||
                            >
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="modal-footer">
 | 
			
		||||
                            <div class="row flex-fill">
 | 
			
		||||
@@ -169,7 +200,7 @@
 | 
			
		||||
                                        data-bs-tooltip=""
 | 
			
		||||
                                        type="button"
 | 
			
		||||
                                        title="Github Page"
 | 
			
		||||
                                        onclick="window.open('https://github.com/OpenFusionProject/OpenFusion','_blank');"
 | 
			
		||||
                                        onclick="window.open('https://github.com/OpenFusionProject/OpenFusion','_blank');"
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <i
 | 
			
		||||
                                            class="fab fa-github"
 | 
			
		||||
@@ -181,7 +212,7 @@
 | 
			
		||||
                                        data-bs-tooltip=""
 | 
			
		||||
                                        type="button"
 | 
			
		||||
                                        title="Discord Chat"
 | 
			
		||||
                                        onclick="window.open('https://discord.gg/DYavckB','_blank');"
 | 
			
		||||
                                        onclick="window.open('https://discord.gg/DYavckB','_blank');"
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <i
 | 
			
		||||
                                            class="fab fa-discord"
 | 
			
		||||
@@ -238,6 +269,7 @@
 | 
			
		||||
                                    required=""
 | 
			
		||||
                                    minlength="1"
 | 
			
		||||
                                    maxlength="70"
 | 
			
		||||
                                    oninput="validateServerSave('add')"
 | 
			
		||||
                                /><label for="addserver-ipinput"
 | 
			
		||||
                                    >Server IP</label
 | 
			
		||||
                                ><input
 | 
			
		||||
@@ -245,8 +277,10 @@
 | 
			
		||||
                                    type="text"
 | 
			
		||||
                                    id="addserver-ipinput"
 | 
			
		||||
                                    placeholder="127.0.0.1:23000"
 | 
			
		||||
                                    value="127.0.0.1:23000"
 | 
			
		||||
                                    required=""
 | 
			
		||||
                                    pattern="^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]):[0-9]+$"
 | 
			
		||||
                                    pattern="^([-a-zA-Z0-9]+\.)+[-a-zA-Z0-9]+:[0-9]+$"
 | 
			
		||||
                                    oninput="validateServerSave('add')"
 | 
			
		||||
                                /><label for="addserver-versionselect"
 | 
			
		||||
                                    >Game Version: </label
 | 
			
		||||
                                ><select
 | 
			
		||||
@@ -254,6 +288,7 @@
 | 
			
		||||
                                    id="addserver-versionselect"
 | 
			
		||||
                                    required=""
 | 
			
		||||
                                    style="margin-left: -5px"
 | 
			
		||||
                                    oninput="validateServerSave('add')"
 | 
			
		||||
                                ></select>
 | 
			
		||||
                            </form>
 | 
			
		||||
                        </div>
 | 
			
		||||
@@ -310,7 +345,8 @@
 | 
			
		||||
                                    required=""
 | 
			
		||||
                                    minlength="1"
 | 
			
		||||
                                    maxlength="70"
 | 
			
		||||
                                /><label for="addserver-ipinput"
 | 
			
		||||
                                    oninput="validateServerSave('edit')"
 | 
			
		||||
                                /><label for="editserver-ipinput"
 | 
			
		||||
                                    >Server IP</label
 | 
			
		||||
                                ><input
 | 
			
		||||
                                    class="form-control form-row w-75"
 | 
			
		||||
@@ -318,7 +354,8 @@
 | 
			
		||||
                                    id="editserver-ipinput"
 | 
			
		||||
                                    placeholder="127.0.0.1:23000"
 | 
			
		||||
                                    required=""
 | 
			
		||||
                                    pattern="^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]):[0-9]+$"
 | 
			
		||||
                                    pattern="^([-a-zA-Z0-9]+\.)+[-a-zA-Z0-9]+:[0-9]+$"
 | 
			
		||||
                                    oninput="validateServerSave('edit')"
 | 
			
		||||
                                /><label for="editserver-versionselect"
 | 
			
		||||
                                    >Game Version: </label
 | 
			
		||||
                                ><select
 | 
			
		||||
@@ -326,6 +363,7 @@
 | 
			
		||||
                                    id="editserver-versionselect"
 | 
			
		||||
                                    required=""
 | 
			
		||||
                                    style="margin-left: -5px"
 | 
			
		||||
                                    oninput="validateServerSave('edit')"
 | 
			
		||||
                                ></select>
 | 
			
		||||
                            </form>
 | 
			
		||||
                        </div>
 | 
			
		||||
@@ -339,7 +377,7 @@
 | 
			
		||||
                                Cancel</button
 | 
			
		||||
                            ><button
 | 
			
		||||
                                class="btn btn-primary border rounded border-primary btn-success border-success"
 | 
			
		||||
                                id="addserver-savebutton"
 | 
			
		||||
                                id="editserver-savebutton"
 | 
			
		||||
                                type="submit"
 | 
			
		||||
                                data-dismiss="modal"
 | 
			
		||||
                                form="editserver-form"
 | 
			
		||||
@@ -432,7 +470,7 @@
 | 
			
		||||
                                Cancel</button
 | 
			
		||||
                            ><button
 | 
			
		||||
                                class="btn btn-primary border rounded border-primary btn-danger border-danger"
 | 
			
		||||
                                id="deleteserver-button"
 | 
			
		||||
                                id="restoreservers-button"
 | 
			
		||||
                                type="button"
 | 
			
		||||
                                data-dismiss="modal"
 | 
			
		||||
                                onclick="restoreDefaultServers();"
 | 
			
		||||
@@ -443,6 +481,401 @@
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div
 | 
			
		||||
                class="modal fade"
 | 
			
		||||
                role="dialog"
 | 
			
		||||
                tabindex="-1"
 | 
			
		||||
                id="of-editconfigmodal"
 | 
			
		||||
            >
 | 
			
		||||
                <div class="modal-dialog modal-dialog-centered" role="document">
 | 
			
		||||
                    <div class="modal-content">
 | 
			
		||||
                        <div class="modal-header">
 | 
			
		||||
                            <h4 class="modal-title">Edit Configuration</h4>
 | 
			
		||||
                            <button
 | 
			
		||||
                                type="button"
 | 
			
		||||
                                class="close"
 | 
			
		||||
                                data-dismiss="modal"
 | 
			
		||||
                                aria-label="Close"
 | 
			
		||||
                            >
 | 
			
		||||
                                <span aria-hidden="true">×</span>
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="modal-body">
 | 
			
		||||
                            <form id="editconfig-form" class="needs-validation">
 | 
			
		||||
                                <label for="editconfig-autoupdate"
 | 
			
		||||
                                    >Automatically update the client:</label
 | 
			
		||||
                                ><input
 | 
			
		||||
                                    class="form-control form-row w-75"
 | 
			
		||||
                                    type="checkbox"
 | 
			
		||||
                                    id="editconfig-autoupdate"
 | 
			
		||||
                                />
 | 
			
		||||
                                <label for="editconfig-cacheswapping"
 | 
			
		||||
                                    >Swap game caches to avoid unnecessary downloads:</label
 | 
			
		||||
                                ><input
 | 
			
		||||
                                    class="form-control form-row w-75"
 | 
			
		||||
                                    type="checkbox"
 | 
			
		||||
                                    id="editconfig-cacheswapping"
 | 
			
		||||
                                />
 | 
			
		||||
                                <label for="editconfig-enableofflinecache"
 | 
			
		||||
                                    >Use offline caches when they are available:</label
 | 
			
		||||
                                ><input
 | 
			
		||||
                                    class="form-control form-row w-75"
 | 
			
		||||
                                    type="checkbox"
 | 
			
		||||
                                    id="editconfig-enableofflinecache"
 | 
			
		||||
                                />
 | 
			
		||||
                                <label for="editconfig-verifyofflinecache"
 | 
			
		||||
                                    >Verify offline caches every time they are loaded:</label
 | 
			
		||||
                                ><input
 | 
			
		||||
                                    class="form-control form-row w-75"
 | 
			
		||||
                                    type="checkbox"
 | 
			
		||||
                                    id="editconfig-verifyofflinecache"
 | 
			
		||||
                                />
 | 
			
		||||
                                <label for="editconfig-offlinecachelocation"
 | 
			
		||||
                                    >Select Offline Cache Location:</label
 | 
			
		||||
                                ><input
 | 
			
		||||
                                    class="form-control form-row w-75"
 | 
			
		||||
                                    id="editconfig-offlinecachelocation"
 | 
			
		||||
                                    type="text"
 | 
			
		||||
                                    oninput="validateCacheLocation()"
 | 
			
		||||
                                />
 | 
			
		||||
                            </form>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="modal-footer">
 | 
			
		||||
                            <button
 | 
			
		||||
                                class="btn btn-primary border rounded border-primary btn-danger border-danger"
 | 
			
		||||
                                id="editconfig-cancel"
 | 
			
		||||
                                type="button"
 | 
			
		||||
                                data-dismiss="modal"
 | 
			
		||||
                            >
 | 
			
		||||
                                Cancel</button
 | 
			
		||||
                            ><button
 | 
			
		||||
                                class="btn btn-primary border rounded border-primary btn-success border-success"
 | 
			
		||||
                                id="editconfig-savebutton"
 | 
			
		||||
                                type="submit"
 | 
			
		||||
                                data-dismiss="modal"
 | 
			
		||||
                                form="editconfig-form"
 | 
			
		||||
                                onclick="editConfig();"
 | 
			
		||||
                            >
 | 
			
		||||
                                Save
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div
 | 
			
		||||
                class="modal fade"
 | 
			
		||||
                role="dialog"
 | 
			
		||||
                tabindex="-1"
 | 
			
		||||
                id="of-editcacheconfigmodal"
 | 
			
		||||
            >
 | 
			
		||||
                <div class="modal-dialog modal-dialog-centered modal-lg" role="document">
 | 
			
		||||
                    <div class="modal-content">
 | 
			
		||||
                        <div class="modal-header">
 | 
			
		||||
                            <h4 class="modal-title">Edit Game Builds</h4>
 | 
			
		||||
                            <button
 | 
			
		||||
                                type="button"
 | 
			
		||||
                                class="close"
 | 
			
		||||
                                data-dismiss="modal"
 | 
			
		||||
                                aria-label="Close"
 | 
			
		||||
                            >
 | 
			
		||||
                                <span aria-hidden="true">×</span>
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="modal-body">
 | 
			
		||||
                            <div
 | 
			
		||||
                                class="row row-cols-2 d-xl-flex justify-content-center justify-content-xl-start"
 | 
			
		||||
                                id="of-versionbuttons"
 | 
			
		||||
                                style="padding-bottom: 16px"
 | 
			
		||||
                            >
 | 
			
		||||
                                <div
 | 
			
		||||
                                    class="col-4 text-left d-inline-flex justify-content-xl-start"
 | 
			
		||||
                                    id="cache-buttons"
 | 
			
		||||
                                >
 | 
			
		||||
                                    <button
 | 
			
		||||
                                        class="btn btn-success mr-1"
 | 
			
		||||
                                        data-toggle="modal"
 | 
			
		||||
                                        data-bs-tooltip=""
 | 
			
		||||
                                        data-placement="bottom"
 | 
			
		||||
                                        id="of-addversion-button"
 | 
			
		||||
                                        type="button"
 | 
			
		||||
                                        title="Add Version"
 | 
			
		||||
                                        data-target="#of-addversionmodal"
 | 
			
		||||
                                        onclick="deselectVersion()"
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <i class="fas fa-plus"></i>
 | 
			
		||||
                                    </button>
 | 
			
		||||
                                    <button
 | 
			
		||||
                                        class="btn btn-primary mr-1 disabled"
 | 
			
		||||
                                        data-toggle="modal"
 | 
			
		||||
                                        data-bs-tooltip=""
 | 
			
		||||
                                        data-placement="bottom"
 | 
			
		||||
                                        id="of-editversion-button"
 | 
			
		||||
                                        type="button"
 | 
			
		||||
                                        title="Edit Version"
 | 
			
		||||
                                        data-target="#of-editversionmodal"
 | 
			
		||||
                                        disabled=""
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <i class="fas fa-edit"></i>
 | 
			
		||||
                                    </button>
 | 
			
		||||
                                    <button
 | 
			
		||||
                                        class="btn btn-danger mr-1 disabled"
 | 
			
		||||
                                        data-toggle="modal"
 | 
			
		||||
                                        data-bs-tooltip=""
 | 
			
		||||
                                        data-placement="bottom"
 | 
			
		||||
                                        id="of-deleteversion-button"
 | 
			
		||||
                                        type="button"
 | 
			
		||||
                                        title="Delete Version"
 | 
			
		||||
                                        data-target="#of-deleteversionmodal"
 | 
			
		||||
                                        disabled=""
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <i class="fas fa-trash-alt"></i>
 | 
			
		||||
                                    </button>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div
 | 
			
		||||
                                class="table-responsive text-center border rounded border-primary"
 | 
			
		||||
                                id="cache-table"
 | 
			
		||||
                            >
 | 
			
		||||
                                <table class="table table-striped table-hover mb-0">
 | 
			
		||||
                                    <thead>
 | 
			
		||||
                                        <tr>
 | 
			
		||||
                                            <th>Game Version</th>
 | 
			
		||||
                                            <th>Game Cache</th>
 | 
			
		||||
                                            <th>Offline Cache</th>
 | 
			
		||||
                                        </tr>
 | 
			
		||||
                                    </thead>
 | 
			
		||||
                                    <tbody id="cache-tablebody">
 | 
			
		||||
                                    </tbody>
 | 
			
		||||
                                </table>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div
 | 
			
		||||
                class="modal fade"
 | 
			
		||||
                role="dialog"
 | 
			
		||||
                tabindex="-1"
 | 
			
		||||
                id="of-addversionmodal"
 | 
			
		||||
            >
 | 
			
		||||
                <div class="modal-dialog modal-dialog-centered" role="document">
 | 
			
		||||
                    <div class="modal-content">
 | 
			
		||||
                        <div class="modal-header">
 | 
			
		||||
                            <h4 class="modal-title">Add Server</h4>
 | 
			
		||||
                            <button
 | 
			
		||||
                                type="button"
 | 
			
		||||
                                class="close"
 | 
			
		||||
                                data-dismiss="modal"
 | 
			
		||||
                                aria-label="Close"
 | 
			
		||||
                            >
 | 
			
		||||
                                <span aria-hidden="true">×</span>
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="modal-body">
 | 
			
		||||
                            <form id="addversion-form" class="needs-validation">
 | 
			
		||||
                                <label for="addversion-nameinput"
 | 
			
		||||
                                    >Version Name</label
 | 
			
		||||
                                ><input
 | 
			
		||||
                                    class="form-control form-row w-75"
 | 
			
		||||
                                    type="text"
 | 
			
		||||
                                    id="addversion-nameinput"
 | 
			
		||||
                                    placeholder="custom-build-000"
 | 
			
		||||
                                    required=""
 | 
			
		||||
                                    pattern="^[-a-zA-Z0-9_]{1,70}$"
 | 
			
		||||
                                    oninput="validateVersionSave('add')"
 | 
			
		||||
                                /><label for="addversion-urlinput"
 | 
			
		||||
                                    >Version URL</label
 | 
			
		||||
                                ><input
 | 
			
		||||
                                    class="form-control form-row w-75"
 | 
			
		||||
                                    type="text"
 | 
			
		||||
                                    id="addversion-urlinput"
 | 
			
		||||
                                    placeholder="http://cdn.dexlabs.systems/custom-build-000/"
 | 
			
		||||
                                    required=""
 | 
			
		||||
                                    pattern="^(https?|file):\/\/\/?([-a-zA-Z0-9@:%._\+~#= ]{1,256}\/){1,64}$"
 | 
			
		||||
                                    oninput="validateVersionSave('add')"
 | 
			
		||||
                                />
 | 
			
		||||
                            </form>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="modal-footer">
 | 
			
		||||
                            <button
 | 
			
		||||
                                class="btn btn-primary border rounded border-primary btn-danger border-danger"
 | 
			
		||||
                                id="addversion-cancel"
 | 
			
		||||
                                type="button"
 | 
			
		||||
                                data-dismiss="modal"
 | 
			
		||||
                            >
 | 
			
		||||
                                Cancel</button
 | 
			
		||||
                            ><button
 | 
			
		||||
                                class="btn btn-primary border rounded border-primary btn-success border-success"
 | 
			
		||||
                                id="addversion-savebutton"
 | 
			
		||||
                                type="submit"
 | 
			
		||||
                                data-dismiss="modal"
 | 
			
		||||
                                form="addversion-form"
 | 
			
		||||
                                onclick="addVersion();"
 | 
			
		||||
                            >
 | 
			
		||||
                                Save
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div
 | 
			
		||||
                class="modal fade"
 | 
			
		||||
                role="dialog"
 | 
			
		||||
                tabindex="-1"
 | 
			
		||||
                id="of-editversionmodal"
 | 
			
		||||
            >
 | 
			
		||||
                <div class="modal-dialog modal-dialog-centered" role="document">
 | 
			
		||||
                    <div class="modal-content">
 | 
			
		||||
                        <div class="modal-header">
 | 
			
		||||
                            <h4 class="modal-title">Edit Version</h4>
 | 
			
		||||
                            <button
 | 
			
		||||
                                type="button"
 | 
			
		||||
                                class="close"
 | 
			
		||||
                                data-dismiss="modal"
 | 
			
		||||
                                aria-label="Close"
 | 
			
		||||
                            >
 | 
			
		||||
                                <span aria-hidden="true">×</span>
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="modal-body">
 | 
			
		||||
                            <form id="editversion-form" class="needs-validation">
 | 
			
		||||
                                <label for="editversion-nameinput"
 | 
			
		||||
                                    >Version Name</label
 | 
			
		||||
                                ><input
 | 
			
		||||
                                    class="form-control form-row w-75"
 | 
			
		||||
                                    type="text"
 | 
			
		||||
                                    id="editversion-nameinput"
 | 
			
		||||
                                    placeholder="custom-build-000"
 | 
			
		||||
                                    required=""
 | 
			
		||||
                                    pattern="^[-a-zA-Z0-9_]{1,70}$"
 | 
			
		||||
                                    oninput="validateVersionSave('edit')"
 | 
			
		||||
                                /><label for="editversion-urlinput"
 | 
			
		||||
                                    >Version URL</label
 | 
			
		||||
                                ><input
 | 
			
		||||
                                    class="form-control form-row w-75"
 | 
			
		||||
                                    type="text"
 | 
			
		||||
                                    id="editversion-urlinput"
 | 
			
		||||
                                    placeholder="http://cdn.dexlabs.systems/custom-build-000/"
 | 
			
		||||
                                    required=""
 | 
			
		||||
                                    pattern="^(https?|file):\/\/\/?([-a-zA-Z0-9@:%._\+~#= ]{1,256}\/){1,64}$"
 | 
			
		||||
                                    oninput="validateVersionSave('edit')"
 | 
			
		||||
                                />
 | 
			
		||||
                            </form>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="modal-footer">
 | 
			
		||||
                            <button
 | 
			
		||||
                                class="btn btn-primary border rounded border-primary btn-danger border-danger"
 | 
			
		||||
                                id="editversion-cancel"
 | 
			
		||||
                                type="button"
 | 
			
		||||
                                data-dismiss="modal"
 | 
			
		||||
                            >
 | 
			
		||||
                                Cancel</button
 | 
			
		||||
                            ><button
 | 
			
		||||
                                class="btn btn-primary border rounded border-primary btn-success border-success"
 | 
			
		||||
                                id="editversion-savebutton"
 | 
			
		||||
                                type="submit"
 | 
			
		||||
                                data-dismiss="modal"
 | 
			
		||||
                                form="editversion-form"
 | 
			
		||||
                                onclick="editVersion();"
 | 
			
		||||
                            >
 | 
			
		||||
                                Save
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div
 | 
			
		||||
                class="modal fade"
 | 
			
		||||
                role="dialog"
 | 
			
		||||
                tabindex="-1"
 | 
			
		||||
                id="of-deleteversionmodal"
 | 
			
		||||
            >
 | 
			
		||||
                <div class="modal-dialog modal-dialog-centered" role="document">
 | 
			
		||||
                    <div class="modal-content">
 | 
			
		||||
                        <div class="modal-header">
 | 
			
		||||
                            <h4 class="modal-title">Are you sure?</h4>
 | 
			
		||||
                            <button
 | 
			
		||||
                                type="button"
 | 
			
		||||
                                class="close"
 | 
			
		||||
                                data-dismiss="modal"
 | 
			
		||||
                                aria-label="Close"
 | 
			
		||||
                            >
 | 
			
		||||
                                <span aria-hidden="true">×</span>
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="modal-body">
 | 
			
		||||
                            <p class="lead">
 | 
			
		||||
                                Do you really want to delete<br />"<a
 | 
			
		||||
                                    id="deleteversion-versionname"
 | 
			
		||||
                                    >VERSION_NAME</a
 | 
			
		||||
                                >"?<br /><br />You could always re-add it later.
 | 
			
		||||
                            </p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="modal-footer">
 | 
			
		||||
                            <button
 | 
			
		||||
                                class="btn btn-primary border rounded border-primary"
 | 
			
		||||
                                type="button"
 | 
			
		||||
                                data-dismiss="modal"
 | 
			
		||||
                            >
 | 
			
		||||
                                Cancel</button
 | 
			
		||||
                            ><button
 | 
			
		||||
                                class="btn btn-primary border rounded border-primary btn-danger border-danger"
 | 
			
		||||
                                id="deleteversion-button"
 | 
			
		||||
                                type="button"
 | 
			
		||||
                                data-dismiss="modal"
 | 
			
		||||
                                onclick="deleteVersion();"
 | 
			
		||||
                            >
 | 
			
		||||
                                Yes, Delete
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div
 | 
			
		||||
                class="modal fade"
 | 
			
		||||
                role="dialog"
 | 
			
		||||
                tabindex="-1"
 | 
			
		||||
                id="of-restoreversionsmodal"
 | 
			
		||||
            >
 | 
			
		||||
                <div class="modal-dialog modal-dialog-centered" role="document">
 | 
			
		||||
                    <div class="modal-content">
 | 
			
		||||
                        <div class="modal-header">
 | 
			
		||||
                            <h4 class="modal-title">Are you sure?</h4>
 | 
			
		||||
                            <button
 | 
			
		||||
                                type="button"
 | 
			
		||||
                                class="close"
 | 
			
		||||
                                data-dismiss="modal"
 | 
			
		||||
                                aria-label="Close"
 | 
			
		||||
                            >
 | 
			
		||||
                                <span aria-hidden="true">×</span>
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="modal-body">
 | 
			
		||||
                            <p class="lead">
 | 
			
		||||
                                Do you really want to restore the default
 | 
			
		||||
                                versions?
 | 
			
		||||
                            </p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="modal-footer">
 | 
			
		||||
                            <button
 | 
			
		||||
                                class="btn btn-primary border rounded border-primary"
 | 
			
		||||
                                type="button"
 | 
			
		||||
                                data-dismiss="modal"
 | 
			
		||||
                            >
 | 
			
		||||
                                Cancel</button
 | 
			
		||||
                            ><button
 | 
			
		||||
                                class="btn btn-primary border rounded border-primary btn-danger border-danger"
 | 
			
		||||
                                id="restoreversions-button"
 | 
			
		||||
                                type="button"
 | 
			
		||||
                                data-dismiss="modal"
 | 
			
		||||
                                onclick="restoreDefaultVersions();"
 | 
			
		||||
                            >
 | 
			
		||||
                                Yes, Restore
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div id="of-versionnumberdiv">
 | 
			
		||||
                <a
 | 
			
		||||
                    id="of-versionnumber"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								index.js
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								index.js
									
									
									
									
									
								
							@@ -28,14 +28,16 @@ var userData = app.getPath("userData");
 | 
			
		||||
var configPath = path.join(userData, "config.json");
 | 
			
		||||
var serversPath = path.join(userData, "servers.json");
 | 
			
		||||
var versionsPath = path.join(userData, "versions.json");
 | 
			
		||||
var hashPath = path.join(userData, "hashes.json");
 | 
			
		||||
 | 
			
		||||
function initialSetup(firstTime) {
 | 
			
		||||
    if (!firstTime) {
 | 
			
		||||
        // Migration from pre-1.4
 | 
			
		||||
        // Migration from pre-1.6
 | 
			
		||||
        // Back everything up, just in case
 | 
			
		||||
        fs.copySync(configPath, configPath + ".bak");
 | 
			
		||||
        fs.copySync(serversPath, serversPath + ".bak");
 | 
			
		||||
        fs.copySync(versionsPath, versionsPath + ".bak");
 | 
			
		||||
        fs.copySync(hashPath, hashPath + ".bak");
 | 
			
		||||
    } else {
 | 
			
		||||
        // First-time setup
 | 
			
		||||
        // Copy default servers
 | 
			
		||||
@@ -48,6 +50,7 @@ function initialSetup(firstTime) {
 | 
			
		||||
    // Copy default versions and config
 | 
			
		||||
    fs.copySync(path.join(__dirname, "/defaults/versions.json"), versionsPath);
 | 
			
		||||
    fs.copySync(path.join(__dirname, "/defaults/config.json"), configPath);
 | 
			
		||||
    fs.copySync(path.join(__dirname, "/defaults/hashes.json"), hashPath);
 | 
			
		||||
 | 
			
		||||
    console.log("JSON files copied.");
 | 
			
		||||
    showMainWindow();
 | 
			
		||||
@@ -90,8 +93,8 @@ app.on("ready", function () {
 | 
			
		||||
            initialSetup(true);
 | 
			
		||||
        } else {
 | 
			
		||||
            var config = fs.readJsonSync(configPath);
 | 
			
		||||
            if (!config["last-version-initialized"]) {
 | 
			
		||||
                console.log("Pre-1.4 config detected. Running migration.");
 | 
			
		||||
            if (config["last-version-initialized"] !== "1.6") {
 | 
			
		||||
                console.log("Pre-1.6 config detected. Running migration.");
 | 
			
		||||
                initialSetup(false);
 | 
			
		||||
            } else {
 | 
			
		||||
                showMainWindow();
 | 
			
		||||
@@ -128,6 +131,7 @@ function showMainWindow() {
 | 
			
		||||
        mainWindow.webContents.executeJavaScript("loadConfig();");
 | 
			
		||||
        mainWindow.webContents.executeJavaScript("loadGameVersions();");
 | 
			
		||||
        mainWindow.webContents.executeJavaScript("loadServerList();");
 | 
			
		||||
        mainWindow.webContents.executeJavaScript("loadCacheList();");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    mainWindow.webContents.on("plugin-crashed", function () {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								lib/cache_handler.exe
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								lib/cache_handler.exe
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
		Reference in New Issue
	
	Block a user