Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/assets/data/ecosystemJsonSchema.json
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@
"shimloader",
"lovely",
"return-of-modding",
"gdpatch",
"gdweave",
"recursive-melonloader",
"bepisloader",
Expand Down Expand Up @@ -412,6 +413,7 @@
"shimloader",
"lovely",
"return-of-modding",
"gdpatch",
"gdweave",
"recursive-melonloader",
"bepisloader",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/data/ecosystemTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export enum TrackingMethod {
export enum Loader {
BEPINEX = "bepinex",
BEPISLOADER = "bepisloader",
GDPATCH = "gdpatch",
GDWEAVE = "gdweave",
GODOTML = "godotml",
LOVELY = "lovely",
Expand Down Expand Up @@ -415,6 +416,7 @@ const typeMap: any = {
"Loader": [
"bepinex",
"bepisloader",
"gdpatch",
"gdweave",
"godotml",
"lovely",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ async function copyLogToClipboard() {
case PackageLoader.GDWEAVE:
logOutputPath = path.join(profile.value.getProfilePath(), "GDWeave", "GDWeave.log");
break;
case PackageLoader.GDPATCH:
logOutputPath = path.join(profile.value.getProfilePath(), "GDPatch", "output.log");
break;
case PackageLoader.UMM:
logOutputPath = path.join(profile.value.getProfilePath(), "UMM", "Core", "Log.txt");
break;
Expand Down
132 changes: 132 additions & 0 deletions src/installers/GDPatchInstaller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { InstallArgs, PackageInstaller } from "./PackageInstaller";
import FileNotFoundError from "../model/errors/FileNotFoundError";
import FileWriteError from "../model/errors/FileWriteError";
import FsProvider from "../providers/generic/file/FsProvider";
import path from "../providers/node/path/path";
import FileUtils from "../utils/FileUtils";

export class GDPatchInstaller implements PackageInstaller {
async install(args: InstallArgs) {
const {
packagePath,
profile,
} = args;

const fs = FsProvider.instance;

const windowsLoaderSrc = path.join(packagePath, "winmm.dll");
const windowsLoaderDest = profile.joinToProfilePath("winmm.dll");
await fs.copyFile(windowsLoaderSrc, windowsLoaderDest);

const linuxLoaderSrc = path.join(packagePath, "libgdpatch_loader.so");
const linuxLoaderDest = profile.joinToProfilePath("libgdpatch_loader.so");
await fs.copyFile(linuxLoaderSrc, linuxLoaderDest);

const macosLoaderSrc = path.join(packagePath, "libgdpatch_loader.dylib");
const macosLoaderDest = profile.joinToProfilePath("libgdpatch_loader.dylib");
await fs.copyFile(macosLoaderSrc, macosLoaderDest);
}

async uninstall(args: InstallArgs) {
const { profile } = args;
const fs = FsProvider.instance;

const loaderPaths = [
profile.joinToProfilePath("winmm.dll"),
profile.joinToProfilePath("libgdpatch_loader.so"),
profile.joinToProfilePath("libgdpatch_loader.dylib"),
];

try {
for (const loaderPath of loaderPaths) {
if (await fs.exists(loaderPath)) {
await fs.unlink(loaderPath);
}
}
} catch (e) {
const name = "Failed to delete GDPatch loader files from profile";
const solution = "Is the game still running?";
throw FileWriteError.fromThrownValue(e, name, solution);
}
}
}

async function findFolderContainingModToml(folder: string): Promise<string | undefined> {
const fs = FsProvider.instance;

if (await fs.exists(path.join(folder, "gdpatch_mod.toml"))) {
return folder;
}

for (const entry of await fs.readdir(folder)) {
const entryPath = path.join(folder, entry);
if ((await fs.lstat(entryPath)).isFile()) {
continue;
}

const modFolder = await findFolderContainingModToml(entryPath);
if (modFolder) {
return modFolder;
}
}

return undefined;
}

export class GDPatchPluginInstaller implements PackageInstaller {
private getModFolderInProfile(args: InstallArgs) {
return args.profile.joinToProfilePath("GDPatch", "mods", args.mod.getName());
}

async install(args: InstallArgs) {
const modFolderInCache = await findFolderContainingModToml(args.packagePath);
if (modFolderInCache === undefined) {
throw new FileNotFoundError(
`Failed to find gdpatch_mod.toml in ${args.mod.getName()}`,
"Either the mod package is malformed, or the files extracted to cache are corrupted"
);
}

const modFolderInProfile = this.getModFolderInProfile(args);
await FileUtils.ensureDirectory(modFolderInProfile);
await FsProvider.instance.copyFolder(modFolderInCache, modFolderInProfile);
}

async uninstall(args: InstallArgs) {
try {
await FileUtils.recursiveRemoveDirectoryIfExists(this.getModFolderInProfile(args));
} catch (e) {
const name = `Failed to delete ${args.mod.getName()} from profile`;
const solution = "Is the game still running?";
throw FileWriteError.fromThrownValue(e, name, solution);
}
}

async enable(args: InstallArgs) {
const fs = FsProvider.instance;

try {
const disabledMarkerPath = path.join(this.getModFolderInProfile(args), "gdpatch_disabled");
if (await fs.exists(disabledMarkerPath)) {
await fs.unlink(disabledMarkerPath);
}
} catch (e) {
const name = `Failed to enable ${args.mod.getName()}`;
const solution = "Is the game still running?";
throw FileWriteError.fromThrownValue(e, name, solution);
}
}

async disable(args: InstallArgs) {
const fs = FsProvider.instance;

try {
const disabledMarkerPath = path.join(this.getModFolderInProfile(args), "gdpatch_disabled");
await fs.writeFile(disabledMarkerPath, "");
} catch (e) {
const name = `Failed to disable ${args.mod.getName()}`;
const solution = "Is the game still running?";
throw FileWriteError.fromThrownValue(e, name, solution);
}
}
}
3 changes: 3 additions & 0 deletions src/installers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ShimloaderInstaller } from './ShimloaderInstaller';
import { LovelyInstaller, LovelyPluginInstaller } from './LovelyInstaller';
import { NorthstarInstaller } from './NorthstarInstaller';
import { ReturnOfModdingInstaller, ReturnOfModdingPluginInstaller } from './ReturnOfModdingInstaller';
import { GDPatchInstaller, GDPatchPluginInstaller } from './GDPatchInstaller';
import { GDWeaveInstaller, GDWeavePluginInstaller } from './GDWeaveInstaller';
import { RecursiveMelonLoaderInstaller, RecursiveMelonLoaderPluginInstaller } from './RecursiveMelonLoaderInstaller';
import { DirectCopyInstaller } from './DirectCopyInstaller';
Expand All @@ -19,6 +20,7 @@ import { PackageLoader } from '../model/schema/ThunderstoreSchema';
export const PackageLoaderInstallers: Record<PackageLoader, PackageInstaller> = {
[PackageLoader.BEPINEX]: new BepInExInstaller(),
[PackageLoader.BEPISLOADER]: new BepisLoaderInstaller(),
[PackageLoader.GDPATCH]: new GDPatchInstaller(),
[PackageLoader.GDWEAVE]: new GDWeaveInstaller(),
[PackageLoader.GODOTML]: new GodotMLInstaller(),
[PackageLoader.LOVELY]: new LovelyInstaller(),
Expand All @@ -37,6 +39,7 @@ const installRulePluginInstaller = new InstallRulePluginInstaller();
export const PluginInstallers: Record<PackageLoader, PackageInstaller> = {
[PackageLoader.BEPINEX]: installRulePluginInstaller,
[PackageLoader.BEPISLOADER]: installRulePluginInstaller,
[PackageLoader.GDPATCH]: new GDPatchPluginInstaller(),
[PackageLoader.GDWEAVE]: new GDWeavePluginInstaller(),
[PackageLoader.GODOTML]: installRulePluginInstaller,
[PackageLoader.LOVELY]: new LovelyPluginInstaller(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ function buildRunners(runners: PlatformRunnersType): LoaderRunnersType {
[PackageLoader.RECURSIVE_MELONLOADER]: runners,
[PackageLoader.UMM]: runners,
[PackageLoader.RIVET]: runners,
[PackageLoader.GDPATCH]: runners,
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/r2mm/data/LogOutput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ export default class LogOutput {
fs.exists(Profile.getActiveProfile().joinToProfilePath('GDWeave', 'GDWeave.log'))
.then(value => this._exists = value);
break;
case PackageLoader.GDPATCH:
fs.exists(Profile.getActiveProfile().joinToProfilePath('GDPatch', 'output.log'))
.then(value => this._exists = value);
break;
case PackageLoader.UMM:
fs.exists(Profile.getActiveProfile().joinToProfilePath('UMM', 'Core', 'Log.txt'))
.then(value => this._exists = value);
Expand Down
1 change: 1 addition & 0 deletions src/r2mm/launching/instructions/DynamicGameInstruction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export enum DynamicGameInstruction {
PROFILE_NAME = "@profileName",
NORTHSTAR_DIRECTORY = "@northstarDirectory",
GDWEAVE_FOLDER = "@gdweaveFolder",
GDPATCH_FOLDER = "@gdpatchFolder",
BEPINEX_RENDERER_PRELOADER_PATH = "@bepInExRendererPreloaderPath",
UMM_PRELOADER_PATH = "@ummPreloaderPath",
}
5 changes: 5 additions & 0 deletions src/r2mm/launching/instructions/GameInstructionParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default class GameInstructionParser {
[DynamicGameInstruction.PROFILE_NAME, GameInstructionParser.profileNameResolver],
[DynamicGameInstruction.NORTHSTAR_DIRECTORY, GameInstructionParser.northstarDirectoryResolver],
[DynamicGameInstruction.GDWEAVE_FOLDER, GameInstructionParser.gdweaveFolderResolver],
[DynamicGameInstruction.GDPATCH_FOLDER, GameInstructionParser.gdpatchFolderResolver],
[DynamicGameInstruction.BEPINEX_RENDERER_PRELOADER_PATH, GameInstructionParser.bepInExRendererPreloaderPath],
[DynamicGameInstruction.UMM_PRELOADER_PATH, GameInstructionParser.ummPreloaderResolver]
]);
Expand Down Expand Up @@ -96,6 +97,10 @@ export default class GameInstructionParser {
return profile.joinToProfilePath("GDWeave");
}

private static async gdpatchFolderResolver(game: Game, profile: Profile): Promise<string | R2Error> {
return profile.joinToProfilePath("GDPatch");
}

private static async bepInExRendererPreloaderPath(game: Game, profile: Profile): Promise<string | R2Error> {
try {
if (['linux'].includes(appWindow.getPlatform().toLowerCase())) {
Expand Down
2 changes: 2 additions & 0 deletions src/r2mm/launching/instructions/GameInstructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CustomInstructions, ModsPathInstructions } from "./instructions/loader/
import ShimloaderGameInstructions from './instructions/loader/ShimloaderGameInstructions';
import LovelyGameInstructions from './instructions/loader/LovelyGameInstructions';
import ReturnOfModdingGameInstructions from './instructions/loader/ReturnOfModdingGameInstructions';
import GDPatchGameInstructions from './instructions/loader/GDPatchGameInstructions';
import GDWeaveGameInstructions from './instructions/loader/GDWeaveGameInstructions';
import BepisLoaderGameInstructions from './instructions/loader/BepisLoaderGameInstructions';
import UMMGameInstructions from './instructions/loader/UMMGameInstructions';
Expand Down Expand Up @@ -40,6 +41,7 @@ export default class GameInstructions {
[PackageLoader.GDWEAVE, new GDWeaveGameInstructions()],
[PackageLoader.UMM, new UMMGameInstructions()],
[PackageLoader.RIVET, new RivetGameInstructions()],
[PackageLoader.GDPATCH, new GDPatchGameInstructions()],
]);

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,14 @@
import GameInstructionGenerator from '../GameInstructionGenerator';
import { GameInstruction } from '../../GameInstructions';
import Game from '../../../../../model/game/Game';
import Profile from '../../../../../model/Profile';
import { DynamicGameInstruction } from '../../DynamicGameInstruction';

export default class GDPatchGameInstructions extends GameInstructionGenerator {
public async generate(game: Game, profile: Profile): Promise<GameInstruction> {
return {
moddedParameterList: [`--gdpatch-root-directory=${DynamicGameInstruction.GDPATCH_FOLDER}`],
Comment thread
ethangreen-dev marked this conversation as resolved.
vanillaParameterList: ["--gdpatch-disable"]
};
}
}
4 changes: 2 additions & 2 deletions src/r2mm/launching/runners/linux/SteamGameRunner_Linux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ export default class SteamGameRunner_Linux extends GameRunnerProvider {
let proxyArgs: Record<string, string> = {};

if (isProton) {
// BepInEx uses winhttp, GDWeave uses winmm. More can be added later.
const proxyDll = game.packageLoader == PackageLoader.GDWEAVE ? "winmm" : "winhttp";
// BepInEx uses winhttp, GDWeave and GDPatch use winmm. More can be added later.
const proxyDll = [PackageLoader.GDWEAVE, PackageLoader.GDPATCH].includes(game.packageLoader) ? "winmm" : "winhttp";
const promise = await this.ensureWineWillLoadDllOverride(game, proxyDll);
if (promise instanceof R2Error) {
// We no longer want to display an error as launch args should be set correctly.
Expand Down
2 changes: 1 addition & 1 deletion src/r2mm/manager/ModLinker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export default class ModLinker {
"melonloader", "plugins", "userdata",
"_state", "userlibs", "qmods", "shimloader",
"returnofmodding", "gdweave", "renderer", "umm",
"rivet"
"rivet", "gdpatch"
];

if (!exclusionsList.includes(file.toLowerCase())) {
Expand Down
Loading
Loading