diff --git a/src/assets/images/game_selection/Balatro.png b/src/assets/images/game_selection/Balatro.png new file mode 100644 index 000000000..f00e53422 Binary files /dev/null and b/src/assets/images/game_selection/Balatro.png differ diff --git a/src/installers/LovelyInstaller.ts b/src/installers/LovelyInstaller.ts new file mode 100644 index 000000000..9072c7102 --- /dev/null +++ b/src/installers/LovelyInstaller.ts @@ -0,0 +1,80 @@ +import { InstallArgs, PackageInstaller } from "./PackageInstaller"; +import { InstallRuleInstaller, addToStateFile } from "./InstallRuleInstaller"; +import FsProvider from "../providers/generic/file/FsProvider"; +import FileUtils from "../utils/FileUtils"; +import FileTree from "../model/file/FileTree"; +import R2Error from "../model/errors/R2Error"; +import path from "path"; + +export class LovelyInstaller extends PackageInstaller { + async install(args: InstallArgs) { + const { + mod, + packagePath, + profile, + } = args; + + const profilePath = profile.getPathOfProfile(); + const fs = FsProvider.instance; + const fileRelocations = new Map(); + + // Manually copy over version.dll + const dwmSrc = path.join(packagePath, "version.dll"); + const dwmDest = path.join(profilePath, "version.dll"); + await fs.copyFile(dwmSrc, dwmDest); + fileRelocations.set(dwmSrc, "version.dll"); + + // Files within the lovely subdirectory need to be recursively copied into the destination. + const lovelyTree = await FileTree.buildFromLocation(path.join(packagePath, "lovely")); + if (lovelyTree instanceof R2Error) { + throw lovelyTree; + } + + const targets = lovelyTree.getRecursiveFiles().map((x) => x.replace(packagePath, "")).map((x) => [x, path.join("mods", x)]); + for (const target of targets) { + const absSrc = path.join(packagePath, target[0]); + const absDest = path.join(profilePath, target[1]); + + await FileUtils.ensureDirectory(path.dirname(absDest)); + await fs.copyFile(absSrc, absDest); + + fileRelocations.set(absSrc, target[1]); + } + + await addToStateFile(mod, fileRelocations, profile); + } +} + +export class LovelyPluginInstaller extends PackageInstaller { + async install(args: InstallArgs) { + const { + mod, + packagePath, + profile, + } = args; + + const profilePath = profile.getPathOfProfile(); + const installDir = path.join("mods", mod.getName()); + + const fs = FsProvider.instance; + const fileRelocations = new Map(); + + const srcTree = await FileTree.buildFromLocation(packagePath); + if (srcTree instanceof R2Error) { + throw R2Error; + } + + const srcFiles = srcTree.getRecursiveFiles(); + for (const srcFile of srcFiles) { + const relFile = srcFile.replace(packagePath, ""); + const destFile = path.join(profilePath, installDir, relFile); + + await FileUtils.ensureDirectory(path.dirname(destFile)); + await fs.copyFile(srcFile, destFile); + + fileRelocations.set(srcFile, path.join(installDir, relFile)); + } + + await addToStateFile(mod, fileRelocations, profile); + } +} diff --git a/src/installers/registry.ts b/src/installers/registry.ts index 9bebadeff..b8cf5dd53 100644 --- a/src/installers/registry.ts +++ b/src/installers/registry.ts @@ -4,6 +4,7 @@ import { MelonLoaderInstaller } from "./MelonLoaderInstaller"; import { PackageInstaller } from "./PackageInstaller"; import { InstallRuleInstaller } from "./InstallRuleInstaller"; import { ShimloaderInstaller, ShimloaderPluginInstaller } from "./ShimloaderInstaller"; +import { LovelyInstaller, LovelyPluginInstaller } from "./LovelyInstaller"; const _PackageInstallers = { @@ -13,6 +14,8 @@ const _PackageInstallers = { "melonloader": new MelonLoaderInstaller(), "shimloader": new ShimloaderInstaller(), "shimloader-plugin": new ShimloaderPluginInstaller(), + "lovely": new LovelyInstaller(), + "lovely-plugin": new LovelyPluginInstaller(), } export type PackageInstallerId = keyof typeof _PackageInstallers; diff --git a/src/model/game/GameManager.ts b/src/model/game/GameManager.ts index 542ebe747..1235a85c1 100644 --- a/src/model/game/GameManager.ts +++ b/src/model/game/GameManager.ts @@ -569,6 +569,7 @@ export default class GameManager { "https://thunderstore.io/c/sailwind/api/v1/package/", EXCLUSIONS, [new StorePlatformMetadata(StorePlatform.STEAM, "1764530")], "Sailwind.png", GameSelectionDisplayMode.VISIBLE, GameInstanceType.GAME, PackageLoader.BEPINEX, []), + new Game( "Voices of the Void", "VotV", "VotV", "", ["VotV.exe"], "VotV", @@ -594,6 +595,12 @@ export default class GameManager { "https://thunderstore.io/c/content-warning/api/v1/package/", EXCLUSIONS, [new StorePlatformMetadata(StorePlatform.STEAM, "2881650")], "ContentWarning.png", GameSelectionDisplayMode.VISIBLE, GameInstanceType.GAME, PackageLoader.BEPINEX, []), + + new Game("Balatro", "Balatro", "Balatro", + "Balatro", ["Balatro.exe"], "Balatro_Data", + "https://thunderstore.io/c/balatro/api/v1/package/", EXCLUSIONS, + [new StorePlatformMetadata(StorePlatform.STEAM, "2379780")], "Balatro.png", + GameSelectionDisplayMode.VISIBLE, GameInstanceType.GAME, PackageLoader.LOVELY, []), ]; static get activeGame(): Game { diff --git a/src/model/installing/PackageLoader.ts b/src/model/installing/PackageLoader.ts index 589af56b6..338036e74 100644 --- a/src/model/installing/PackageLoader.ts +++ b/src/model/installing/PackageLoader.ts @@ -8,6 +8,7 @@ export enum PackageLoader { GODOT_ML, ANCIENT_DUNGEON_VR, SHIMLOADER, + LOVELY, } export function GetInstallerIdForLoader(loader: PackageLoader): PackageInstallerId | null { @@ -19,6 +20,7 @@ export function GetInstallerIdForLoader(loader: PackageLoader): PackageInstaller case PackageLoader.GODOT_ML: return "godotml"; case PackageLoader.NORTHSTAR: return "bepinex"; case PackageLoader.SHIMLOADER: return "shimloader"; + case PackageLoader.LOVELY: return "lovely"; case PackageLoader.ANCIENT_DUNGEON_VR: return null; } } @@ -26,6 +28,7 @@ export function GetInstallerIdForLoader(loader: PackageLoader): PackageInstaller export function GetInstallerIdForPlugin(loader: PackageLoader): PackageInstallerId | null { switch (loader) { case PackageLoader.SHIMLOADER: return "shimloader-plugin"; + case PackageLoader.LOVELY: return "lovely-plugin"; } return null; diff --git a/src/providers/generic/game/platform_interceptor/PlatformInterceptorImpl.ts b/src/providers/generic/game/platform_interceptor/PlatformInterceptorImpl.ts index ae5d22e55..c965633a5 100644 --- a/src/providers/generic/game/platform_interceptor/PlatformInterceptorImpl.ts +++ b/src/providers/generic/game/platform_interceptor/PlatformInterceptorImpl.ts @@ -64,6 +64,7 @@ function buildRunners(runners: PlatformRunnersType): LoaderRunnersType { [PackageLoader.ANCIENT_DUNGEON_VR]: runners, [PackageLoader.GODOT_ML]: runners, [PackageLoader.SHIMLOADER]: runners, + [PackageLoader.LOVELY]: runners, } } diff --git a/src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts b/src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts index e399b3274..d91f63fee 100644 --- a/src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts +++ b/src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts @@ -67,6 +67,7 @@ export const MODLOADER_PACKAGES = [ new ModLoaderPackageMapping("BepInEx_Wormtown-BepInExPack", "BepInExPack", PackageLoader.BEPINEX), new ModLoaderPackageMapping("0xFFF7-votv_shimloader", "", PackageLoader.SHIMLOADER), new ModLoaderPackageMapping("Thunderstore-unreal_shimloader", "", PackageLoader.SHIMLOADER), + new ModLoaderPackageMapping("Thunderstore-lovely", "", PackageLoader.LOVELY), ]; @@ -165,6 +166,7 @@ const VARIANTS = { Palworld: MODLOADER_PACKAGES, Plasma: MODLOADER_PACKAGES, ContentWarning: MODLOADER_PACKAGES, + Balatro: MODLOADER_PACKAGES, }; // Exported separately from the definition in order to preserve the key names in the type definition. // Otherwise this would become [key: string] and we couldn't use the game names for type hinting elsewhere. diff --git a/src/r2mm/launching/instructions/GameInstructions.ts b/src/r2mm/launching/instructions/GameInstructions.ts index 4cbe9818f..37368db03 100644 --- a/src/r2mm/launching/instructions/GameInstructions.ts +++ b/src/r2mm/launching/instructions/GameInstructions.ts @@ -8,6 +8,7 @@ import NorthstarGameInstructions from './instructions/loader/NorthstarGameInstru import { GodotMLGameInstructions } from "../../launching/instructions/instructions/loader/GodotMLGameInstructions"; import { AncientVRGameInstructions } from "../../launching/instructions/instructions/loader/AncientVRGameInstructions"; import ShimloaderGameInstructions from './instructions/loader/ShimloaderGameInstructions'; +import LovelyGameInstructions from './instructions/loader/LovelyGameInstructions'; export interface GameInstruction { moddedParameters: string, @@ -23,7 +24,8 @@ export default class GameInstructions { [PackageLoader.NORTHSTAR, new NorthstarGameInstructions()], [PackageLoader.GODOT_ML, new GodotMLGameInstructions()], [PackageLoader.ANCIENT_DUNGEON_VR, new AncientVRGameInstructions()], - [PackageLoader.SHIMLOADER, new ShimloaderGameInstructions()] + [PackageLoader.SHIMLOADER, new ShimloaderGameInstructions()], + [PackageLoader.LOVELY, new LovelyGameInstructions()], ]); public static async getInstructionsForGame(game: Game, profile: Profile): Promise { diff --git a/src/r2mm/launching/instructions/instructions/loader/LovelyGameInstructions.ts b/src/r2mm/launching/instructions/instructions/loader/LovelyGameInstructions.ts new file mode 100644 index 000000000..802ec78bd --- /dev/null +++ b/src/r2mm/launching/instructions/instructions/loader/LovelyGameInstructions.ts @@ -0,0 +1,17 @@ +import GameInstructionGenerator from '../GameInstructionGenerator'; +import { GameInstruction } from '../../GameInstructions'; +import Game from '../../../../../model/game/Game'; +import Profile from '../../../../../model/Profile'; +import * as path from 'path'; + +export default class LovelyGameInstructions extends GameInstructionGenerator { + + public async generate(game: Game, profile: Profile): Promise { + const modDir = path.join(profile.getPathOfProfile(), "mods"); + + return { + moddedParameters: `--mod-dir "${modDir}"`, + vanillaParameters: "--vanilla" + }; + } +} diff --git a/src/r2mm/manager/SettingsDexieStore.ts b/src/r2mm/manager/SettingsDexieStore.ts index 2c871c81f..65d3cafde 100644 --- a/src/r2mm/manager/SettingsDexieStore.ts +++ b/src/r2mm/manager/SettingsDexieStore.ts @@ -34,7 +34,7 @@ export default class SettingsDexieStore extends Dexie { // Add all games to store. Borked v2-3 locally // Increment per game or change to settings. - this.version(68).stores(store); + this.version(69).stores(store); this.activeGame = game; this.global = this.table("value");