Skip to content

Commit

Permalink
Add Balatro + Love2D support via lovely-injector (#1267)
Browse files Browse the repository at this point in the history
lovely-injector is "JIT-like" lua injector for Love2D games:
https://github.com/ethangreen-dev/lovely-injector.
It injects lua into the game process at runtime, bypassing the need for
manual executable patching.

---------

Co-authored-by: Oksamies <[email protected]>
  • Loading branch information
ethangreen-dev and Oksamies authored Apr 17, 2024
1 parent dd003ba commit f22547b
Show file tree
Hide file tree
Showing 10 changed files with 117 additions and 2 deletions.
Binary file added src/assets/images/game_selection/Balatro.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
80 changes: 80 additions & 0 deletions src/installers/LovelyInstaller.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>();

// 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<string, string>();

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);
}
}
3 changes: 3 additions & 0 deletions src/installers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions src/model/game/GameManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions src/model/installing/PackageLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export enum PackageLoader {
GODOT_ML,
ANCIENT_DUNGEON_VR,
SHIMLOADER,
LOVELY,
}

export function GetInstallerIdForLoader(loader: PackageLoader): PackageInstallerId | null {
Expand All @@ -19,13 +20,15 @@ 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;
}
}

export function GetInstallerIdForPlugin(loader: PackageLoader): PackageInstallerId | null {
switch (loader) {
case PackageLoader.SHIMLOADER: return "shimloader-plugin";
case PackageLoader.LOVELY: return "lovely-plugin";
}

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ function buildRunners(runners: PlatformRunnersType): LoaderRunnersType {
[PackageLoader.ANCIENT_DUNGEON_VR]: runners,
[PackageLoader.GODOT_ML]: runners,
[PackageLoader.SHIMLOADER]: runners,
[PackageLoader.LOVELY]: runners,
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
];


Expand Down Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion src/r2mm/launching/instructions/GameInstructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<GameInstruction> {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<GameInstruction> {
const modDir = path.join(profile.getPathOfProfile(), "mods");

return {
moddedParameters: `--mod-dir "${modDir}"`,
vanillaParameters: "--vanilla"
};
}
}
2 changes: 1 addition & 1 deletion src/r2mm/manager/SettingsDexieStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down

0 comments on commit f22547b

Please sign in to comment.