config: fix issue related to loading the settings on initial launch (#109)

This commit is contained in:
Tyler Wilding 2022-07-22 23:50:40 -04:00 committed by GitHub
parent 00f6218015
commit 2cee538376
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 282 additions and 246 deletions

View file

@ -30,7 +30,6 @@
},
"dependencies": {
"@tauri-apps/api": "^1.0.2",
"svelte-navigator": "^3.1.6",
"tauri-plugin-store-api": "github:tauri-apps/tauri-plugin-store"
"svelte-navigator": "^3.1.6"
}
}

13
src-tauri/Cargo.lock generated
View file

@ -61,7 +61,6 @@ dependencies = [
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-store",
]
[[package]]
@ -2846,18 +2845,6 @@ dependencies = [
"tauri-utils",
]
[[package]]
name = "tauri-plugin-store"
version = "0.0.0"
source = "git+https://github.com/tauri-apps/tauri-plugin-store#17cf4d781c82a6c6578c472068c11136c0652d50"
dependencies = [
"log",
"serde",
"serde_json",
"tauri",
"thiserror",
]
[[package]]
name = "tauri-runtime"
version = "0.10.2"

View file

@ -19,10 +19,6 @@ serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.0.3", features = ["api-all", "devtools"] }
[dependencies.tauri-plugin-store]
git = "https://github.com/tauri-apps/tauri-plugin-store"
#branch = "main"
[features]
# by default Tauri runs in production mode
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL

View file

@ -4,7 +4,6 @@
)]
use tauri::RunEvent;
use tauri_plugin_store::PluginBuilder;
mod commands;
use commands::close_splashscreen;
@ -14,7 +13,6 @@ use commands::copy_dir;
fn main() {
tauri::Builder::default()
.plugin(PluginBuilder::default().build())
.invoke_handler(tauri::generate_handler![
get_highest_simd,
open_dir,

View file

@ -1,5 +1,5 @@
<script type="ts">
import { getGameInstallVersion, setInstallStatus } from "$lib/config";
import { launcherConfig } from "$lib/config";
import { getInternalName, SupportedGame } from "$lib/constants";
import { launchGame } from "$lib/launch";
import { openDir } from "$lib/rpc/commands";
@ -15,7 +15,7 @@ import { createEventDispatcher, onMount } from "svelte";
let gameVersion = undefined;
onMount(async () => {
gameVersion = await getGameInstallVersion(activeGame);
gameVersion = await launcherConfig.getGameInstallVersion(activeGame);
configPath = await join(await configDir(), "OpenGOAL", getInternalName(activeGame));
componentLoaded = true;
});
@ -25,7 +25,7 @@ import { createEventDispatcher, onMount } from "svelte";
}
async function onClickUninstall() {
await setInstallStatus(SupportedGame.Jak1, false);
await launcherConfig.setInstallStatus(SupportedGame.Jak1, false);
dispatch('change');
}
@ -45,7 +45,7 @@ import { createEventDispatcher, onMount } from "svelte";
{#if componentLoaded}
<div id="launcherControls">
<button class="btn lg" on:click={onClickPlay}>Play</button>
<p>Game Version: {gameVersion}</p>
<p class="text-shadow">Game Version: {gameVersion}</p>
<!-- TODO - when clicking decompile/compile -- show logs -->
<div class="mt-1">
<button class="btn md" on:click={() => openDir(configPath)}>Settings and Saves</button>
@ -55,3 +55,9 @@ import { createEventDispatcher, onMount } from "svelte";
</div>
</div>
{/if}
<style>
.text-shadow {
text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;
}
</style>

View file

@ -1,7 +1,7 @@
<script type="ts">
import { areRequirementsMet } from "$lib/config";
import { launcherConfig } from "$lib/config";
import { gameNeedsReinstall, isInstalling, ProcessLogs } from "$lib/stores/AppStore";
import { fullInstallation, recompileGame } from "$lib/setup/setup";
import { checkRequirements, fullInstallation, recompileGame } from "$lib/setup/setup";
// components
import Progress from "./Progress.svelte";
// constants
@ -18,7 +18,11 @@
let requirementsMet = false;
onMount(async () => {
requirementsMet = await areRequirementsMet();
// NOTE - potentially has problems if the user changes hardware
if (!(await launcherConfig.areRequirementsMet())) {
await checkRequirements();
}
requirementsMet = await launcherConfig.areRequirementsMet();
componentLoaded = true;
});

View file

@ -1,5 +1,5 @@
<script>
import { InstallStatus } from "../../../lib/stores/AppStore";
import { InstallStatus } from "$lib/stores/AppStore";
import { tweened } from "svelte/motion";
import { cubicOut } from "svelte/easing";

View file

@ -1,5 +1,5 @@
<script type="ts">
import { isAVXRequirementMet, isOpenGLRequirementMet } from "$lib/config";
import { launcherConfig } from "$lib/config";
import { onMount } from "svelte";
@ -9,8 +9,8 @@ import { onMount } from "svelte";
let isOpenGLMet = false;
onMount(async () => {
isAVXMet = await isAVXRequirementMet();
isOpenGLMet = await isOpenGLRequirementMet();
isAVXMet = await launcherConfig.isAVXRequirementMet();
isOpenGLMet = await launcherConfig.isOpenGLRequirementMet();
componentLoaded = true;
});
</script>

View file

@ -1,212 +1,255 @@
import { createDir, readTextFile, writeFile } from "@tauri-apps/api/fs";
import { appDir, join } from "@tauri-apps/api/path";
import { Store } from "tauri-plugin-store-api";
import { SupportedGame } from "./constants";
import { fileExists } from "./utils/file";
import { log } from "./utils/log";
class GameConfig {
isInstalled: boolean = false;
version: string = null;
interface Serializable<T> {
deserialize(input: Object): T;
}
// TODO: LINK REQUIREMENTS TO CHECK REQUIREMENTS FUNCTION TO AVOID RUNNING FUNCTION IF REQUIREMENTS ARE MET
class LauncherConfig {
version = "1.0";
requirements: { avx: boolean | null; openGL: boolean | null } = {
avx: null,
openGL: null,
};
class GameConfig implements Serializable<GameConfig> {
isInstalled: boolean = false;
version: string = null;
deserialize(json: JSON): GameConfig {
this.isInstalled = json["isInstalled"];
this.version = json["version"];
return this;
}
}
class GameRequirements implements Serializable<GameRequirements> {
avx: boolean | null = null;
openGL: boolean | null = null;
deserialize(json: JSON): GameRequirements {
this.avx = json["avx"];
this.openGL = json["openGL"];
return this;
}
}
export class LauncherConfig implements Serializable<LauncherConfig> {
version: string | null = "1.0";
requirements: GameRequirements = new GameRequirements();
games = {
[SupportedGame.Jak1]: new GameConfig(),
[SupportedGame.Jak2]: new GameConfig(),
[SupportedGame.Jak3]: new GameConfig(),
[SupportedGame.JakX]: new GameConfig(),
};
lastActiveGame: SupportedGame;
}
lastActiveGame: SupportedGame | null;
const store = new Store("settings.json");
private _loaded: boolean = false;
/**
* Checks the version to enable safe config operations
* @param {*} version "<major>.<minor>"
* @returns True if majors match, and expected minor greater than or equal to stored. False otherwise, or if no version can be found
*/
async function validVersion(version: string): Promise<boolean> {
let [major, minor] = version.split(".");
await store.load();
if (!(await store.has("version"))) {
return false;
deserialize(json: JSON): LauncherConfig {
this.version = json["version"];
this.requirements = new GameRequirements().deserialize(
json["requirements"]
);
this.games[SupportedGame.Jak1] = new GameConfig().deserialize(
json["games"][SupportedGame.Jak1]
);
this.games[SupportedGame.Jak2] = new GameConfig().deserialize(
json["games"][SupportedGame.Jak2]
);
this.games[SupportedGame.Jak3] = new GameConfig().deserialize(
json["games"][SupportedGame.Jak3]
);
this.games[SupportedGame.JakX] = new GameConfig().deserialize(
json["games"][SupportedGame.JakX]
);
this.lastActiveGame = json["lastActiveGame"];
return this;
}
let [storedMajor, storedMinor]: string[] = (await store.get("version")).split(
"."
);
if (major != storedMajor) {
return false;
}
if (parseInt(minor) < parseInt(storedMinor)) {
return false;
}
return true;
}
export async function initConfig() {
const path = await join(await appDir(), "settings.json");
let configExists = await fileExists(path);
if (!configExists) {
log.info("settings file not found or could not be loaded!");
await createDir(await appDir(), { recursive: true });
private async loadConfigFromFile() {
if (this._loaded) {
return;
}
const configPath = await join(await appDir(), "settings.json");
const configExists = await fileExists(configPath);
if (!configExists) {
console.log(
`[Launcher]: Settings file not found at '${configPath}, initializing with defaults!`
);
await createDir(await appDir(), { recursive: true });
await writeFile({
contents: JSON.stringify(this, null, 2),
path: configPath,
});
console.log("[Launcher]: Settings file initialized");
} else {
const data = await readTextFile(configPath);
this.deserialize(JSON.parse(data));
}
this._loaded = true;
}
private async saveConfigToFile() {
if (!this._loaded) {
log.info("config not loaded when trying to save, initializing it first!");
await this.loadConfigFromFile();
return;
}
const path = await join(await appDir(), "settings.json");
await writeFile({
contents: JSON.stringify(new LauncherConfig(), null, 2),
contents: JSON.stringify(this, null, 2),
path: path,
});
log.info("settings file initialized");
}
}
/**
* If a game is installed or not
* @param {string} supportedGame
* @returns {Promise<boolean>}
*/
export async function getInstallStatus(
supportedGame: SupportedGame
): Promise<boolean> {
await store.load();
if (!(await validVersion("1.0"))) {
return false;
}
// TODO: create a proper type for gameConfigs - exists with 'LauncherConfig'
const gameConfigs: object = await store.get("games");
if (gameConfigs == null || !(supportedGame in gameConfigs)) {
return false;
}
return gameConfigs[supportedGame].isInstalled;
}
/**
* The last game that was considered active in the launcher
* @returns {Promise<SupportedGame | null>}
*/
export async function getLastActiveGame(): Promise<SupportedGame> {
await store.load();
if (!(await validVersion("1.0"))) {
return null;
}
const lastActiveGame: SupportedGame = await store.get("lastActiveGame");
return lastActiveGame;
}
/**
* @param {string} supportedGame
* @param {boolean} installed
* @returns
*/
export async function setInstallStatus(
supportedGame: SupportedGame,
installed: boolean
): Promise<void> {
await store.load();
if (!(await validVersion("1.0"))) {
return;
}
// TODO: create a proper type for gameConfigs - 'LauncherConfig'
let gameConfigs: object = await store.get("games");
// NOTE: Do we need this conditional? Considering we generate the store file this condition should never happen.
if (gameConfigs == null || !(supportedGame in gameConfigs)) {
return;
}
gameConfigs[supportedGame].isInstalled = installed;
await store.set("games", gameConfigs);
await store.save();
}
export async function setRequirementsMet(
avx: boolean = null,
openGL: boolean = null
) {
await store.load();
await store.set("requirements", { avx, openGL });
await store.save();
return;
}
/**
* Checks the user config file to see if avx and openGL requirements are met.
*/
export async function areRequirementsMet(): Promise<boolean> {
if ((await isAVXRequirementMet()) && (await isOpenGLRequirementMet())) {
/**
* Checks the version to enable safe config operations
*
* Assumes the config has been loaded before calling to reduce boilerplate
*
* @param {*} version "<major>.<minor>"
* @returns True if majors match, and expected minor greater than or equal to stored. False otherwise, or if no version can be found
*/
private validVersion(requiredVersion: string): boolean {
const [requiredMajor, requiredMinor] = requiredVersion.split(".");
if (this.version === null) {
return false;
}
const [storedMajor, storedMinor] = this.version.split(".");
if (requiredMajor != storedMajor) {
return false;
}
if (parseInt(requiredMinor) < parseInt(storedMinor)) {
return false;
}
return true;
}
return false;
}
export async function isAVXRequirementMet(): Promise<boolean> {
await store.load();
let requirements = await store.get("requirements");
if (!requirements["avx"]) {
log.error("requirement false - AVX unsupported");
return false;
}
return true;
}
// GETTERS
export async function isOpenGLRequirementMet(): Promise<boolean> {
await store.load();
let requirements = await store.get("requirements");
if (!requirements["openGL"]) {
log.error("requirement false - OpenGL unsupported");
return false;
}
return true;
}
export async function getGameInstallVersion(
game: SupportedGame
): Promise<String> {
// TODO - this can fail on first time startup from splash (where the config is init)
// no idea why yet
await store.load();
let games: GameConfig = await store.get("games");
const { version } = games[game];
return version;
}
export async function setGameInstallVersion(game: SupportedGame) {
const version = await getLatestToolsVersion();
await store.load();
let games: GameConfig = await store.get("games");
games[game].version = version;
await store.set("games", games);
return await store.save();
}
export async function getLatestToolsVersion(): Promise<String> {
const appDirPath = await appDir();
const userMetaPath = await join(appDirPath, "data", "metadata.json");
const data = await readTextFile(userMetaPath);
const { version } = JSON.parse(data);
return version;
}
export async function shouldUpdateGameInstall(
game: SupportedGame
): Promise<Boolean> {
const installVersion = await getGameInstallVersion(game);
if (installVersion === null || installVersion === undefined) {
return false;
}
const toolsVersion = await getLatestToolsVersion();
if (installVersion === toolsVersion) {
return false;
/**
* If a game is installed or not
* @param {string} supportedGame
* @returns {Promise<boolean>}
*/
async getInstallStatus(supportedGame: SupportedGame): Promise<boolean> {
await this.loadConfigFromFile();
if (!this.validVersion("1.0")) {
return false;
}
const gameConfigs = this.games;
if (gameConfigs === null || !(supportedGame in gameConfigs)) {
return false;
}
return gameConfigs[supportedGame].isInstalled;
}
log.warn("Tools version is different than install verison", {
tools: toolsVersion,
installed: installVersion,
});
return true;
/**
* The last game that was considered active in the launcher
* @returns {Promise<SupportedGame | null>}
*/
async getLastActiveGame(): Promise<SupportedGame | null> {
await this.loadConfigFromFile();
if (!this.validVersion("1.0")) {
return null;
}
return this.lastActiveGame;
}
/**
* Checks the user config file to see if avx and openGL requirements are met.
*/
async areRequirementsMet(): Promise<boolean> {
await this.loadConfigFromFile();
if (!this.validVersion("1.0")) {
return false;
}
return this.requirements.avx && this.requirements.openGL;
}
async isAVXRequirementMet(): Promise<boolean> {
await this.loadConfigFromFile();
if (!this.validVersion("1.0")) {
log.error("requirement false - AVX unsupported");
return false;
}
return this.requirements.avx;
}
async isOpenGLRequirementMet(): Promise<boolean> {
await this.loadConfigFromFile();
if (!this.validVersion("1.0")) {
log.error("requirement false - OpenGL 4.3 unsupported");
return false;
}
return this.requirements.openGL;
}
async getGameInstallVersion(game: SupportedGame): Promise<String> {
await this.loadConfigFromFile();
if (!this.validVersion("1.0")) {
return null;
}
return this.games[game].version;
}
private async getLatestProjectBinaryVersion(): Promise<string | null> {
// TODO - make a LauncherMetadata class similar to this
const appDirPath = await appDir();
const userMetaPath = await join(appDirPath, "data", "metadata.json");
// TODO - ensure it exists!
const data = await readTextFile(userMetaPath);
const { version } = JSON.parse(data);
return version;
}
async shouldUpdateGameInstall(game: SupportedGame): Promise<Boolean> {
const installVersion = await this.getGameInstallVersion(game);
if (installVersion === null || installVersion === undefined) {
return false;
}
const toolsVersion = await this.getLatestProjectBinaryVersion();
if (installVersion === toolsVersion) {
return false;
}
log.warn("Tools version is different than install verison", {
tools: toolsVersion,
installed: installVersion,
});
return true;
}
// SETTERS
/**
* @param {string} supportedGame
* @param {boolean} installed
* @returns
*/
async setInstallStatus(
supportedGame: SupportedGame,
installed: boolean
): Promise<void> {
this.games[supportedGame].isInstalled = installed;
await this.saveConfigToFile();
}
async setRequirementsMet(
avx: boolean = null,
openGL: boolean = null
): Promise<void> {
this.requirements.avx = avx;
this.requirements.openGL = openGL;
await this.saveConfigToFile();
}
async setGameInstallVersion(game: SupportedGame) {
const version = await this.getLatestProjectBinaryVersion();
this.games[game].version = version;
await this.saveConfigToFile();
}
}
// Initialize with defaults
export let launcherConfig: LauncherConfig = new LauncherConfig();

View file

@ -5,13 +5,10 @@ import { getHighestSimd } from "$lib/rpc/commands";
import { InstallStatus, isInstalling } from "../stores/AppStore";
import { SETUP_ERROR, SETUP_SUCCESS, SupportedGame } from "$lib/constants";
import { filePrompt } from "$lib/utils/file";
import {
setGameInstallVersion,
setInstallStatus,
setRequirementsMet,
} from "../config";
import { launcherConfig } from "$lib/config";
import { resolveErrorCode } from "./setup_errors";
import { installLog, log } from "$lib/utils/log";
import { ProcessLogs } from "$lib/stores/AppStore";
let sidecarOptions = {};
@ -27,7 +24,7 @@ export async function isAVXSupported() {
if (highestSIMD.toLowerCase().startsWith("avx")) {
return true;
}
throw new Error("UNSUPPORTED AVX");
return false;
}
/**
@ -38,7 +35,8 @@ export async function isOpenGLVersionSupported(
version: string
): Promise<boolean> {
if ((await os.platform()) === "darwin") {
throw new Error("Unsupported OS!");
// TODO - log!
return false;
}
// Otherwise, query for the version
let command = Command.sidecar("bin/glewinfo", ["-version", version]);
@ -47,21 +45,22 @@ export async function isOpenGLVersionSupported(
return true;
}
log.error("opengl requirement check failed", {
version: version,
statusCode: output.code,
stdout: output.stdout,
stderr: output.stderr,
});
throw new Error("UNSUPPORTED OPENGL VERSION");
return false;
}
export async function checkRequirements(): Promise<Boolean> {
export async function checkRequirements(): Promise<void> {
try {
await isAVXSupported();
await isOpenGLVersionSupported("4.3");
await setRequirementsMet(true, true);
return true;
const isAVX = await isAVXSupported();
const isOpenGL = await isOpenGLVersionSupported("4.3");
console.log(`avx - ${isAVX} opengl - ${isOpenGL}`);
await launcherConfig.setRequirementsMet(isAVX, isOpenGL);
} catch (err) {
return false;
await launcherConfig.setRequirementsMet(false, false);
}
}
@ -109,11 +108,13 @@ export async function extractAndValidateISO(
installLog.info(output.stdout, {
game: SupportedGame.Jak1,
});
ProcessLogs.update((currLogs) => currLogs + output.stdout);
}
if (output.stderr) {
installLog.error(output.stderr, {
game: SupportedGame.Jak1,
});
ProcessLogs.update((currLogs) => currLogs + output.stderr);
}
if (output.code === 0) {
return true;
@ -143,11 +144,13 @@ export async function decompileGameData(filePath: string): Promise<boolean> {
installLog.info(output.stdout, {
game: SupportedGame.Jak1,
});
ProcessLogs.update((currLogs) => currLogs + output.stdout);
}
if (output.stderr) {
installLog.error(output.stderr, {
game: SupportedGame.Jak1,
});
ProcessLogs.update((currLogs) => currLogs + output.stderr);
}
if (output.code === 0) {
return true;
@ -175,11 +178,13 @@ export async function compileGame(filePath: string): Promise<Boolean> {
installLog.info(output.stdout, {
game: SupportedGame.Jak1,
});
ProcessLogs.update((currLogs) => currLogs + output.stdout);
}
if (output.stderr) {
installLog.error(output.stderr, {
game: SupportedGame.Jak1,
});
ProcessLogs.update((currLogs) => currLogs + output.stderr);
}
if (output.code === 0) {
InstallStatus.update(() => SETUP_SUCCESS.ready);
@ -193,12 +198,13 @@ export async function fullInstallation(game: SupportedGame): Promise<boolean> {
isInstalling.update(() => true);
try {
isoPath = await isoPrompt();
ProcessLogs.update(() => "");
await extractAndValidateISO(isoPath);
await decompileGameData(isoPath);
await compileGame(isoPath);
await setInstallStatus(game, true);
await launcherConfig.setInstallStatus(game, true);
isInstalling.update(() => false);
await setGameInstallVersion(game);
await launcherConfig.setGameInstallVersion(game);
return true;
} catch (err) {
installLog.error("unexpected error encountered", {
@ -226,7 +232,7 @@ export async function recompileGame(game: SupportedGame) {
await decompileGameData(isoPath);
await compileGame(isoPath);
// update settings.json with latest tools version from metadata.json
await setGameInstallVersion(game);
await launcherConfig.setGameInstallVersion(game);
isInstalling.update(() => false);
} catch (err) {
installLog.error("unexpected error encountered", {

View file

@ -6,4 +6,4 @@ export const InstallStatus = writable({
});
export const isInstalling = writable(false);
export const gameNeedsReinstall = writable(false);
export const ProcessLogs = writable();
export const ProcessLogs = writable("");

View file

@ -1,6 +1,6 @@
<script>
import { fade } from "svelte/transition";
import { getInstallStatus, shouldUpdateGameInstall } from "$lib/config";
import { launcherConfig } from "$lib/config";
import { fromRoute, getGameTitle, SupportedGame } from "$lib/constants";
import { useParams } from "svelte-navigator";
import GameContent from "../components/games/GameControls.svelte";
@ -31,7 +31,7 @@
activeGame = fromRoute($params["game_name"]);
}
isGameInstalled = await getInstallStatus(activeGame);
isGameInstalled = await launcherConfig.getInstallStatus(activeGame);
// Do some checks before the user can play the game
// First, let's see if their data directory needs updating
@ -39,7 +39,7 @@
dataDirUpToDate = await isDataDirectoryUpToDate();
// If it's up to date we'll do the second check now, does their game need to be re-compiled?
if (dataDirUpToDate) {
if (await shouldUpdateGameInstall(activeGame)) {
if (await launcherConfig.shouldUpdateGameInstall(activeGame)) {
// await recompileGame(activeGame);
gameNeedsReinstall.update(() => true);
}
@ -53,7 +53,7 @@
try {
await copyDataDirectory();
// Now that the directory is up to date, let's see if they need to reinstall the game
if (await shouldUpdateGameInstall(activeGame)) {
if (await launcherConfig.shouldUpdateGameInstall(activeGame)) {
gameNeedsReinstall.update(() => true);
}
} catch (err) {
@ -63,13 +63,13 @@
}
async function updateGameState(evt) {
isGameInstalled = await getInstallStatus(activeGame);
isGameInstalled = await launcherConfig.getInstallStatus(activeGame);
}
</script>
{#if componentLoaded}
<div class="flex-center" in:fade>
<h1>
<h1 class="text-shadow">
{getGameTitle(activeGame)}
</h1>
{#if isGameInstalled && !$gameNeedsReinstall}
@ -102,3 +102,9 @@
{:else}
<!-- TODO - component library - spinner -->
{/if}
<style>
.text-shadow {
text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;
}
</style>

View file

@ -1,7 +1,5 @@
<script>
import { closeSplashScreen } from "$lib/rpc/commands";
import { areRequirementsMet, initConfig } from "$lib/config";
import { checkRequirements } from "$lib/setup/setup";
import { onMount } from "svelte";
import logo from "$assets/images/icon.webp";
import { copyDataDirectory, dataDirectoryExists } from "$lib/utils/data-files";
@ -12,13 +10,6 @@ import { log } from "$lib/utils/log";
// Events
onMount(async () => {
await initConfig();
currentStatusText = "Checking Requirements";
currentProgress = 10;
// NOTE - potentially has problems if the user changes hardware
if (!(await areRequirementsMet())) {
await checkRequirements();
}
currentStatusText = "Checking Data Files";
currentProgress = 25;