const path = require('path'); const { fs, log, util } = require('vortex-api'); const GAME_ID = 'retrorewindvideostoresimulator'; const STEAM_APP_ID = '3552140'; // all relative to gamePath == /common/RetroRewind const REL_PAKS = path.join('RetroRewind', 'Content', 'Paks', '~mods'); const REL_LOGICMODS = path.join('RetroRewind', 'Content', 'Paks', 'LogicMods'); const REL_UE4SS = path.join('RetroRewind', 'Binaries', 'Win64', 'ue4ss', 'Mods'); const MODTYPE_LOGICMODS = 'retrorewind-logicmods'; const MODTYPE_UE4SS = 'retrorewind-ue4ss'; const PAK_EXTS = ['.pak', '.ucas', '.utoc', '.sig']; // archive paths come from Vortex with the host separator, normalise to '/' function norm(filePath) { return filePath.split(/[\\/]/).join('/'); } function isDirectory(filePath) { return filePath.endsWith(path.sep) || filePath.endsWith('/'); } let _api; // under a wine/proton-hosted Vortex (steamtinkerlaunch) there is no Steam client // in the prefix, so the store lookup throws ENOENT on libraryfolders.vdf. // Swallow it - Vortex then keeps the manually browsed/stored discovery path. function findGame() { return util.GameStoreHelper.findByAppId([STEAM_APP_ID]) .then(game => game.gamePath) .catch(err => { log('info', 'Retro Rewind: steam store lookup unavailable, using manual discovery', { error: err.message }); return undefined; }); } // the game object handed to modType.getPath has no usable path while the game is // still being discovered/managed, so read the discovery from state instead function discoveryPath(game) { if (util.getSafe(game, ['path'], undefined) !== undefined) { return game.path; } return util.getSafe(_api.store.getState(), ['settings', 'gameMode', 'discovered', GAME_ID, 'path'], undefined); } function modTypePath(relPath) { return (game) => { const gamePath = discoveryPath(game); if (gamePath === undefined) { log('debug', 'Retro Rewind not discovered yet, modtype path unresolved', relPath); return undefined; } return path.join(gamePath, relPath); }; } function prepareForModding(discovery) { return Promise.all([REL_PAKS, REL_LOGICMODS, REL_UE4SS] .map(rel => fs.ensureDirWritableAsync(path.join(discovery.path, rel)))); } // ---------------------------------------------------------------- UE4SS mods // layout: /Scripts/main.lua or /dlls/main.dll const UE4SS_MARKER = /(^|\/)(scripts\/main\.lua|dlls\/main\.dll)$/i; function testUe4ssMod(files, gameId) { const supported = (gameId === GAME_ID) && files.some(file => UE4SS_MARKER.test(norm(file))); return Promise.resolve({ supported, requiredFiles: [] }); } function installUe4ssMod(files, destinationPath) { const marker = files.find(file => UE4SS_MARKER.test(norm(file))); // strip "Scripts/main.lua" (2 segments) to get the mod root inside the archive const markerSegments = norm(marker).split('/'); const rootSegments = markerSegments.slice(0, markerSegments.length - 2); const rootPath = rootSegments.join('/'); // mod name: the folder holding Scripts/, else the staging folder name const modName = rootSegments.length > 0 ? rootSegments[rootSegments.length - 1] : path.basename(destinationPath).replace(/\.installing$/, ''); const filtered = files.filter(file => !isDirectory(file) && (rootPath === '' || norm(file).startsWith(rootPath + '/'))); const instructions = filtered.map(file => ({ type: 'copy', source: file, destination: path.join(modName, rootPath === '' ? norm(file) : norm(file).substr(rootPath.length + 1)), })); // UE4SS 3.x activates a mod folder that contains enabled.txt - no mods.txt edit, // and the marker disappears again when Vortex purges the mod instructions.push({ type: 'generatefile', data: Buffer.from(''), destination: path.join(modName, 'enabled.txt'), }); instructions.push({ type: 'setmodtype', value: MODTYPE_UE4SS }); log('info', 'installing Retro Rewind UE4SS mod', { modName, files: filtered.length }); return Promise.resolve({ instructions }); } // ------------------------------------------------------------ blueprint mods // blueprint (LogicMods) paks ship inside a folder called LogicMods const LOGICMODS_MARKER = /(^|\/)logicmods\//i; function testLogicMod(files, gameId) { const supported = (gameId === GAME_ID) && files.some(file => LOGICMODS_MARKER.test(norm(file)) && PAK_EXTS.includes(path.extname(file).toLowerCase())); return Promise.resolve({ supported, requiredFiles: [] }); } function installLogicMod(files) { const instructions = files .filter(file => !isDirectory(file) && PAK_EXTS.includes(path.extname(file).toLowerCase())) .map(file => ({ type: 'copy', source: file, destination: path.basename(file), })); instructions.push({ type: 'setmodtype', value: MODTYPE_LOGICMODS }); return Promise.resolve({ instructions }); } // ------------------------------------------------------------------ pak mods function testPakMod(files, gameId) { const supported = (gameId === GAME_ID) && files.some(file => path.extname(file).toLowerCase() === '.pak'); return Promise.resolve({ supported, requiredFiles: [] }); } // flatten into ~mods - archives commonly wrap the pak in one or more folders function installPakMod(files) { const instructions = files .filter(file => !isDirectory(file) && PAK_EXTS.includes(path.extname(file).toLowerCase())) .map(file => ({ type: 'copy', source: file, destination: path.basename(file), })); return Promise.resolve({ instructions }); } function main(context) { _api = context.api; context.registerGame({ id: GAME_ID, name: 'Retro Rewind - Video Store Simulator', mergeMods: true, queryPath: findGame, queryModPath: () => REL_PAKS, logo: 'gameart.jpg', executable: () => 'RetroRewind.exe', requiredFiles: ['RetroRewind.exe'], setup: prepareForModding, supportedTools: [], environment: { SteamAPPId: STEAM_APP_ID, }, details: { steamAppId: parseInt(STEAM_APP_ID, 10), }, }); context.registerModType(MODTYPE_UE4SS, 25, gameId => gameId === GAME_ID, modTypePath(REL_UE4SS), () => Promise.resolve(false), { name: 'UE4SS Mod' }); context.registerModType(MODTYPE_LOGICMODS, 25, gameId => gameId === GAME_ID, modTypePath(REL_LOGICMODS), () => Promise.resolve(false), { name: 'Blueprint Mod (LogicMods)' }); // lower number == higher priority, so UE4SS wins over the generic pak installer context.registerInstaller('retrorewind-ue4ss', 25, testUe4ssMod, installUe4ssMod); context.registerInstaller('retrorewind-logicmods', 26, testLogicMod, installLogicMod); context.registerInstaller('retrorewind-pak', 27, testPakMod, installPakMod); return true; } module.exports = { default: main, };