Skip to content

Commit

Permalink
Merge pull request ebkr#1079 from ethangreen-dev/votv-and-shimloader
Browse files Browse the repository at this point in the history
Add Palworld + Voices of the Void support, initial Unreal engine support
  • Loading branch information
MythicManiac authored Feb 9, 2024
2 parents b3c5f3b + 0bdd2c2 commit fcb5151
Show file tree
Hide file tree
Showing 22 changed files with 453 additions and 55 deletions.
38 changes: 38 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Build

on: [push]

jobs:
build:
name: Test on ${{ matrix.platform }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- kind: linux
os: ubuntu-latest
platform: linux
- kind: windows
os: windows-latest
platform: win
- kind: mac
os: macos-11
platform: osx
steps:
- uses: actions/checkout@v3

- name: Set up Node
uses: actions/setup-node@v3
with:
# The talk on the street says this might be a good version for building.
node-version: 14.20.1
cache: yarn

- name: Install Yarn dependencies
run: yarn install --frozen-lockfile

- name: Run tests
run: >
node test/folder-structure-testing/populator.mjs &&
yarn run test:unit:ci
Binary file added src/assets/images/game_selection/Palworld.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/images/game_selection/VotV.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions src/components/config-components/ConfigSelectionLayout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,14 @@ import ProfileModList from '../../r2mm/mods/ProfileModList';
this.configFiles.push(new ConfigFile(file.substring(configLocation.length + 1), file, fileStat.mtime));
}
}
// HACK: Force the UE4SS-settings.ini file for shimloader mod installs to be visible.
const ue4ssSettingsPath = tree.getFiles().find(x => x.toLowerCase().endsWith("ue4ss-settings.ini"));
if (ue4ssSettingsPath) {
const lstat = await fs.lstat(ue4ssSettingsPath);
this.configFiles.push(new ConfigFile("UE4SS-settings.ini", ue4ssSettingsPath, lstat.mtime));
}
this.shownConfigFiles = [...this.configFiles];
}
Expand Down
3 changes: 0 additions & 3 deletions src/installers/InstallRuleInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ import ConflictManagementProvider from "../providers/generic/installing/Conflict
import PathResolver from "../r2mm/manager/PathResolver";
import ZipProvider from "../providers/generic/zip/ZipProvider";

const basePackageFiles = ["manifest.json", "readme.md", "icon.png"];


type InstallRuleArgs = {
profile: Profile,
coreRule: CoreRuleType,
Expand Down
87 changes: 87 additions & 0 deletions src/installers/ShimloaderInstaller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { InstallArgs, PackageInstaller } from "./PackageInstaller";
import path from "path";
import FsProvider from "../providers/generic/file/FsProvider";
import FileTree from "../model/file/FileTree";
import FileUtils from "../utils/FileUtils";
import R2Error from "../model/errors/R2Error";
import { InstallRuleInstaller } from "./InstallRuleInstaller";

export class ShimloaderInstaller extends PackageInstaller {
/**
* Handle installation of unreal-shimloader
*/
async install(args: InstallArgs) {
const {
mod,
packagePath,
profile,
} = args;

const fs = FsProvider.instance;
const fileRelocations = new Map<string, string>();

const targets = [
["dwmapi.dll", "dwmapi.dll"],
["UE4SS/ue4ss.dll", "ue4ss.dll"],
["UE4SS/UE4SS-settings.ini", "UE4SS-settings.ini"],
];

const ue4ssTree = await FileTree.buildFromLocation(path.join(packagePath, "UE4SS/Mods"));
if (ue4ssTree instanceof R2Error) {
throw ue4ssTree;
}

for (const subFile of ue4ssTree.getRecursiveFiles()) {
const relSrc = path.relative(path.join(packagePath, "UE4SS/Mods"), subFile);

targets.push([path.join("UE4SS/Mods", relSrc), path.join("shimloader/mod", relSrc)]);
}

for (const targetPath of targets) {
const absSrc = path.join(packagePath, targetPath[0]);
const absDest = path.join(profile.getPathOfProfile(), targetPath[1]);

await FileUtils.ensureDirectory(path.dirname(absDest));
await fs.copyFile(absSrc, absDest);

fileRelocations.set(absSrc, targetPath[1]);
}

// The config subdir needs to be created for shimloader (it will get cranky if it's not there).
const configDir = path.join(profile.getPathOfProfile(), "shimloader", "cfg");
if (!await fs.exists(configDir)) {
await fs.mkdirs(configDir);
}
}
}

export class ShimloaderPluginInstaller extends PackageInstaller {
readonly installer = new InstallRuleInstaller({
gameName: "none" as any, // This isn't acutally used for actual installation but needs some value
rules: [
{
route: path.join("shimloader", "mod"),
isDefaultLocation: true,
defaultFileExtensions: [],
trackingMethod: "SUBDIR",
subRoutes: [],
},
{
route: path.join("shimloader", "pak"),
defaultFileExtensions: [],
trackingMethod: "SUBDIR",
subRoutes: [],
},
{
route: path.join("shimloader", "cfg"),
defaultFileExtensions: [],
trackingMethod: "NONE",
subRoutes: [],
}
]
});

async install(args: InstallArgs) {
await this.installer.install(args);
}
}
3 changes: 3 additions & 0 deletions src/installers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import { GodotMLInstaller } from "./GodotMLInstaller";
import { MelonLoaderInstaller } from "./MelonLoaderInstaller";
import { PackageInstaller } from "./PackageInstaller";
import { InstallRuleInstaller } from "./InstallRuleInstaller";
import { ShimloaderInstaller, ShimloaderPluginInstaller } from "./ShimloaderInstaller";


const _PackageInstallers = {
// "legacy": new InstallRuleInstaller(), // TODO: Enable
"bepinex": new BepInExInstaller(),
"godotml": new GodotMLInstaller(),
"melonloader": new MelonLoaderInstaller(),
"shimloader": new ShimloaderInstaller(),
"shimloader-plugin": new ShimloaderPluginInstaller(),
}

export type PackageInstallerId = keyof typeof _PackageInstallers;
Expand Down
13 changes: 13 additions & 0 deletions src/model/game/GameManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,19 @@ export default class GameManager {
"https://thunderstore.io/c/sailwind/api/v1/package/", "https://raw.githubusercontent.com/ebkr/r2modmanPlus/master/modExclusions.md",
[new StorePlatformMetadata(StorePlatform.STEAM, "1764530")], "Sailwind.png",
GameSelectionDisplayMode.VISIBLE, GameInstanceType.GAME, PackageLoader.BEPINEX, []),
new Game(
"Voices of the Void", "VotV", "VotV",
"", ["VotV-Win64-Shipping.exe"], "VotV",
"https://thunderstore.io/c/voices-of-the-void/api/v1/package/", "https://raw.githubusercontent.com/ebkr/r2modmanPlus/master/modExclusions.md",
[new StorePlatformMetadata(StorePlatform.OTHER)], "VotV.png",
GameSelectionDisplayMode.VISIBLE, GameInstanceType.GAME, PackageLoader.SHIMLOADER, ["votv"]),

new Game(
"Palworld", "Palworld", "Palworld",
"Palworld", ["Palworld.exe"], "Pal",
"https://thunderstore.io/c/palworld/api/v1/package/", "https://raw.githubusercontent.com/ebkr/r2modmanPlus/master/modExclusions.md",
[new StorePlatformMetadata(StorePlatform.STEAM, "1623730")], "Palworld.png",
GameSelectionDisplayMode.VISIBLE, GameInstanceType.GAME, PackageLoader.SHIMLOADER, ["palworld"])
];

static get activeGame(): Game {
Expand Down
11 changes: 11 additions & 0 deletions src/model/installing/PackageLoader.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { PackageInstallerId, PackageInstallers } from "../../installers/registry";
import Game from "../game/Game";

export enum PackageLoader {
BEPINEX,
MELON_LOADER,
NORTHSTAR,
GODOT_ML,
ANCIENT_DUNGEON_VR,
SHIMLOADER,
}

export function GetInstallerIdForLoader(loader: PackageLoader): PackageInstallerId | null {
Expand All @@ -16,6 +18,15 @@ export function GetInstallerIdForLoader(loader: PackageLoader): PackageInstaller
case PackageLoader.MELON_LOADER: return "melonloader";
case PackageLoader.GODOT_ML: return "godotml";
case PackageLoader.NORTHSTAR: return "bepinex";
case PackageLoader.SHIMLOADER: return "shimloader";
case PackageLoader.ANCIENT_DUNGEON_VR: return null;
}
}

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

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

Expand Down
28 changes: 8 additions & 20 deletions src/r2mm/installing/InstallationRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,27 +52,15 @@ export default class InstallationRules {
public static getAllManagedPaths(rules: RuleSubtype[], pathBuilder?: string): ManagedRule[] {
const paths: ManagedRule[] = [];
rules.forEach(value => {
if (pathBuilder === undefined) {
paths.push({
route: value.route,
trackingMethod: value.trackingMethod,
extensions: value.defaultFileExtensions,
isDefaultLocation: value.isDefaultLocation || false,
ref: value
});
} else {
paths.push({
route: path.join(pathBuilder, value.route),
trackingMethod: value.trackingMethod,
extensions: value.defaultFileExtensions,
isDefaultLocation: value.isDefaultLocation || false,
ref: value
});
}
let subPath = pathBuilder === undefined ? value.route : path.join(pathBuilder, value.route);
this.getAllManagedPaths(value.subRoutes, subPath).forEach(value1 => {
paths.push(value1);
const route = !pathBuilder ? value.route : path.join(pathBuilder, value.route);
paths.push({
route: route,
trackingMethod: value.trackingMethod,
extensions: value.defaultFileExtensions,
isDefaultLocation: value.isDefaultLocation || false,
ref: value
});
this.getAllManagedPaths(value.subRoutes, route).forEach(x => paths.push(x));
});
return paths;
}
Expand Down
30 changes: 23 additions & 7 deletions src/r2mm/installing/profile_installers/GenericProfileInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,13 @@ import GameManager from '../../../model/game/GameManager';
import { MOD_LOADER_VARIANTS } from '../../installing/profile_installers/ModLoaderVariantRecord';
import FileWriteError from '../../../model/errors/FileWriteError';
import FileUtils from '../../../utils/FileUtils';
import { GetInstallerIdForLoader } from '../../../model/installing/PackageLoader';
import { GetInstallerIdForLoader, GetInstallerIdForPlugin } from '../../../model/installing/PackageLoader';
import ZipProvider from "../../../providers/generic/zip/ZipProvider";
import { PackageInstallers } from "../../../installers/registry";
import { PackageInstallerId, PackageInstallers } from "../../../installers/registry";
import { InstallArgs } from "../../../installers/PackageInstaller";
import { InstallRuleInstaller } from "../../../installers/InstallRuleInstaller";



export default class GenericProfileInstaller extends ProfileInstallerProvider {

private readonly rule: CoreRuleType;
Expand Down Expand Up @@ -156,9 +155,17 @@ export default class GenericProfileInstaller extends ProfileInstallerProvider {

if (variant !== undefined) {
return this.installModLoader(variant, args);
} else {
return this.installForManifestV2(args);
}

const pluginInstaller = GetInstallerIdForPlugin(activeGame.packageLoader);

if (pluginInstaller !== null) {
await PackageInstallers[pluginInstaller].install(args);
return Promise.resolve(null);
}

// Revert to legacy install behavior.
return this.installForManifestV2(args);
}

async installModLoader(mapping: ModLoaderPackageMapping, args: InstallArgs): Promise<R2Error | null> {
Expand Down Expand Up @@ -215,8 +222,17 @@ export default class GenericProfileInstaller extends ProfileInstallerProvider {
);
}
}
const bepInExLocation: string = path.join(profile.getPathOfProfile(), 'BepInEx');
if (await fs.exists(bepInExLocation)) {

// BepInEx & shimloader plugin uninstall logic
// TODO: Move to work through the installer interface
const profilePath = profile.getPathOfProfile();
const searchLocations = ["BepInEx", "shimloader"];
for (const searchLocation of searchLocations) {
const bepInExLocation: string = path.join(profilePath, searchLocation);
if (!(await fs.exists(bepInExLocation))) {
continue
}

try {
for (const file of (await fs.readdir(bepInExLocation))) {
if ((await fs.lstat(path.join(bepInExLocation, file))).isDirectory()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ export const MODLOADER_PACKAGES = [
new ModLoaderPackageMapping("BepInEx-BepInExPack_WizardWithAGun", "BepInExPack", PackageLoader.BEPINEX),
new ModLoaderPackageMapping("SunkenlandModding-BepInExPack_Sunkenland", "BepInExPack_Sunkenland", PackageLoader.BEPINEX),
new ModLoaderPackageMapping("BepInEx_Wormtown-BepInExPack", "BepInExPack", PackageLoader.BEPINEX),
new ModLoaderPackageMapping("0xFFF7-votv_shimloader", "", PackageLoader.SHIMLOADER),
new ModLoaderPackageMapping("Thunderstore-unreal_shimloader", "", PackageLoader.SHIMLOADER),
];


Expand Down Expand Up @@ -159,6 +161,8 @@ const VARIANTS = {
MeepleStation: MODLOADER_PACKAGES,
VoidCrew: MODLOADER_PACKAGES,
Sailwind: MODLOADER_PACKAGES,
VotV: MODLOADER_PACKAGES,
Palworld: 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
2 changes: 2 additions & 0 deletions src/r2mm/launching/instructions/GameInstructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Profile from '../../../model/Profile';
import NorthstarGameInstructions from './instructions/loader/NorthstarGameInstructions';
import { GodotMLGameInstructions } from "../../launching/instructions/instructions/loader/GodotMLGameInstructions";
import { AncientVRGameInstructions } from "../../launching/instructions/instructions/loader/AncientVRGameInstructions";
import ShimloaderGameInstructions from './instructions/loader/ShimloaderGameInstructions';

export interface GameInstruction {
moddedParameters: string,
Expand All @@ -22,6 +23,7 @@ export default class GameInstructions {
[PackageLoader.NORTHSTAR, new NorthstarGameInstructions()],
[PackageLoader.GODOT_ML, new GodotMLGameInstructions()],
[PackageLoader.ANCIENT_DUNGEON_VR, new AncientVRGameInstructions()],
[PackageLoader.SHIMLOADER, new ShimloaderGameInstructions()]
]);

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,21 @@
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 ShimloaderGameInstructions extends GameInstructionGenerator {

public async generate(game: Game, profile: Profile): Promise<GameInstruction> {
const shimloader = path.join(profile.getPathOfProfile(), "shimloader");

const luaDir = path.join(shimloader, "mod");
const pakDir = path.join(shimloader, "pak");
const cfgDir = path.join(shimloader, "cfg");

return {
moddedParameters: `--mod-dir "${luaDir}" --pak-dir "${pakDir}" --cfg-dir "${cfgDir}"`,
vanillaParameters: ""
}
}
}
Loading

0 comments on commit fcb5151

Please sign in to comment.