commit f6170b535f3083c0961d7f0e9bdc1758a87f6c59 Author: Tobias Ganter Date: Wed Aug 5 13:41:47 2026 +0200 init diff --git a/gameart.jpg b/gameart.jpg new file mode 100644 index 0000000..f7f893f Binary files /dev/null and b/gameart.jpg differ diff --git a/index.js b/index.js new file mode 100644 index 0000000..72f2342 --- /dev/null +++ b/index.js @@ -0,0 +1,203 @@ +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, +}; diff --git a/info.json b/info.json new file mode 100644 index 0000000..36ea331 --- /dev/null +++ b/info.json @@ -0,0 +1,7 @@ +{ + "name": "game-retro-rewind", + "author": "Interstellarcodes", + "version": "1.1.2", + "description": "Vortex support for Retro Rewind - Video Store Simulator (pak, LogicMods and UE4SS mods)", + "url": "https://www.nexusmods.com/retrorewindvideostoresimulator/mods/66" +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2ae6450 --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "game-retro-rewind", + "version": "1.1.2", + "description": "Vortex support for Retro Rewind - Video Store Simulator", + "main": "index.js", + "author": "Interstellarcodes", + "license": "MIT", + "dependencies": { + "vortex-api": "^1.0.0" + } +}