From e4eb7b52c0b4aec2cedd6a931ba62c8afeca2afa Mon Sep 17 00:00:00 2001 From: jonanoj <4640274+jonanoj@users.noreply.github.com> Date: Sat, 26 Jul 2025 16:07:59 +0300 Subject: [PATCH 1/9] Add deploy git-bash script --- deploy-dev.sh | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 deploy-dev.sh diff --git a/deploy-dev.sh b/deploy-dev.sh new file mode 100644 index 0000000..a610072 --- /dev/null +++ b/deploy-dev.sh @@ -0,0 +1,4 @@ +set -x +rm -rf "$APPDATA/Vortex/plugins/tgfoadev" +mkdir -p "$APPDATA/Vortex/plugins/tgfoadev" +cp -r dist/* "$APPDATA/Vortex/plugins/tgfoadev/" \ No newline at end of file From 02bb01021b2d9ab385292b679d3af52f3465efa2 Mon Sep 17 00:00:00 2001 From: jonanoj <4640274+jonanoj@users.noreply.github.com> Date: Sat, 26 Jul 2025 17:34:10 +0300 Subject: [PATCH 2/9] Add prettier --- .prettierignore | 3 +++ .prettierrc | 1 + package.json | 1 + yarn.lock | 5 +++++ 4 files changed, 10 insertions(+) create mode 100644 .prettierignore create mode 100644 .prettierrc diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..c2bc31c --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +# Ignore artifacts: +build +dist diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.prettierrc @@ -0,0 +1 @@ +{} diff --git a/package.json b/package.json index 6618994..49ddab0 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": {}, "devDependencies": { "modtype-bepinex": "Nexus-Mods/extension-modtype-bepinex", + "prettier": "3.6.2", "ts-loader": "^9.5.2", "typescript": "^5.8.3", "vortex-api": "Nexus-Mods/vortex-api", diff --git a/yarn.lock b/yarn.lock index bff129e..6be6ea5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -642,6 +642,11 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +prettier@3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.6.2.tgz#ccda02a1003ebbb2bfda6f83a074978f608b9393" + integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ== + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" From f9986c2e980e888d137170b144cdda55ee980a44 Mon Sep 17 00:00:00 2001 From: jonanoj <4640274+jonanoj@users.noreply.github.com> Date: Sat, 26 Jul 2025 17:36:34 +0300 Subject: [PATCH 3/9] prettier --write --- .github/workflows/release.yml | 2 +- README.md | 4 +- assets/info.json | 2 +- copyassets.js | 40 ++--- package.json | 44 ++--- src/index.ts | 308 ++++++++++++++++++---------------- tsconfig.json | 38 ++--- webpack.config.js | 8 +- 8 files changed, 231 insertions(+), 215 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e6b9476..d55959b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '22.x' + node-version: "22.x" - run: npm install -g yarn - run: yarn install - run: yarn build diff --git a/README.md b/README.md index ec7cae6..4534084 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Tainted Grail - The Fall of Avalon Vortex Extension -This extension adds support for managing BepInEx mods for Tainted Grail: The Fall of Avalon in Vortex Mod Manager. +This extension adds support for managing BepInEx mods for Tainted Grail: The Fall of Avalon in Vortex Mod Manager. ## Build @@ -16,4 +16,4 @@ Copy the contents of `dist` to `%APPDATA%\Vortex\plugins\tgfoadev` rmdir /S /Q %APPDATA%\Vortex\plugins\tgfoadev mkdir %APPDATA%\Vortex\plugins\tgfoadev xcopy /Y /E /I dist %APPDATA%\Vortex\plugins\tgfoadev -``` \ No newline at end of file +``` diff --git a/assets/info.json b/assets/info.json index 9e7931c..030621f 100644 --- a/assets/info.json +++ b/assets/info.json @@ -3,4 +3,4 @@ "author": "jonanoj", "version": "1.1.0", "description": "Support for Tainted Grail: The Fall of Avalon mods using BepInEx" -} \ No newline at end of file +} diff --git a/copyassets.js b/copyassets.js index da8acbb..ec28aa1 100644 --- a/copyassets.js +++ b/copyassets.js @@ -1,29 +1,29 @@ -import fs from 'fs/promises'; -import path from 'path'; -import { fileURLToPath } from 'url'; +import fs from "fs/promises"; +import path from "path"; +import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const assetPath = path.join(__dirname, 'assets'); -const destPath = path.join(__dirname, 'dist'); +const assetPath = path.join(__dirname, "assets"); +const destPath = path.join(__dirname, "dist"); async function start() { - try { - const assets = await fs.readdir(assetPath); - for (const asset of assets) { - try { - await fs.copyFile(path.join(assetPath, asset), path.join(destPath, asset)); - } - catch (err) { - console.error('Error copying asset', { asset, err }); - } - } - - } - catch (err) { - console.error('Failed to copy assets!', err); + try { + const assets = await fs.readdir(assetPath); + for (const asset of assets) { + try { + await fs.copyFile( + path.join(assetPath, asset), + path.join(destPath, asset), + ); + } catch (err) { + console.error("Error copying asset", { asset, err }); + } } + } catch (err) { + console.error("Failed to copy assets!", err); + } } -await start(); \ No newline at end of file +await start(); diff --git a/package.json b/package.json index 49ddab0..5571ae3 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,23 @@ { - "name": "tgfoa-bepinex", - "version": "1.1.0", - "description": "Support for Tainted Grail: The Fall of Avalon mods using BepInEx", - "author": "jonanoj", - "main": "./out/index.js", - "scripts": { - "copyassets": "node copyassets.js", - "build": "webpack --config webpack.config.js && extractInfo && yarn copyassets" - }, - "repository": "git@github.com:jonanoj/TGFoAVortexExtension.git", - "license": "MIT", - "dependencies": {}, - "devDependencies": { - "modtype-bepinex": "Nexus-Mods/extension-modtype-bepinex", - "prettier": "3.6.2", - "ts-loader": "^9.5.2", - "typescript": "^5.8.3", - "vortex-api": "Nexus-Mods/vortex-api", - "webpack": "^5.99.9", - "webpack-cli": "^6.0.1" - } -} \ No newline at end of file + "name": "tgfoa-bepinex", + "version": "1.1.0", + "description": "Support for Tainted Grail: The Fall of Avalon mods using BepInEx", + "author": "jonanoj", + "main": "./out/index.js", + "scripts": { + "copyassets": "node copyassets.js", + "build": "webpack --config webpack.config.js && extractInfo && yarn copyassets" + }, + "repository": "git@github.com:jonanoj/TGFoAVortexExtension.git", + "license": "MIT", + "dependencies": {}, + "devDependencies": { + "modtype-bepinex": "Nexus-Mods/extension-modtype-bepinex", + "prettier": "3.6.2", + "ts-loader": "^9.5.2", + "typescript": "^5.8.3", + "vortex-api": "Nexus-Mods/vortex-api", + "webpack": "^5.99.9", + "webpack-cli": "^6.0.1" + } +} diff --git a/src/index.ts b/src/index.ts index c9d7039..011f7fa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,170 +1,190 @@ -import path from 'path'; -import { fs, util, type types } from 'vortex-api'; -import type { IBepInExGameConfig, INexusDownloadInfo } from '../node_modules/modtype-bepinex/src/types'; - -const GAME_ID = 'taintedgrailthefallofavalon' -const STEAM_ID = '1466060' -const GOG_ID = '1887281589' +import path from "path"; +import { fs, util, type types } from "vortex-api"; +import type { + IBepInExGameConfig, + INexusDownloadInfo, +} from "../node_modules/modtype-bepinex/src/types"; + +const GAME_ID = "taintedgrailthefallofavalon"; +const STEAM_ID = "1466060"; +const GOG_ID = "1887281589"; const GAME_DATA_DIR = "Fall of Avalon_Data"; const IL2CPP_DIR = path.join(GAME_DATA_DIR, "il2cpp_data"); -const MANAGED_DLL_DIR = path.join(GAME_DATA_DIR, "Managed") -const GAME_EXE_FILENAME = 'Fall of Avalon.exe' +const MANAGED_DLL_DIR = path.join(GAME_DATA_DIR, "Managed"); +const GAME_EXE_FILENAME = "Fall of Avalon.exe"; const BEPINEX5_DOWNLOAD_INFO: INexusDownloadInfo = { - gameId: GAME_ID, - domainId: GAME_ID, - modId: '50', - fileId: '162', - archiveName: 'BepInEx Mono Windows x64-50-5-4-23-3-1752802821.zip', - architecture: 'x64', - version: '5.4.23.3', - allowAutoInstall: false, -} + gameId: GAME_ID, + domainId: GAME_ID, + modId: "50", + fileId: "162", + archiveName: "BepInEx Mono Windows x64-50-5-4-23-3-1752802821.zip", + architecture: "x64", + version: "5.4.23.3", + allowAutoInstall: false, +}; const BEPINEX6_DOWNLOAD_INFO: INexusDownloadInfo = { - gameId: GAME_ID, - domainId: GAME_ID, - modId: '16', - fileId: '100', - archiveName: 'BepinEx-16-6-0-0-be-735b-1749459702.zip', - architecture: 'x64', - version: '6.0.0-be+735b', // Semver compliant version https://semver.org/#backusnaur-form-grammar-for-valid-semver-versions - allowAutoInstall: false, -} + gameId: GAME_ID, + domainId: GAME_ID, + modId: "16", + fileId: "100", + archiveName: "BepinEx-16-6-0-0-be-735b-1749459702.zip", + architecture: "x64", + version: "6.0.0-be+735b", // Semver compliant version https://semver.org/#backusnaur-form-grammar-for-valid-semver-versions + allowAutoInstall: false, +}; const enum InstallationType { - Mono = 'Mono (v5)', - IL2CPP = 'IL2CPP (v6)', + Mono = "Mono (v5)", + IL2CPP = "IL2CPP (v6)", } async function doesDirectoryExist(dir: string): Promise { - try { - const stat = await fs.statSilentAsync(dir) - return stat && stat.isDirectory() - } catch { - return false - } + try { + const stat = await fs.statSilentAsync(dir); + return stat && stat.isDirectory(); + } catch { + return false; + } } async function checkGameBuild(gamePath: string): Promise { - console.debug("Looking for Mono dir") - if (await doesDirectoryExist(path.join(gamePath, MANAGED_DLL_DIR))) { - console.debug("Found Mono dir") - const monoDlls = (await fs.readdirAsync(path.join(gamePath, MANAGED_DLL_DIR))).filter((f: string) => f.startsWith('Mono.') && f.endsWith('.dll')); - if (monoDlls.length > 0) { - console.debug('Found Mono DLLs:', monoDlls); - return InstallationType.Mono; - } + console.debug("Looking for Mono dir"); + if (await doesDirectoryExist(path.join(gamePath, MANAGED_DLL_DIR))) { + console.debug("Found Mono dir"); + const monoDlls = ( + await fs.readdirAsync(path.join(gamePath, MANAGED_DLL_DIR)) + ).filter((f: string) => f.startsWith("Mono.") && f.endsWith(".dll")); + if (monoDlls.length > 0) { + console.debug("Found Mono DLLs:", monoDlls); + return InstallationType.Mono; } - - console.debug("Looking for IL2CPP dir") - if (await doesDirectoryExist(path.join(gamePath, IL2CPP_DIR))) { - console.debug("Found IL2CPP dir") - const il2CppDat = await fs.readdirAsync(path.join(gamePath, IL2CPP_DIR)) - if (il2CppDat.length > 0) { - console.debug('Found IL2CPP dirs:', il2CppDat); - return InstallationType.IL2CPP; - } + } + + console.debug("Looking for IL2CPP dir"); + if (await doesDirectoryExist(path.join(gamePath, IL2CPP_DIR))) { + console.debug("Found IL2CPP dir"); + const il2CppDat = await fs.readdirAsync(path.join(gamePath, IL2CPP_DIR)); + if (il2CppDat.length > 0) { + console.debug("Found IL2CPP dirs:", il2CppDat); + return InstallationType.IL2CPP; } + } - throw new Error(`Couldn't find "${MANAGED_DLL_DIR}" or "${IL2CPP_DIR}" under the game folder, is the game installed correctly?`); + throw new Error( + `Couldn't find "${MANAGED_DLL_DIR}" or "${IL2CPP_DIR}" under the game folder, is the game installed correctly?`, + ); } -async function checkBepInExBuild(gamePath: string): Promise { - const bepInExPath = path.join(gamePath, 'BepInEx', 'core'); - if (await doesDirectoryExist(bepInExPath)) { - console.debug("Found BepInEx/core directory"); - - const coreDlls = await fs.readdirAsync(bepInExPath); - if (coreDlls.some((file: string) => file.toLowerCase().includes('il2cpp') && file.endsWith('.dll'))) { - console.debug('Found IL2CPP BepInEx DLLs:', coreDlls); - return InstallationType.IL2CPP; - } - console.debug('IL2CPP BepInEx DLLs not found, assuming Mono is installed'); - return InstallationType.Mono; +async function checkBepInExBuild( + gamePath: string, +): Promise { + const bepInExPath = path.join(gamePath, "BepInEx", "core"); + if (await doesDirectoryExist(bepInExPath)) { + console.debug("Found BepInEx/core directory"); + + const coreDlls = await fs.readdirAsync(bepInExPath); + if ( + coreDlls.some( + (file: string) => + file.toLowerCase().includes("il2cpp") && file.endsWith(".dll"), + ) + ) { + console.debug("Found IL2CPP BepInEx DLLs:", coreDlls); + return InstallationType.IL2CPP; } - return undefined; + console.debug("IL2CPP BepInEx DLLs not found, assuming Mono is installed"); + return InstallationType.Mono; + } + return undefined; } - function main(context: types.IExtensionContext): boolean { - context.requireExtension('modtype-bepinex'); - - const modPath = path.join('BepInEx', 'plugins'); - - context.registerGame({ - id: GAME_ID, - name: 'Tainted Grail: The Fall of Avalon', - shortName: 'TG:FoA', - mergeMods: true, - queryPath: () => util.GameStoreHelper.findByAppId([STEAM_ID, GOG_ID]).then(game => game.gamePath), - setup: (discovery) => fs.ensureDirAsync(path.join(discovery.path, modPath)), - logo: 'gameart.png', - queryModPath: () => modPath, - executable: () => GAME_EXE_FILENAME, - requiredFiles: [ - GAME_EXE_FILENAME - ], - environment: { - SteamAPPId: STEAM_ID, - }, - details: { - steamAppId: STEAM_ID, - gogAppId: GOG_ID, - }, - }) - - const bepInEx5Config: IBepInExGameConfig = { - gameId: GAME_ID, - autoDownloadBepInEx: true, - unityBuild: 'unitymono', - customPackDownloader: () => Promise.resolve(BEPINEX5_DOWNLOAD_INFO), - } - const bepInEx6Config: IBepInExGameConfig = { - gameId: GAME_ID, - autoDownloadBepInEx: true, - unityBuild: 'unityil2cpp', - customPackDownloader: () => Promise.resolve(BEPINEX6_DOWNLOAD_INFO), + context.requireExtension("modtype-bepinex"); + + const modPath = path.join("BepInEx", "plugins"); + + context.registerGame({ + id: GAME_ID, + name: "Tainted Grail: The Fall of Avalon", + shortName: "TG:FoA", + mergeMods: true, + queryPath: () => + util.GameStoreHelper.findByAppId([STEAM_ID, GOG_ID]).then( + (game) => game.gamePath, + ), + setup: (discovery) => fs.ensureDirAsync(path.join(discovery.path, modPath)), + logo: "gameart.png", + queryModPath: () => modPath, + executable: () => GAME_EXE_FILENAME, + requiredFiles: [GAME_EXE_FILENAME], + environment: { + SteamAPPId: STEAM_ID, + }, + details: { + steamAppId: STEAM_ID, + gogAppId: GOG_ID, + }, + }); + + const bepInEx5Config: IBepInExGameConfig = { + gameId: GAME_ID, + autoDownloadBepInEx: true, + unityBuild: "unitymono", + customPackDownloader: () => Promise.resolve(BEPINEX5_DOWNLOAD_INFO), + }; + const bepInEx6Config: IBepInExGameConfig = { + gameId: GAME_ID, + autoDownloadBepInEx: true, + unityBuild: "unityil2cpp", + customPackDownloader: () => Promise.resolve(BEPINEX6_DOWNLOAD_INFO), + }; + + context.once(async () => { + if (context.api.ext.bepinexAddGame !== undefined) { + const game = util.getGame(GAME_ID); + const gamePath = await game.queryPath(); + const gameBuild = await checkGameBuild(gamePath); + if (gameBuild === InstallationType.Mono) { + console.log("Installing BepInEx 5 (Mono)"); + await context.api.ext.bepinexAddGame(bepInEx5Config); + } else { + console.log("Installing BepInEx 6 (IL2CPP)"); + await context.api.ext.bepinexAddGame(bepInEx6Config); + } + + const bepInExBuild = await checkBepInExBuild(gamePath); + if (bepInExBuild && bepInExBuild !== gameBuild) { + console.warn( + `Detected BepInEx build mismatch: expected ${gameBuild}, but got ${bepInExBuild}.`, + ); + + const t = context.api.translate; + const replace = { + game: game.name, + gameBuild, + bepInExBuild, + bl: "[br][/br][br][/br]", + }; + + context.api.showDialog( + "error", + "BepInEx Version Mismatch", + { + bbcode: t( + '"{{game}}" is currently installed with the "{{gameBuild}}" game build,{{bl}}' + + "but the currently installed BepInEx version is for the {{bepInExBuild}} build.{{bl}}" + + "Please go to the Mods tab and install the correct BepInEx version for your game build.", + { replace }, + ), + }, + [{ label: "Close", default: true }], + ); + } } - - context.once(async () => { - if (context.api.ext.bepinexAddGame !== undefined) { - const game = util.getGame(GAME_ID) - const gamePath = await game.queryPath(); - const gameBuild = await checkGameBuild(gamePath); - if (gameBuild === InstallationType.Mono) { - console.log('Installing BepInEx 5 (Mono)'); - await context.api.ext.bepinexAddGame(bepInEx5Config); - } - else { - console.log('Installing BepInEx 6 (IL2CPP)'); - await context.api.ext.bepinexAddGame(bepInEx6Config); - } - - const bepInExBuild = await checkBepInExBuild(gamePath); - if (bepInExBuild && bepInExBuild !== gameBuild) { - console.warn(`Detected BepInEx build mismatch: expected ${gameBuild}, but got ${bepInExBuild}.`); - - const t = context.api.translate; - const replace = { - game: game.name, - gameBuild, - bepInExBuild, - bl: '[br][/br][br][/br]', - }; - - context.api.showDialog('error', 'BepInEx Version Mismatch', { - bbcode: t('"{{game}}" is currently installed with the "{{gameBuild}}" game build,{{bl}}' + - 'but the currently installed BepInEx version is for the {{bepInExBuild}} build.{{bl}}' + - 'Please go to the Mods tab and install the correct BepInEx version for your game build.', { replace }), - }, [ - { label: 'Close', default: true } - ]); - } - - } - }) - return true + }); + return true; } -export default main \ No newline at end of file +export default main; diff --git a/tsconfig.json b/tsconfig.json index 81d2521..00c7c64 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,22 +1,18 @@ { - "compilerOptions": { - "target": "es6", - "module": "commonjs", - "moduleResolution": "node", - "noImplicitAny": false, - "removeComments": true, - "preserveConstEnums": true, - "rootDir": "./src", - "outDir": "./out", - "jsx": "react", - "sourceMap": true, - "skipLibCheck": true, - "esModuleInterop": true - }, - "exclude": [ - "node_modules" - ], - "include": [ - "src/**/*" - ] -} \ No newline at end of file + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "moduleResolution": "node", + "noImplicitAny": false, + "removeComments": true, + "preserveConstEnums": true, + "rootDir": "./src", + "outDir": "./out", + "jsx": "react", + "sourceMap": true, + "skipLibCheck": true, + "esModuleInterop": true + }, + "exclude": ["node_modules"], + "include": ["src/**/*"] +} diff --git a/webpack.config.js b/webpack.config.js index 25be7c4..36e9011 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,11 +1,11 @@ -const webpack = require('vortex-api/bin/webpack').default; +const webpack = require("vortex-api/bin/webpack").default; -const config = webpack('tgfoa-bepinex', __dirname, 5); +const config = webpack("tgfoa-bepinex", __dirname, 5); // Remove source maps from output config.devtool = false; if (config.output && config.output.sourceMapFilename) { - delete config.output.sourceMapFilename; + delete config.output.sourceMapFilename; } -module.exports = config; \ No newline at end of file +module.exports = config; From 95611eab5c9236bf27fa4aafedefbe23fdbbb247 Mon Sep 17 00:00:00 2001 From: jonanoj <4640274+jonanoj@users.noreply.github.com> Date: Sat, 26 Jul 2025 17:44:23 +0300 Subject: [PATCH 4/9] Add prettier GitHub Action --- .github/workflows/prettier.yml | 20 ++++++++++++++++++++ package.json | 4 +++- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/prettier.yml diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml new file mode 100644 index 0000000..57d576c --- /dev/null +++ b/.github/workflows/prettier.yml @@ -0,0 +1,20 @@ +name: Prettier + +on: + pull_request: + push: + +jobs: + prettier: + name: Check Formatting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22.x" + - run: npm install -g yarn + - run: yarn install + - name: Run Prettier + run: | + yarn format:check diff --git a/package.json b/package.json index 5571ae3..a50ba06 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "main": "./out/index.js", "scripts": { "copyassets": "node copyassets.js", - "build": "webpack --config webpack.config.js && extractInfo && yarn copyassets" + "build": "webpack --config webpack.config.js && extractInfo && yarn copyassets", + "format": "prettier --write .", + "format:check": "prettier --check ." }, "repository": "git@github.com:jonanoj/TGFoAVortexExtension.git", "license": "MIT", From 4a2612dcfd41e046d2da3ccbe60c64ef737c00c1 Mon Sep 17 00:00:00 2001 From: jonanoj <4640274+jonanoj@users.noreply.github.com> Date: Sat, 26 Jul 2025 17:52:51 +0300 Subject: [PATCH 5/9] Add mono migration notification for Steam users + persistent settings --- package.json | 5 ++- src/actions.ts | 6 +++ src/consts.ts | 57 ++++++++++++++++++++++++++ src/index.ts | 103 ++++++++++++++--------------------------------- src/migration.ts | 61 ++++++++++++++++++++++++++++ src/reducers.ts | 23 +++++++++++ src/types.ts | 8 ++++ yarn.lock | 17 ++++++++ 8 files changed, 206 insertions(+), 74 deletions(-) create mode 100644 src/actions.ts create mode 100644 src/consts.ts create mode 100644 src/migration.ts create mode 100644 src/reducers.ts create mode 100644 src/types.ts diff --git a/package.json b/package.json index a50ba06..46ff669 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,11 @@ }, "repository": "git@github.com:jonanoj/TGFoAVortexExtension.git", "license": "MIT", - "dependencies": {}, + "dependencies": { + "redux-act": "^1.8.0" + }, "devDependencies": { + "@types/redux": "^3.6.0", "modtype-bepinex": "Nexus-Mods/extension-modtype-bepinex", "prettier": "3.6.2", "ts-loader": "^9.5.2", diff --git a/src/actions.ts b/src/actions.ts new file mode 100644 index 0000000..be06dd2 --- /dev/null +++ b/src/actions.ts @@ -0,0 +1,6 @@ +import { createAction } from "redux-act"; + +export const setMonoMigrationNextReminderTime = createAction( + "SET_TGFOA_MONO_MIGRATION_NEXT_REMINDER_TIME", + (nextReminder: number) => ({ nextReminder }), +); diff --git a/src/consts.ts b/src/consts.ts new file mode 100644 index 0000000..b6a830f --- /dev/null +++ b/src/consts.ts @@ -0,0 +1,57 @@ +import { INexusDownloadInfo } from "modtype-bepinex/src/types"; +import path from "path"; +import { fs, util } from "vortex-api"; +import { IGame } from "vortex-api/lib/types/api"; + +export const STEAM_ID = "1466060"; +export const GOG_ID = "1887281589"; +export const GAME_DATA_DIR = "Fall of Avalon_Data"; +export const IL2CPP_DIR = path.join(GAME_DATA_DIR, "il2cpp_data"); +export const MANAGED_DLL_DIR = path.join(GAME_DATA_DIR, "Managed"); +export const GAME_EXE_FILENAME = "Fall of Avalon.exe"; +export const MOD_PATH = path.join("BepInEx", "plugins"); + +export const GAME: IGame = { + id: "taintedgrailthefallofavalon", + name: "Tainted Grail: The Fall of Avalon", + shortName: "TG:FoA", + mergeMods: true, + queryPath: () => + util.GameStoreHelper.findByAppId([STEAM_ID, GOG_ID]).then( + (game) => game.gamePath, + ), + setup: (discovery) => fs.ensureDirAsync(path.join(discovery.path, MOD_PATH)), + logo: "gameart.png", + queryModPath: () => MOD_PATH, + executable: () => GAME_EXE_FILENAME, + requiredFiles: [GAME_EXE_FILENAME], + environment: { + SteamAPPId: STEAM_ID, + }, + details: { + steamAppId: STEAM_ID, + gogAppId: GOG_ID, + }, +}; + +export const BEPINEX5_DOWNLOAD_INFO: INexusDownloadInfo = { + gameId: GAME.id, + domainId: GAME.id, + modId: "50", + fileId: "162", + archiveName: "BepInEx Mono Windows x64-50-5-4-23-3-1752802821.zip", + architecture: "x64", + version: "5.4.23.3", + allowAutoInstall: false, +}; + +export const BEPINEX6_DOWNLOAD_INFO: INexusDownloadInfo = { + gameId: GAME.id, + domainId: GAME.id, + modId: "16", + fileId: "100", + archiveName: "BepinEx-16-6-0-0-be-735b-1749459702.zip", + architecture: "x64", + version: "6.0.0-be+735b", // Semver compliant version https://semver.org/#backusnaur-form-grammar-for-valid-semver-versions + allowAutoInstall: false, +}; diff --git a/src/index.ts b/src/index.ts index 011f7fa..fd3eb1b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,44 +1,16 @@ import path from "path"; import { fs, util, type types } from "vortex-api"; -import type { - IBepInExGameConfig, - INexusDownloadInfo, -} from "../node_modules/modtype-bepinex/src/types"; - -const GAME_ID = "taintedgrailthefallofavalon"; -const STEAM_ID = "1466060"; -const GOG_ID = "1887281589"; -const GAME_DATA_DIR = "Fall of Avalon_Data"; -const IL2CPP_DIR = path.join(GAME_DATA_DIR, "il2cpp_data"); -const MANAGED_DLL_DIR = path.join(GAME_DATA_DIR, "Managed"); -const GAME_EXE_FILENAME = "Fall of Avalon.exe"; - -const BEPINEX5_DOWNLOAD_INFO: INexusDownloadInfo = { - gameId: GAME_ID, - domainId: GAME_ID, - modId: "50", - fileId: "162", - archiveName: "BepInEx Mono Windows x64-50-5-4-23-3-1752802821.zip", - architecture: "x64", - version: "5.4.23.3", - allowAutoInstall: false, -}; - -const BEPINEX6_DOWNLOAD_INFO: INexusDownloadInfo = { - gameId: GAME_ID, - domainId: GAME_ID, - modId: "16", - fileId: "100", - archiveName: "BepinEx-16-6-0-0-be-735b-1749459702.zip", - architecture: "x64", - version: "6.0.0-be+735b", // Semver compliant version https://semver.org/#backusnaur-form-grammar-for-valid-semver-versions - allowAutoInstall: false, -}; - -const enum InstallationType { - Mono = "Mono (v5)", - IL2CPP = "IL2CPP (v6)", -} +import type { IBepInExGameConfig } from "../node_modules/modtype-bepinex/src/types"; +import { + BEPINEX5_DOWNLOAD_INFO, + BEPINEX6_DOWNLOAD_INFO, + GAME, + IL2CPP_DIR, + MANAGED_DLL_DIR, +} from "./consts"; +import { showMonoMigrationReminder } from "./migration"; +import { settingsReducer } from "./reducers"; +import { InstallationType } from "./types"; async function doesDirectoryExist(dir: string): Promise { try { @@ -102,40 +74,17 @@ async function checkBepInExBuild( function main(context: types.IExtensionContext): boolean { context.requireExtension("modtype-bepinex"); - - const modPath = path.join("BepInEx", "plugins"); - - context.registerGame({ - id: GAME_ID, - name: "Tainted Grail: The Fall of Avalon", - shortName: "TG:FoA", - mergeMods: true, - queryPath: () => - util.GameStoreHelper.findByAppId([STEAM_ID, GOG_ID]).then( - (game) => game.gamePath, - ), - setup: (discovery) => fs.ensureDirAsync(path.join(discovery.path, modPath)), - logo: "gameart.png", - queryModPath: () => modPath, - executable: () => GAME_EXE_FILENAME, - requiredFiles: [GAME_EXE_FILENAME], - environment: { - SteamAPPId: STEAM_ID, - }, - details: { - steamAppId: STEAM_ID, - gogAppId: GOG_ID, - }, - }); + context.registerReducer(["settings", GAME.id], settingsReducer); + context.registerGame(GAME); const bepInEx5Config: IBepInExGameConfig = { - gameId: GAME_ID, + gameId: GAME.id, autoDownloadBepInEx: true, unityBuild: "unitymono", customPackDownloader: () => Promise.resolve(BEPINEX5_DOWNLOAD_INFO), }; const bepInEx6Config: IBepInExGameConfig = { - gameId: GAME_ID, + gameId: GAME.id, autoDownloadBepInEx: true, unityBuild: "unityil2cpp", customPackDownloader: () => Promise.resolve(BEPINEX6_DOWNLOAD_INFO), @@ -143,15 +92,23 @@ function main(context: types.IExtensionContext): boolean { context.once(async () => { if (context.api.ext.bepinexAddGame !== undefined) { - const game = util.getGame(GAME_ID); - const gamePath = await game.queryPath(); + const gamePath = await GAME.queryPath(); + const gameStore = await util.GameStoreHelper.identifyStore(gamePath); const gameBuild = await checkGameBuild(gamePath); + + console.log(`Game Store: ${gameStore}, Game Build: ${gameBuild}`); + if (gameBuild === InstallationType.Mono) { - console.log("Installing BepInEx 5 (Mono)"); + console.log("Using BepInEx 5 (Mono)"); await context.api.ext.bepinexAddGame(bepInEx5Config); } else { - console.log("Installing BepInEx 6 (IL2CPP)"); + console.log("Using BepInEx 6 (IL2CPP)"); await context.api.ext.bepinexAddGame(bepInEx6Config); + + if (gameStore === util.steam.id) { + // Mono version is currently only available on Steam + showMonoMigrationReminder(context); + } } const bepInExBuild = await checkBepInExBuild(gamePath); @@ -162,10 +119,10 @@ function main(context: types.IExtensionContext): boolean { const t = context.api.translate; const replace = { - game: game.name, + game: GAME.name, gameBuild, bepInExBuild, - bl: "[br][/br][br][/br]", + bl: "[br][/br]", }; context.api.showDialog( @@ -179,7 +136,7 @@ function main(context: types.IExtensionContext): boolean { { replace }, ), }, - [{ label: "Close", default: true }], + [{ label: t("Close"), default: true }], ); } } diff --git a/src/migration.ts b/src/migration.ts new file mode 100644 index 0000000..c1595e9 --- /dev/null +++ b/src/migration.ts @@ -0,0 +1,61 @@ +import { setMonoMigrationNextReminderTime } from "./actions"; +import { BEPINEX5_DOWNLOAD_INFO, GAME } from "./consts"; +import { getSettings } from "./reducers"; + +export function showMonoMigrationReminder(context) { + const state = context.api.store.getState(); + const settings = getSettings(state); + + const t = context.api.translate; + const shouldShowReminder = + settings.monoMigrationNextReminderTime == undefined || + (settings.monoMigrationNextReminderTime !== -1 && // -1 means the user has dismissed forever + settings.monoMigrationNextReminderTime < Date.now()); + + if (shouldShowReminder) { + const replace = { + game: GAME.name, + bl: "[br][/br]", + }; + context.api.showDialog( + "info", + "Mono Version available", + { + bbcode: t( + "Your {{game}} installation is using the IL2CPP build.{{bl}}" + + "Mods are considered unstable on this version, and your game might crash frequently.{{bl}}" + + "It is [b]highly recommended[/b] to use the [b]Mono[/b] version instead.{{bl}}{{bl}}" + + "Follow the BepInEx Mono description on how to switch to the Mono build.{{bl}}{{bl}}" + + "After switching, delete your existing BepInEx installation and Vortex should automatically download the Mono version for you.", + { replace }, + ), + }, + [ + { + label: t("Open Guide"), + action: () => + context.api.ext.nexusOpenModPage( + GAME.id, + BEPINEX5_DOWNLOAD_INFO.modId, + "nexus", + ), + }, + { + label: t("Remind in 1 week"), + action: () => { + const nextReminder = Date.now() + 7 * 24 * 60 * 60 * 1000; // 1 week later + context.api.store.dispatch( + setMonoMigrationNextReminderTime(nextReminder), + ); + }, + }, + { + label: t("Never show again"), + action: () => { + context.api.store.dispatch(setMonoMigrationNextReminderTime(-1)); + }, + }, + ], + ); + } +} diff --git a/src/reducers.ts b/src/reducers.ts new file mode 100644 index 0000000..ce023a0 --- /dev/null +++ b/src/reducers.ts @@ -0,0 +1,23 @@ +import { types, util } from "vortex-api"; +import { IState } from "vortex-api/lib/types/api"; +import { setMonoMigrationNextReminderTime } from "./actions"; +import { GAME } from "./consts"; +import { ExtensionSettings } from "./types"; + +export function getSettings(state: IState): ExtensionSettings { + return util.getSafe(state, ["settings", GAME.id], {}); +} + +export const settingsReducer: types.IReducerSpec = { + reducers: { + [setMonoMigrationNextReminderTime as any]: (state, payload) => { + const { nextReminder } = payload; + return util.setSafe( + state, + ["monoMigrationNextReminderTime"], + nextReminder, + ); + }, + }, + defaults: {}, +}; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..26b43a8 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,8 @@ +export const enum InstallationType { + Mono = "Mono (v5)", + IL2CPP = "IL2CPP (v6)", +} + +export interface ExtensionSettings { + monoMigrationNextReminderTime?: number; +} diff --git a/yarn.lock b/yarn.lock index 6be6ea5..f413196 100644 --- a/yarn.lock +++ b/yarn.lock @@ -85,6 +85,13 @@ dependencies: undici-types "~6.21.0" +"@types/redux@^3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@types/redux/-/redux-3.6.0.tgz#f1ebe1e5411518072e4fdfca5c76e16e74c1399a" + integrity sha512-ic+60DXHW5seNyqFvfr7Sk5cnXs+HsF9tIeIaxjOuSP5kzgDXC+AzKTYmjAfuLx4Sccm/0vjwBQj3OOkUkwOqg== + dependencies: + redux "*" + "@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1": version "1.14.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.14.1.tgz#a9f6a07f2b03c95c8d38c4536a1fdfb521ff55b6" @@ -661,6 +668,16 @@ rechoir@^0.8.0: dependencies: resolve "^1.20.0" +redux-act@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/redux-act/-/redux-act-1.8.0.tgz#5394a9433b36937d1a1da11aca6547d97d10ab00" + integrity sha512-xW3FIco/VwJGMW3eCD3JGAxyozNC4k35qrWM5lEQf9T2pfv3leDC+3vQ8WdHZjcrol/jjWAeJ+ULqrscuDqlKg== + +redux@*: + version "5.0.1" + resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.1.tgz#97fa26881ce5746500125585d5642c77b6e9447b" + integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w== + require-from-string@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" From ffa30c206e40e881f044d7d0575cecf17f340fd3 Mon Sep 17 00:00:00 2001 From: jonanoj <4640274+jonanoj@users.noreply.github.com> Date: Sat, 26 Jul 2025 18:40:06 +0300 Subject: [PATCH 6/9] Rephrase migration notice options --- src/migration.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/migration.ts b/src/migration.ts index c1595e9..523d3c3 100644 --- a/src/migration.ts +++ b/src/migration.ts @@ -6,6 +6,16 @@ export function showMonoMigrationReminder(context) { const state = context.api.store.getState(); const settings = getSettings(state); + const nextReminderTime = settings.monoMigrationNextReminderTime; + console.log( + "Next reminder time:", + nextReminderTime === -1 + ? "Never" + : nextReminderTime != undefined + ? new Date(nextReminderTime) + : "Now", + ); + const t = context.api.translate; const shouldShowReminder = settings.monoMigrationNextReminderTime == undefined || @@ -32,7 +42,7 @@ export function showMonoMigrationReminder(context) { }, [ { - label: t("Open Guide"), + label: t("Open migration guide"), action: () => context.api.ext.nexusOpenModPage( GAME.id, @@ -41,7 +51,7 @@ export function showMonoMigrationReminder(context) { ), }, { - label: t("Remind in 1 week"), + label: t("Remind me in a week"), action: () => { const nextReminder = Date.now() + 7 * 24 * 60 * 60 * 1000; // 1 week later context.api.store.dispatch( @@ -50,7 +60,7 @@ export function showMonoMigrationReminder(context) { }, }, { - label: t("Never show again"), + label: t("Don't remind me again"), action: () => { context.api.store.dispatch(setMonoMigrationNextReminderTime(-1)); }, From 6e1544a96cef4d3549976cd366bd3392226b0cce Mon Sep 17 00:00:00 2001 From: jonanoj <4640274+jonanoj@users.noreply.github.com> Date: Sat, 26 Jul 2025 18:47:53 +0300 Subject: [PATCH 7/9] Rephrase next reminder time log --- src/migration.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/migration.ts b/src/migration.ts index 523d3c3..5571c90 100644 --- a/src/migration.ts +++ b/src/migration.ts @@ -7,22 +7,21 @@ export function showMonoMigrationReminder(context) { const settings = getSettings(state); const nextReminderTime = settings.monoMigrationNextReminderTime; - console.log( - "Next reminder time:", - nextReminderTime === -1 - ? "Never" - : nextReminderTime != undefined - ? new Date(nextReminderTime) - : "Now", - ); - const t = context.api.translate; const shouldShowReminder = settings.monoMigrationNextReminderTime == undefined || (settings.monoMigrationNextReminderTime !== -1 && // -1 means the user has dismissed forever settings.monoMigrationNextReminderTime < Date.now()); + const nextReminderTimeString = shouldShowReminder + ? "Now" + : nextReminderTime === -1 + ? "Never" + : new Date(nextReminderTime).toLocaleString(); + console.log(`Next Mono migration reminder time: ${nextReminderTimeString}`); + if (shouldShowReminder) { + const t = context.api.translate; const replace = { game: GAME.name, bl: "[br][/br]", From 44910fd182f80a39d85adc7269e51a1fdb789ff8 Mon Sep 17 00:00:00 2001 From: jonanoj <4640274+jonanoj@users.noreply.github.com> Date: Sat, 26 Jul 2025 18:50:53 +0300 Subject: [PATCH 8/9] Bump version to 1.2.0 --- assets/info.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/info.json b/assets/info.json index 030621f..d429704 100644 --- a/assets/info.json +++ b/assets/info.json @@ -1,6 +1,6 @@ { "name": "Game: Tainted Grail: The Fall of Avalon", "author": "jonanoj", - "version": "1.1.0", + "version": "1.2.0", "description": "Support for Tainted Grail: The Fall of Avalon mods using BepInEx" } diff --git a/package.json b/package.json index 46ff669..811ca2a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tgfoa-bepinex", - "version": "1.1.0", + "version": "1.2.0", "description": "Support for Tainted Grail: The Fall of Avalon mods using BepInEx", "author": "jonanoj", "main": "./out/index.js", From 55e66730718db07234c72b563accef6383001f16 Mon Sep 17 00:00:00 2001 From: jonanoj <4640274+jonanoj@users.noreply.github.com> Date: Sat, 26 Jul 2025 18:56:58 +0300 Subject: [PATCH 9/9] Only prettier on push to main --- .github/workflows/prettier.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml index 57d576c..212a482 100644 --- a/.github/workflows/prettier.yml +++ b/.github/workflows/prettier.yml @@ -3,6 +3,8 @@ name: Prettier on: pull_request: push: + branches: + - main jobs: prettier: