diff --git a/CHANGELOG.md b/CHANGELOG.md
index 32c0737d7..3a6a08d73 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,15 @@
+### 3.1.53
+#### Games added
+- WEBFISHING
+
+#### Other changes
+- BONELAB support should now function correctly
+ - Assemblies will no longer be regenerated on every launch
+- "Preloader fix" has been replaced with "Reset \ installation"
+ - This will delete the entire game directory and prompt Steam to re-validate files
+ - The preloader fix was originally a solution for old Risk of Rain 2 modded installations using SeikoML failing to
+ launch
+
### 3.1.52
#### Small quality of life changes
- Upgraded from Electron v11 to v24
@@ -6,6 +18,10 @@
- Fixed sorting by download count
- Reduced time taken to install when a mod has a large number of dependencies
+#### Games added
+- Old Market Simulator
+- Subterranauts
+
### 3.1.51
#### Memory and performance improvements
The TS team have been working hard to improve the following:
diff --git a/docs/Adding a game.md b/docs/Adding a game.md
index 619bc1ed5..f4bf17927 100644
--- a/docs/Adding a game.md
+++ b/docs/Adding a game.md
@@ -35,7 +35,6 @@
- An array of different executable names (for all platforms). For `StorePlatform` types such as `OTHER` and `STEAM_DIRECT` then the first found executable is used and called when the game is launched.
- **DataFolderName**
- Required for Unreal games relying on unreal-shimloader.
- - Relevant for Mono (C#) Unity games, which use it for the `Preloader Fix` in the manager settings.
- **TsUrl**
- The Thunderstore API endpoint for the listing.
- **ExclusionsUrl**
diff --git a/modExclusions.json b/modExclusions.json
index a42acf96f..0a4ca40a2 100644
--- a/modExclusions.json
+++ b/modExclusions.json
@@ -23,6 +23,8 @@
"L4rs-QuickFSR",
"MPModTeam-Boneworks_MP",
"MADH95Mods-JSONRenameUtility",
- "GamefaceGamers-Mod_Sync"
+ "GamefaceGamers-Mod_Sync",
+ "Pyoid-Hook_Line_and_Sinker",
+ "GardenGals-Hatchery"
]
}
diff --git a/modExclusions.md b/modExclusions.md
index 2b8302a5f..7d31408e6 100644
--- a/modExclusions.md
+++ b/modExclusions.md
@@ -51,3 +51,7 @@ codengine-SOTFEdit
DevLeon-SonsOfTheForestMap
Kesomannen-GaleModManager
+
+Pyoid-Hook_Line_and_Sinker
+
+GardenGals-Hatchery
diff --git a/package.json b/package.json
index d5150a19f..69797535d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "r2modman",
- "version": "3.1.52",
+ "version": "3.1.53",
"description": "A simple and easy to use mod manager for many games using Thunderstore.",
"productName": "r2modman",
"author": "ebkr",
@@ -109,6 +109,7 @@
"sinon": "^11.1.1",
"ts-node": "^8.10.2",
"typescript": "^4.5.5",
+ "vue": "2.7.16",
"vue-jest": "^3.0.0",
"wallaby-vue-compiler": "^1.0.3"
},
diff --git a/src/App.vue b/src/App.vue
index bff31f2db..b7f47824c 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -100,6 +100,8 @@ export default class App extends mixins(UtilityMixin) {
this.$watch('$q.dark.isActive', () => {
document.documentElement.classList.toggle('html--dark', this.$q.dark.isActive);
});
+
+ this.$store.commit('updateModLoaderPackageNames');
}
beforeCreate() {
diff --git a/src/_managerinf/ManagerInformation.ts b/src/_managerinf/ManagerInformation.ts
index 6450fe194..b68d2ee9a 100644
--- a/src/_managerinf/ManagerInformation.ts
+++ b/src/_managerinf/ManagerInformation.ts
@@ -1,7 +1,7 @@
import VersionNumber from '../model/VersionNumber';
export default class ManagerInformation {
- public static VERSION: VersionNumber = new VersionNumber('3.1.52');
+ public static VERSION: VersionNumber = new VersionNumber('3.1.53');
public static IS_PORTABLE: boolean = false;
public static APP_NAME: string = "r2modman";
}
diff --git a/src/assets/images/game_selection/SULFUR.png b/src/assets/images/game_selection/SULFUR.png
new file mode 100644
index 000000000..fb97a40ff
Binary files /dev/null and b/src/assets/images/game_selection/SULFUR.png differ
diff --git a/src/assets/images/game_selection/WEBFISHING.png b/src/assets/images/game_selection/WEBFISHING.png
new file mode 100644
index 000000000..5c947a411
Binary files /dev/null and b/src/assets/images/game_selection/WEBFISHING.png differ
diff --git a/src/components/profiles-modals/CreateProfileModal.vue b/src/components/profiles-modals/CreateProfileModal.vue
index 97cfd7997..488c6191f 100644
--- a/src/components/profiles-modals/CreateProfileModal.vue
+++ b/src/components/profiles-modals/CreateProfileModal.vue
@@ -9,6 +9,7 @@ import ProfilesMixin from "../../components/mixins/ProfilesMixin.vue";
})
export default class CreateProfileModal extends ProfilesMixin {
+ private creatingInProgress: boolean = false;
private newProfileName = '';
get isOpen(): boolean {
@@ -17,17 +18,23 @@ export default class CreateProfileModal extends ProfilesMixin {
closeModal() {
this.newProfileName = '';
+ this.creatingInProgress = false;
this.$store.commit('closeCreateProfileModal');
}
// User confirmed creation of a new profile with a name that didn't exist before.
async createProfile() {
+ if (this.creatingInProgress) {
+ return;
+ }
const safeName = this.makeProfileNameSafe(this.newProfileName);
if (safeName !== '') {
try {
+ this.creatingInProgress = true;
await this.$store.dispatch('profiles/addProfile', safeName);
this.closeModal();
} catch (e) {
+ this.creatingInProgress = false;
const err = R2Error.fromThrownValue(e, 'Error whilst creating a profile');
this.$store.commit('error/handleError', err);
}
@@ -49,7 +56,7 @@ export default class CreateProfileModal extends ProfilesMixin {
@@ -67,7 +74,7 @@ export default class CreateProfileModal extends ProfilesMixin {
-
+
diff --git a/src/components/profiles-modals/DeleteProfileModal.vue b/src/components/profiles-modals/DeleteProfileModal.vue
index d4f2dfc08..b0080afca 100644
--- a/src/components/profiles-modals/DeleteProfileModal.vue
+++ b/src/components/profiles-modals/DeleteProfileModal.vue
@@ -8,16 +8,23 @@ import ProfilesMixin from "../../components/mixins/ProfilesMixin.vue";
components: {ModalCard}
})
export default class DeleteProfileModal extends ProfilesMixin {
+ private deletingInProgress: boolean = false;
+
get isOpen(): boolean {
return this.$store.state.modals.isDeleteProfileModalOpen;
}
closeDeleteProfileModal() {
+ this.deletingInProgress = false;
this.$store.commit('closeDeleteProfileModal');
}
async removeProfile() {
+ if (this.deletingInProgress) {
+ return;
+ }
try {
+ this.deletingInProgress = true;
await this.$store.dispatch('profiles/removeSelectedProfile');
} catch (e) {
const err = R2Error.fromThrownValue(e, 'Error whilst deleting profile');
@@ -41,6 +48,7 @@ export default class DeleteProfileModal extends ProfilesMixin {
diff --git a/src/components/profiles-modals/ImportProfileModal.vue b/src/components/profiles-modals/ImportProfileModal.vue
index 7b97c03b2..628aa2366 100644
--- a/src/components/profiles-modals/ImportProfileModal.vue
+++ b/src/components/profiles-modals/ImportProfileModal.vue
@@ -117,19 +117,28 @@ export default class ImportProfileModal extends mixins(ProfilesMixin) {
return;
}
- let read: string | null = await ProfileUtils.readProfileFile(files[0]);
+ let read: string = '';
+ try {
+ read = await ProfileUtils.readProfileFile(files[0]);
+ } catch (e: unknown) {
+ const err = R2Error.fromThrownValue(e);
+ this.$store.commit('error/handleError', err);
+ this.closeModal();
+ return;
+ }
- if (read !== null) {
- this.profileImportFilePath = files[0];
+ this.profileImportFilePath = files[0];
+ try {
this.profileImportContent = await ProfileUtils.parseYamlToExportFormat(read);
-
- if (this.profileToOnlineMods.length === 0) {
- this.activeStep = 'NO_PACKAGES_IN_IMPORT';
- return;
- }
-
- this.activeStep = 'REVIEW_IMPORT';
+ } catch (e: unknown) {
+ const err = R2Error.fromThrownValue(e);
+ this.$store.commit('error/handleError', err)
+ this.closeModal();
+ return;
}
+
+ this.activeStep = this.profileToOnlineMods.length ? 'REVIEW_IMPORT' : 'NO_PACKAGES_IN_IMPORT';
+ return;
}
// Fired when user has accepted the mods to be imported in the review phase.
diff --git a/src/components/profiles-modals/RenameProfileModal.vue b/src/components/profiles-modals/RenameProfileModal.vue
index f606d8521..d6ee54b3c 100644
--- a/src/components/profiles-modals/RenameProfileModal.vue
+++ b/src/components/profiles-modals/RenameProfileModal.vue
@@ -11,6 +11,7 @@ import ProfilesMixin from "../../components/mixins/ProfilesMixin.vue";
export default class RenameProfileModal extends ProfilesMixin {
@Ref() readonly nameInput: HTMLInputElement | undefined;
private newProfileName: string = '';
+ private renamingInProgress: boolean = false;
@Watch('$store.state.profile.activeProfile')
activeProfileChanged(newProfile: Profile, oldProfile: Profile|null) {
@@ -41,12 +42,17 @@ export default class RenameProfileModal extends ProfilesMixin {
}
closeModal() {
+ this.renamingInProgress = false;
this.newProfileName = this.$store.state.profile.activeProfile.getProfileName();
this.$store.commit('closeRenameProfileModal');
}
async performRename() {
+ if (this.renamingInProgress) {
+ return;
+ }
try {
+ this.renamingInProgress = true;
await this.$store.dispatch('profiles/renameProfile', {newName: this.newProfileName});
} catch (e) {
const err = R2Error.fromThrownValue(e, 'Error whilst renaming profile');
@@ -69,7 +75,7 @@ export default class RenameProfileModal extends ProfilesMixin {
@@ -85,7 +91,7 @@ export default class RenameProfileModal extends ProfilesMixin {
-
+
diff --git a/src/components/settings-components/SettingsView.vue b/src/components/settings-components/SettingsView.vue
index 34e1f5d38..2800fda6e 100644
--- a/src/components/settings-components/SettingsView.vue
+++ b/src/components/settings-components/SettingsView.vue
@@ -142,7 +142,7 @@ import CdnProvider from '../../providers/generic/connection/CdnProvider';
),
new SettingsRow(
'Locations',
- 'Change data folder folder',
+ 'Change data folder',
'Change the folder where mods are stored for all games and profiles. The folder will not be deleted, and existing profiles will not carry across.',
async () => {
return PathResolver.ROOT;
@@ -170,14 +170,6 @@ import CdnProvider from '../../providers/generic/connection/CdnProvider';
'fa-exchange-alt',
() => this.emitInvoke('ToggleDownloadCache')
),
- new SettingsRow(
- 'Debugging',
- 'Run preloader fix',
- 'Run this to fix most errors mentioning the preloader, or about duplicate assemblies.',
- async () => `This will delete the ${this.activeGame.dataFolderName}/Managed folder, and verify the files through Steam`,
- 'fa-wrench',
- () => this.emitInvoke('RunPreloaderFix')
- ),
new SettingsRow(
'Debugging',
'Set launch parameters',
@@ -388,6 +380,14 @@ import CdnProvider from '../../providers/generic/connection/CdnProvider';
},
'fa-folder-open',
() => this.emitInvoke('ChangeSteamDirectory')
+ ),
+ new SettingsRow(
+ 'Debugging',
+ `Reset ${this.activeGame.displayName} installation`,
+ 'Fix problems caused by corrupted files or files left over from manual modding attempts.',
+ async () => `This will delete all contents of the ${this.activeGame.steamFolderName} folder, and verify the files through Steam`,
+ 'fa-wrench',
+ () => this.emitInvoke('ValidateSteamInstallation')
)
)
}
diff --git a/src/components/v2/Hero.vue b/src/components/v2/Hero.vue
new file mode 100644
index 000000000..ea82efcb7
--- /dev/null
+++ b/src/components/v2/Hero.vue
@@ -0,0 +1,24 @@
+
+
+
+
+
+ {{title}}
+
+
+ {{subtitle}}
+
+
+
+
+
+
+
diff --git a/src/components/views/DownloadModModal.vue b/src/components/views/DownloadModModal.vue
index a7390c19b..c4be2850f 100644
--- a/src/components/views/DownloadModModal.vue
+++ b/src/components/views/DownloadModModal.vue
@@ -174,7 +174,7 @@ let assignId = 0;
const existing = DownloadModModal.allVersions[assignIndex]
existing[1].failed = true;
DownloadModModal.allVersions[assignIndex] = [currentAssignId, existing[1]];
- DownloadModModal.addCdnSolutionToError(err);
+ DownloadModModal.addSolutionsToError(err);
return reject(err);
}
} else if (status === StatusEnum.PENDING) {
@@ -306,7 +306,7 @@ let assignId = 0;
const existing = DownloadModModal.allVersions[assignIndex]
existing[1].failed = true;
this.$set(DownloadModModal.allVersions, assignIndex, [currentAssignId, existing[1]]);
- DownloadModModal.addCdnSolutionToError(err);
+ DownloadModModal.addSolutionsToError(err);
this.$store.commit('error/handleError', err);
return;
}
@@ -348,7 +348,7 @@ let assignId = 0;
const existing = DownloadModModal.allVersions[assignIndex]
existing[1].failed = true;
this.$set(DownloadModModal.allVersions, assignIndex, [currentAssignId, existing[1]]);
- DownloadModModal.addCdnSolutionToError(err);
+ DownloadModModal.addSolutionsToError(err);
this.$store.commit('error/handleError', err);
return;
}
@@ -432,13 +432,22 @@ let assignId = 0;
});
}
- static addCdnSolutionToError(err: R2Error): void {
+ static addSolutionsToError(err: R2Error): void {
+ // Sanity check typing.
+ if (!(err instanceof R2Error)) {
+ return;
+ }
+
if (
err.name.includes("Failed to download mod") ||
err.name.includes("System.Net.WebException")
) {
err.solution = "Try toggling the preferred Thunderstore CDN in the settings";
}
+
+ if (err.message.includes("System.IO.PathTooLongException")) {
+ err.solution = 'Using "Change data folder" option in the settings to select a shorter path might solve the issue';
+ }
}
}
diff --git a/src/components/views/LocalModList/LocalModCard.vue b/src/components/views/LocalModList/LocalModCard.vue
index dfe29b9ba..f2b149d8c 100644
--- a/src/components/views/LocalModList/LocalModCard.vue
+++ b/src/components/views/LocalModList/LocalModCard.vue
@@ -25,6 +25,12 @@ export default class LocalModCard extends Vue {
missingDependencies: string[] = [];
disableChangePending = false;
+ get canBeDisabled(): boolean {
+ // Mod loader packages can't be disabled as it's hard to define
+ // what that should even do in all cases.
+ return !this.$store.getters['isModLoader'](this.mod.getName());
+ }
+
get donationLink() {
return this.tsMod ? this.tsMod.getDonationLink() : undefined;
}
@@ -246,7 +252,8 @@ function dependencyStringToModName(x: string) {
class='fas fa-exclamation-circle'
>
- mod.isEnabled() ? disableMod() : enableMod(mod)"
+ mod.isEnabled() ? disableMod() : enableMod(mod)"
class='card-header-icon'>
-
-
diff --git a/src/installers/GDWeaveInstaller.ts b/src/installers/GDWeaveInstaller.ts
new file mode 100644
index 000000000..7c859ad2e
--- /dev/null
+++ b/src/installers/GDWeaveInstaller.ts
@@ -0,0 +1,160 @@
+import {
+ disableModByRenamingFiles,
+ enableModByRenamingFiles,
+ InstallArgs,
+ PackageInstaller
+} from './PackageInstaller';
+import path from 'path';
+import FsProvider from '../providers/generic/file/FsProvider';
+import FileUtils from '../utils/FileUtils';
+import { MODLOADER_PACKAGES } from '../r2mm/installing/profile_installers/ModLoaderVariantRecord';
+import { PackageLoader } from '../model/installing/PackageLoader';
+import FileWriteError from '../model/errors/FileWriteError';
+import R2Error from '../model/errors/R2Error';
+
+export class GDWeaveInstaller implements PackageInstaller {
+ async install(args: InstallArgs) {
+ const { mod, packagePath, profile } = args;
+
+ const mapping = MODLOADER_PACKAGES.find(
+ (entry) =>
+ entry.packageName.toLowerCase() ==
+ mod.getName().toLowerCase() &&
+ entry.loaderType == PackageLoader.GDWEAVE
+ );
+
+ if (!mapping) {
+ throw new Error(`Missing modloader for ${mod.getName()}`);
+ }
+
+ const root = path.join(packagePath, mapping.rootFolder);
+ const toCopy = ['winmm.dll', 'GDWeave'];
+
+ for (const fileOrFolder of toCopy) {
+ await FileUtils.copyFileOrFolder(
+ path.join(root, fileOrFolder),
+ profile.joinToProfilePath(fileOrFolder)
+ );
+ }
+ }
+
+ async uninstall(args: InstallArgs): Promise
{
+ const { profile } = args;
+
+ try {
+ // Remove GDWeave/core, but keep mods and config subfolder and the log file.
+ const toDelete = [
+ profile.joinToProfilePath('winmm.dll'),
+ profile.joinToProfilePath('GDWeave', 'core'),
+ ];
+ for (const fileOrFolder of toDelete) {
+ if (!(await FsProvider.instance.exists(fileOrFolder))) {
+ continue;
+ }
+
+ if ((await FsProvider.instance.lstat(fileOrFolder)).isFile()) {
+ await FsProvider.instance.unlink(fileOrFolder);
+ } else {
+ await FileUtils.recursiveRemoveDirectoryIfExists(
+ fileOrFolder
+ );
+ }
+ }
+ } catch (e) {
+ const name = 'Failed to delete GDWeave files from profile root';
+ const solution = 'Is the game still running?';
+ throw FileWriteError.fromThrownValue(e, name, solution);
+ }
+ }
+}
+
+async function searchForManifest(
+ dir: string,
+ topLevel: boolean = true
+): Promise {
+ // Ignore `manifest.json` at the top level - that's Thunderstore's
+ if (
+ !topLevel &&
+ (await FsProvider.instance.exists(path.join(dir, 'manifest.json')))
+ ) {
+ return dir;
+ }
+
+ for (const item of await FsProvider.instance.readdir(dir)) {
+ if (
+ (await FsProvider.instance.stat(path.join(dir, item))).isDirectory()
+ ) {
+ const result = await searchForManifest(path.join(dir, item), false);
+ if (result) return result;
+ }
+ }
+
+ return null;
+}
+
+export class GDWeavePluginInstaller implements PackageInstaller {
+ getModFolderInProfile(args: InstallArgs): string {
+ return args.profile.joinToProfilePath(
+ 'GDWeave',
+ 'mods',
+ args.mod.getName()
+ );
+ }
+
+ async install(args: InstallArgs) {
+ // Packaging is all over the place. Find a folder with a manifest.json
+ // not at the top level (because the top level is Thunderstore's packaging)
+ const modFolderInCache = await searchForManifest(args.packagePath);
+ if (!modFolderInCache) {
+ throw new R2Error(
+ 'Could not find mod folder',
+ 'Either the mod package is malformed, or the files extracted to cache are corrupted'
+ );
+ }
+
+ const modFolderInProfile = this.getModFolderInProfile(args);
+
+ try {
+ await FsProvider.instance.copyFolder(modFolderInCache, modFolderInProfile);
+ } catch (e) {
+ const name = 'Failed to copy mod to profile';
+ throw FileWriteError.fromThrownValue(e, name);
+ }
+ }
+
+ async uninstall(args: InstallArgs): Promise {
+ try {
+ await FileUtils.recursiveRemoveDirectoryIfExists(
+ this.getModFolderInProfile(args)
+ );
+ } catch (e) {
+ const name = 'Failed to delete mod from profile root';
+ const solution = 'Is the game still running?';
+ throw FileWriteError.fromThrownValue(e, name, solution);
+ }
+ }
+
+ async enable(args: InstallArgs): Promise {
+ try {
+ await enableModByRenamingFiles(
+ this.getModFolderInProfile(args)
+ );
+ } catch (e) {
+ const name = 'Failed to enable mod';
+ const solution = 'Is the game still running?';
+ throw FileWriteError.fromThrownValue(e, name, solution);
+ }
+ }
+
+ async disable(args: InstallArgs): Promise {
+ try {
+ await disableModByRenamingFiles(
+ this.getModFolderInProfile(args)
+ );
+ } catch (e) {
+ const name = 'Failed to disable mod';
+ const solution = 'Is the game still running?';
+ throw FileWriteError.fromThrownValue(e, name, solution);
+ }
+ }
+}
diff --git a/src/installers/PackageInstaller.ts b/src/installers/PackageInstaller.ts
index ccca7d3e8..96aae3ab5 100644
--- a/src/installers/PackageInstaller.ts
+++ b/src/installers/PackageInstaller.ts
@@ -1,5 +1,8 @@
+import R2Error from "../model/errors/R2Error";
+import FileTree from "../model/file/FileTree";
import { ImmutableProfile } from "../model/Profile";
import ManifestV2 from "../model/ManifestV2";
+import FsProvider from "../providers/generic/file/FsProvider";
export type InstallArgs = {
mod: ManifestV2;
@@ -10,4 +13,35 @@ export type InstallArgs = {
export interface PackageInstaller {
install(args: InstallArgs): Promise;
uninstall?(args: InstallArgs): Promise;
+ enable?(args: InstallArgs): Promise;
+ disable?(args: InstallArgs): Promise;
+}
+
+export async function disableModByRenamingFiles(folderName: string) {
+ const tree = await FileTree.buildFromLocation(folderName);
+ if (tree instanceof R2Error) {
+ throw tree;
+ }
+
+ for (const filePath of tree.getRecursiveFiles()) {
+ if (!filePath.toLowerCase().endsWith(".old")) {
+ await FsProvider.instance.rename(filePath, `${filePath}.old`);
+ }
+ }
+}
+
+export async function enableModByRenamingFiles(folderName: string) {
+ const tree = await FileTree.buildFromLocation(folderName);
+ if (tree instanceof R2Error) {
+ throw tree;
+ }
+
+ for (const filePath of tree.getRecursiveFiles()) {
+ if (filePath.toLowerCase().endsWith(".old")) {
+ await FsProvider.instance.rename(
+ filePath,
+ filePath.substring(0, filePath.length - ('.old').length)
+ );
+ }
+ }
}
diff --git a/src/installers/registry.ts b/src/installers/registry.ts
index b665f989d..e0c3dc816 100644
--- a/src/installers/registry.ts
+++ b/src/installers/registry.ts
@@ -6,7 +6,7 @@ import { ShimloaderInstaller, ShimloaderPluginInstaller } from './ShimloaderInst
import { LovelyInstaller, LovelyPluginInstaller } from './LovelyInstaller';
import { NorthstarInstaller } from './NorthstarInstaller';
import { ReturnOfModdingInstaller, ReturnOfModdingPluginInstaller } from './ReturnOfModdingInstaller';
-
+import { GDWeaveInstaller, GDWeavePluginInstaller } from './GDWeaveInstaller';
const _PackageInstallers = {
// "legacy": new InstallRuleInstaller(), // TODO: Enable
@@ -20,6 +20,8 @@ const _PackageInstallers = {
"lovely-plugin": new LovelyPluginInstaller(),
"returnofmodding": new ReturnOfModdingInstaller(),
"returnofmodding-plugin": new ReturnOfModdingPluginInstaller(),
+ "gdweave": new GDWeaveInstaller(),
+ "gdweave-plugin": new GDWeavePluginInstaller(),
}
export type PackageInstallerId = keyof typeof _PackageInstallers;
diff --git a/src/model/enums/DependencyListDisplayType.ts b/src/model/enums/DependencyListDisplayType.ts
deleted file mode 100644
index 9f4be9dae..000000000
--- a/src/model/enums/DependencyListDisplayType.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export default {
- DISABLE: 'disable',
- UNINSTALL: 'uninstall',
- VIEW: 'view',
-};
\ No newline at end of file
diff --git a/src/model/game/GameManager.ts b/src/model/game/GameManager.ts
index e6c4c4106..d1c8beecb 100644
--- a/src/model/game/GameManager.ts
+++ b/src/model/game/GameManager.ts
@@ -812,6 +812,18 @@ export default class GameManager {
"https://thunderstore.io/c/subterranauts/api/v1/package-listing-index/", EXCLUSIONS,
[new StorePlatformMetadata(StorePlatform.STEAM, "3075800")], "Subterranauts.png",
GameSelectionDisplayMode.VISIBLE, GameInstanceType.GAME, PackageLoader.BEPINEX, [""]),
+
+ new Game("SULFUR", "SULFUR", "SULFUR",
+ "Sulfur", ["Sulfur.exe"], "Sulfur_Data",
+ "https://thunderstore.io/c/sulfur/api/v1/package-listing-index/", EXCLUSIONS,
+ [new StorePlatformMetadata(StorePlatform.STEAM, "2124120")], "SULFUR.png",
+ GameSelectionDisplayMode.VISIBLE, GameInstanceType.GAME, PackageLoader.BEPINEX, [""]),
+
+ new Game("WEBFISHING", "WEBFISHING", "WEBFISHING",
+ "WEBFISHING", ["webfishing.exe"], "",
+ "https://thunderstore.io/c/webfishing/api/v1/package-listing-index/", EXCLUSIONS,
+ [new StorePlatformMetadata(StorePlatform.STEAM, "3146520")], "WEBFISHING.png",
+ GameSelectionDisplayMode.VISIBLE, GameInstanceType.GAME, PackageLoader.GDWEAVE, [""]),
];
static get activeGame(): Game {
diff --git a/src/model/installing/PackageLoader.ts b/src/model/installing/PackageLoader.ts
index 0d90c3b75..f984f3d5f 100644
--- a/src/model/installing/PackageLoader.ts
+++ b/src/model/installing/PackageLoader.ts
@@ -9,6 +9,7 @@ export enum PackageLoader {
SHIMLOADER,
LOVELY,
RETURN_OF_MODDING,
+ GDWEAVE,
}
export function GetInstallerIdForLoader(loader: PackageLoader): PackageInstallerId | null {
@@ -22,6 +23,7 @@ export function GetInstallerIdForLoader(loader: PackageLoader): PackageInstaller
case PackageLoader.SHIMLOADER: return "shimloader";
case PackageLoader.LOVELY: return "lovely";
case PackageLoader.RETURN_OF_MODDING: return "returnofmodding";
+ case PackageLoader.GDWEAVE: return "gdweave";
case PackageLoader.ANCIENT_DUNGEON_VR: return null;
}
}
@@ -31,6 +33,7 @@ export function GetInstallerIdForPlugin(loader: PackageLoader): PackageInstaller
case PackageLoader.SHIMLOADER: return "shimloader-plugin";
case PackageLoader.LOVELY: return "lovely-plugin";
case PackageLoader.RETURN_OF_MODDING: return "returnofmodding-plugin";
+ case PackageLoader.GDWEAVE: return "gdweave-plugin";
}
return null;
diff --git a/src/pages/GameSelectionScreen.vue b/src/pages/GameSelectionScreen.vue
index 2fb4bd95e..7263af503 100644
--- a/src/pages/GameSelectionScreen.vue
+++ b/src/pages/GameSelectionScreen.vue
@@ -164,7 +164,7 @@
import { Component, Vue } from 'vue-property-decorator';
import Game from '../model/game/Game';
import GameManager from '../model/game/GameManager';
-import Hero from '../components/Hero.vue';
+import Hero from '../components/v2/Hero.vue';
import * as ManagerUtils from '../utils/ManagerUtils';
import ManagerSettings from '../r2mm/manager/ManagerSettings';
import { StorePlatform } from '../model/game/StorePlatform';
diff --git a/src/pages/Help.vue b/src/pages/Help.vue
index c53cc10b1..74cef80cc 100644
--- a/src/pages/Help.vue
+++ b/src/pages/Help.vue
@@ -71,7 +71,7 @@
That's because you don't legally own the game. The manager only supports legal copies.
A text window appears and closes immediately.
- Try running the preloader fix on the Settings screen.
+ Try running "Reset {{$store.state.activeGame.displayName}} installation" on the Settings screen.
If it persists, force exit Steam and start modded with Steam closed.
diff --git a/src/pages/Manager.vue b/src/pages/Manager.vue
index 8f4366499..459999cd7 100644
--- a/src/pages/Manager.vue
+++ b/src/pages/Manager.vue
@@ -35,25 +35,29 @@
-
+
- Attempting to fix preloader issues
+ Clearing the {{activeGame.displayName}} installation directory
-
You will not not be able to launch the game until Steam has verified the integrity of the
- game.
+
+ You will not not be able to launch the game until
+ Steam has verified the integrity of the game files.
- Steam will be started, and will attempt to verify the integrity of {{ activeGame.displayName }}.
+
+ Steam will be started and will attempt to verify the
+ integrity of {{ activeGame.displayName }}.
+
- Please check the Steam window for validation progress. If the window has not yet appeared, please be
- patient.
+
+ Please check the Steam window for validation progress.
+ If the window has not yet appeared, please be patient.
-
@@ -121,7 +125,7 @@
-
+
Done
@@ -144,13 +148,12 @@ import { Hero, Link, Modal, Progress } from '../components/all';
import ThunderstoreCombo from '../model/ThunderstoreCombo';
import ProfileModList from '../r2mm/mods/ProfileModList';
import PathResolver from '../r2mm/manager/PathResolver';
-import PreloaderFixer from '../r2mm/manager/PreloaderFixer';
+import { SteamInstallationValidator} from '../r2mm/manager/SteamInstallationValidator';
import { LogSeverity } from '../providers/ror2/logging/LoggerProvider';
import Profile from '../model/Profile';
import VersionNumber from '../model/VersionNumber';
-import DependencyListDisplayType from '../model/enums/DependencyListDisplayType';
import R2Error from '../model/errors/R2Error';
import ManifestV2 from '../model/ManifestV2';
import ManagerSettings from '../r2mm/manager/ManagerSettings';
@@ -187,10 +190,9 @@ import ModalCard from '../components/ModalCard.vue';
}
})
export default class Manager extends Vue {
- dependencyListDisplayType: string = DependencyListDisplayType.DISABLE;
portableUpdateAvailable: boolean = false;
updateTagName: string = '';
- fixingPreloader: boolean = false;
+ isValidatingSteamInstallation: boolean = false;
exportCode: string = '';
showSteamIncorrectDirectoryModal: boolean = false;
showRor2IncorrectDirectoryModal: boolean = false;
@@ -217,16 +219,16 @@ import ModalCard from '../components/ModalCard.vue';
return this.$store.state.profile.modList;
}
- closePreloaderFixModal() {
- this.fixingPreloader = false;
+ closeSteamInstallationValidationModal() {
+ this.isValidatingSteamInstallation = false;
}
- async fixPreloader() {
- const res = await PreloaderFixer.fix(this.activeGame);
+ async validateSteamInstallation() {
+ const res = await SteamInstallationValidator.validateInstallation(this.activeGame);
if (res instanceof R2Error) {
this.$store.commit('error/handleError', res);
} else {
- this.fixingPreloader = true;
+ this.isValidatingSteamInstallation = true;
}
}
@@ -490,9 +492,12 @@ import ModalCard from '../components/ModalCard.vue';
case PackageLoader.MELON_LOADER:
logOutputPath = path.join(this.profile.getProfilePath(), "MelonLoader", "Latest.log");
break;
- case PackageLoader.RETURN_OF_MODDING:
+ case PackageLoader.RETURN_OF_MODDING:
logOutputPath = path.join(this.profile.getProfilePath(), "ReturnOfModding", "LogOutput.log");
break;
+ case PackageLoader.GDWEAVE:
+ logOutputPath = path.join(this.profile.getProfilePath(), "GDWeave", "GDWeave.log");
+ break;
}
const text = (await fs.readFile(logOutputPath)).toString();
if (text.length >= 1992) {
@@ -543,8 +548,8 @@ import ModalCard from '../components/ModalCard.vue';
case "ToggleDownloadCache":
this.toggleIgnoreCache();
break;
- case "RunPreloaderFix":
- this.fixPreloader();
+ case "ValidateSteamInstallation":
+ this.validateSteamInstallation();
break;
case "SetLaunchParameters":
this.showLaunchParameters();
diff --git a/src/providers/generic/game/platform_interceptor/PlatformInterceptorImpl.ts b/src/providers/generic/game/platform_interceptor/PlatformInterceptorImpl.ts
index f85900f39..5ecfc5483 100644
--- a/src/providers/generic/game/platform_interceptor/PlatformInterceptorImpl.ts
+++ b/src/providers/generic/game/platform_interceptor/PlatformInterceptorImpl.ts
@@ -66,6 +66,7 @@ function buildRunners(runners: PlatformRunnersType): LoaderRunnersType {
[PackageLoader.SHIMLOADER]: runners,
[PackageLoader.LOVELY]: runners,
[PackageLoader.RETURN_OF_MODDING]: runners,
+ [PackageLoader.GDWEAVE]: runners,
}
}
diff --git a/src/providers/generic/zip/AdmZipProvider.ts b/src/providers/generic/zip/AdmZipProvider.ts
index d2bb96139..d12d14f99 100644
--- a/src/providers/generic/zip/AdmZipProvider.ts
+++ b/src/providers/generic/zip/AdmZipProvider.ts
@@ -1,18 +1,15 @@
import ZipProvider from './ZipProvider';
-import AdmZip, { IZipEntry } from 'adm-zip';
+import AdmZip from 'adm-zip';
import * as path from 'path';
import ZipBuilder from './ZipBuilder';
import ZipEntryInterface from './ZipEntryInterface';
-import FileUtils from '../../../utils/FileUtils';
-import FsProvider from '../file/FsProvider';
export default class AdmZipProvider extends ZipProvider {
async extractAllTo(zip: string | Buffer, outputFolder: string): Promise {
const adm = new AdmZip(zip);
- for (let entry of adm.getEntries()) {
- await this.sanitizedExtraction(entry, outputFolder);
- }
+ outputFolder = outputFolder.replace(/\\/g, '/');
+ adm.extractAllTo(outputFolder, true);
}
async readFile(zip: string | Buffer, file: string): Promise {
@@ -27,16 +24,14 @@ export default class AdmZipProvider extends ZipProvider {
async extractEntryTo(zip: string | Buffer, target: string, outputPath: string): Promise {
const adm = new AdmZip(zip);
- return this.sanitizedExtraction(adm.getEntry(target)!, outputPath);
- }
-
- private async sanitizedExtraction(entry: IZipEntry, outputPath: string): Promise {
- const sanitizedTargetName = entry.entryName.split('\\').join('/');
- await FileUtils.ensureDirectory(path.dirname(path.join(outputPath, sanitizedTargetName)));
- if (entry.isDirectory)
- await FileUtils.ensureDirectory(path.join(outputPath, sanitizedTargetName));
- else
- await FsProvider.instance.writeFile(path.join(outputPath, sanitizedTargetName), entry.getData());
+ target = target.replace(/\\/g, '/');
+ outputPath = outputPath.replace(/\\/g, '/');
+ var fullPath = path.join(outputPath, target).replace(/\\/g, '/');
+ if(!path.posix.normalize(fullPath).startsWith(outputPath))
+ {
+ throw Error("Entry " + target + " would extract outside of expected folder");
+ }
+ adm.extractEntryTo(target, outputPath, true, true);
}
zipBuilder(): ZipBuilder {
diff --git a/src/r2mm/data/LogOutput.ts b/src/r2mm/data/LogOutput.ts
index 475d28b31..6f831b323 100644
--- a/src/r2mm/data/LogOutput.ts
+++ b/src/r2mm/data/LogOutput.ts
@@ -46,6 +46,10 @@ export default class LogOutput {
fs.exists(Profile.getActiveProfile().joinToProfilePath('MelonLoader', 'Latest.log'))
.then(value => this._exists = value);
break;
+ case PackageLoader.GDWEAVE:
+ fs.exists(Profile.getActiveProfile().joinToProfilePath('GDWeave', 'GDWeave.log'))
+ .then(value => this._exists = value);
+ break;
}
}
diff --git a/src/r2mm/installing/ZipExtract.ts b/src/r2mm/installing/ZipExtract.ts
index e595e2d00..cfb735ec4 100644
--- a/src/r2mm/installing/ZipExtract.ts
+++ b/src/r2mm/installing/ZipExtract.ts
@@ -36,16 +36,20 @@ export default class ZipExtract {
}
}
} catch (e) {
+ // Cleanup might also fail e.g. for too long file paths on TSMM.
+ // Show the original error instead of the one caused by the cleanup,
+ // as the former is probably more informative for debugging.
+ if (err) {
+ callback(false, FileWriteError.fromThrownValue(err));
+ return;
+ }
+
callback(result, new FileWriteError(
'Failed to extract zip',
(e as Error).message,
'Try to re-download the mod. If the issue persists, ask for help in the Thunderstore modding discord.'
));
}
- // TODO: Is this needed?
- // finally {
- // callback(result);
- // }
}
});
}
diff --git a/src/r2mm/installing/default_installation_rules/InstallationRuleApplicator.ts b/src/r2mm/installing/default_installation_rules/InstallationRuleApplicator.ts
index b05dd4c47..4e1bbdf78 100644
--- a/src/r2mm/installing/default_installation_rules/InstallationRuleApplicator.ts
+++ b/src/r2mm/installing/default_installation_rules/InstallationRuleApplicator.ts
@@ -152,6 +152,7 @@ export default class InstallationRuleApplicator {
buildBepInExRules("TCGCardShopSimulator"),
buildBepInExRules("OldMarketSimulator"),
buildBepInExRules("Subterranauts"),
+ buildBepInExRules("SULFUR"),
]
}
}
diff --git a/src/r2mm/installing/profile_installers/GenericProfileInstaller.ts b/src/r2mm/installing/profile_installers/GenericProfileInstaller.ts
index 518629288..35936215f 100644
--- a/src/r2mm/installing/profile_installers/GenericProfileInstaller.ts
+++ b/src/r2mm/installing/profile_installers/GenericProfileInstaller.ts
@@ -118,10 +118,32 @@ export default class GenericProfileInstaller extends ProfileInstallerProvider {
}
async disableMod(mod: ManifestV2, profile: ImmutableProfile): Promise {
+ // Support for installer specific disable methods are rolled out
+ // gradually and therefore might not be defined yet. Disabling
+ // mod loader packages are intentionally not supported.
+ try {
+ if (await this.disableModWithInstaller(mod, profile)) {
+ return;
+ }
+ } catch (e) {
+ return R2Error.fromThrownValue(e);
+ }
+
return this.applyModMode(mod, profile, ModMode.DISABLED);
}
async enableMod(mod: ManifestV2, profile: ImmutableProfile): Promise {
+ // Support for installer specific enable methods are rolled out
+ // gradually and therefore might not be defined yet. Enabling
+ // mod loader packages are intentionally not supported.
+ try {
+ if (await this.enableModWithInstaller(mod, profile)) {
+ return;
+ }
+ } catch (e) {
+ return R2Error.fromThrownValue(e);
+ }
+
return this.applyModMode(mod, profile, ModMode.ENABLED);
}
@@ -149,8 +171,12 @@ export default class GenericProfileInstaller extends ProfileInstallerProvider {
const pluginInstaller = GetInstallerIdForPlugin(GameManager.activeGame.packageLoader);
if (pluginInstaller !== null) {
- await PackageInstallers[pluginInstaller].install(args);
- return Promise.resolve(null);
+ try {
+ await PackageInstallers[pluginInstaller].install(args);
+ return Promise.resolve(null);
+ } catch (e) {
+ return Promise.resolve(R2Error.fromThrownValue(e));
+ }
}
// Revert to legacy install behavior.
@@ -308,7 +334,7 @@ export default class GenericProfileInstaller extends ProfileInstallerProvider {
* implements a custom uninstallation method.
* @return true if mod loader was uninstalled
*/
- async uninstallModLoaderWithInstaller(mod: ManifestV2, profile: ImmutableProfile): Promise {
+ private async uninstallModLoaderWithInstaller(mod: ManifestV2, profile: ImmutableProfile): Promise {
const modLoader = this.getModLoader(mod);
const installerId = modLoader ? GetInstallerIdForLoader(modLoader.loaderType) : null;
return this.uninstallWithInstaller(installerId, mod, profile);
@@ -319,7 +345,7 @@ export default class GenericProfileInstaller extends ProfileInstallerProvider {
* uninstallation method.
* @return true if mod was uninstalled
*/
- async uninstallModWithInstaller(mod: ManifestV2, profile: ImmutableProfile): Promise {
+ private async uninstallModWithInstaller(mod: ManifestV2, profile: ImmutableProfile): Promise {
const installerId = GetInstallerIdForPlugin(GameManager.activeGame.packageLoader);
return this.uninstallWithInstaller(installerId, mod, profile);
}
@@ -339,4 +365,52 @@ export default class GenericProfileInstaller extends ProfileInstallerProvider {
return false;
}
+
+ /**
+ * Enable mod if its registered installer implements a custom
+ * enable method.
+ * @return true if mod was enable
+ */
+ private async enableModWithInstaller(mod: ManifestV2, profile: ImmutableProfile): Promise {
+ const modLoader = this.getModLoader(mod);
+
+ if (modLoader) {
+ return false; // Don't process mod loader with plugin installer.
+ }
+
+ const installerId = GetInstallerIdForPlugin(GameManager.activeGame.packageLoader);
+ const installer = installerId ? PackageInstallers[installerId] : undefined;
+
+ if (installer && installer.enable) {
+ const args = this.getInstallArgs(mod, profile);
+ await installer.enable(args);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Disable mod if its registered installer implements a custom
+ * disable method.
+ * @return true if mod was disabled
+ */
+ private async disableModWithInstaller(mod: ManifestV2, profile: ImmutableProfile): Promise {
+ const modLoader = this.getModLoader(mod);
+
+ if (modLoader) {
+ return false; // Don't process mod loader with plugin installer.
+ }
+
+ const installerId = GetInstallerIdForPlugin(GameManager.activeGame.packageLoader);
+ const installer = installerId ? PackageInstallers[installerId] : undefined;
+
+ if (installer && installer.disable) {
+ const args = this.getInstallArgs(mod, profile);
+ await installer.disable(args);
+ return true;
+ }
+
+ return false;
+ }
}
diff --git a/src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts b/src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts
index 31e4bd606..71c87a178 100644
--- a/src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts
+++ b/src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts
@@ -70,6 +70,7 @@ export const MODLOADER_PACKAGES = [
new ModLoaderPackageMapping("Thunderstore-lovely", "", PackageLoader.LOVELY),
new ModLoaderPackageMapping("ReturnOfModding-ReturnOfModding", "ReturnOfModdingPack", PackageLoader.RETURN_OF_MODDING),
new ModLoaderPackageMapping("Hell2Modding-Hell2Modding", "ReturnOfModdingPack", PackageLoader.RETURN_OF_MODDING),
+ new ModLoaderPackageMapping("NotNet-GDWeave", "", PackageLoader.GDWEAVE),
];
@@ -123,7 +124,7 @@ const VARIANTS = {
Aloft: MODLOADER_PACKAGES,
COTL: MODLOADER_PACKAGES,
ChronoArk: MODLOADER_PACKAGES,
- BONELAB: [new ModLoaderPackageMapping("LavaGang-MelonLoader", "", PackageLoader.MELON_LOADER, new VersionNumber("0.5.7"))],
+ BONELAB: [new ModLoaderPackageMapping("LavaGang-MelonLoader", "", PackageLoader.MELON_LOADER)],
TromboneChamp: MODLOADER_PACKAGES,
RogueGenesia: MODLOADER_PACKAGES,
AcrossTheObelisk: MODLOADER_PACKAGES,
@@ -203,9 +204,22 @@ const VARIANTS = {
TCGCardShopSimulator: MODLOADER_PACKAGES,
OldMarketSimulator: MODLOADER_PACKAGES,
Subterranauts: MODLOADER_PACKAGES,
+ SULFUR: MODLOADER_PACKAGES,
+ WEBFISHING: 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.
// Casting is done here to ensure the values are ModLoaderPackageMapping[]
export type GAME_NAME = keyof typeof VARIANTS;
export const MOD_LOADER_VARIANTS: {[key in GAME_NAME]: ModLoaderPackageMapping[]} = VARIANTS;
+
+export function getModLoaderPackageNames() {
+ const names = MODLOADER_PACKAGES.map((mapping) => mapping.packageName);
+
+ // Hard code MelonLoader to avoid having to iterate over MODLOADER_PACKAGES
+ // for each game separately. Hopefully we'll get rid of this once ML v0.6.6
+ // is released, as it's supposed to fix a bug that forces some games to
+ // currently use the older versions.
+ names.push("LavaGang-MelonLoader");
+ return names;
+}
diff --git a/src/r2mm/launching/instructions/DynamicGameInstruction.ts b/src/r2mm/launching/instructions/DynamicGameInstruction.ts
index 93d79f8f5..25d35a871 100644
--- a/src/r2mm/launching/instructions/DynamicGameInstruction.ts
+++ b/src/r2mm/launching/instructions/DynamicGameInstruction.ts
@@ -4,4 +4,5 @@ export enum DynamicGameInstruction {
BEPINEX_CORLIBS = "@bepInExCorlibs",
PROFILE_NAME = "@profileName",
NORTHSTAR_DIRECTORY = "@northstarDirectory",
+ GDWEAVE_FOLDER = "@gdweaveFolder"
}
diff --git a/src/r2mm/launching/instructions/GameInstructionParser.ts b/src/r2mm/launching/instructions/GameInstructionParser.ts
index fc72ba2b3..60962eadf 100644
--- a/src/r2mm/launching/instructions/GameInstructionParser.ts
+++ b/src/r2mm/launching/instructions/GameInstructionParser.ts
@@ -15,7 +15,8 @@ export default class GameInstructionParser {
[DynamicGameInstruction.PROFILE_DIRECTORY, GameInstructionParser.profileDirectoryResolver],
[DynamicGameInstruction.BEPINEX_CORLIBS, GameInstructionParser.bepInExCorelibsPathResolver],
[DynamicGameInstruction.PROFILE_NAME, GameInstructionParser.profileNameResolver],
- [DynamicGameInstruction.NORTHSTAR_DIRECTORY, GameInstructionParser.northstarDirectoryResolver]
+ [DynamicGameInstruction.NORTHSTAR_DIRECTORY, GameInstructionParser.northstarDirectoryResolver],
+ [DynamicGameInstruction.GDWEAVE_FOLDER, GameInstructionParser.gdweaveFolderResolver]
]);
public static async parse(launchString: string, game: Game, profile: Profile): Promise {
@@ -75,4 +76,7 @@ export default class GameInstructionParser {
return profile.joinToProfilePath("R2Northstar");
}
+ private static async gdweaveFolderResolver(game: Game, profile: Profile): Promise {
+ return profile.joinToProfilePath("GDWeave");
+ }
}
diff --git a/src/r2mm/launching/instructions/GameInstructions.ts b/src/r2mm/launching/instructions/GameInstructions.ts
index 43b7ae233..db3561b1d 100644
--- a/src/r2mm/launching/instructions/GameInstructions.ts
+++ b/src/r2mm/launching/instructions/GameInstructions.ts
@@ -10,6 +10,7 @@ import { AncientVRGameInstructions } from "../../launching/instructions/instruct
import ShimloaderGameInstructions from './instructions/loader/ShimloaderGameInstructions';
import LovelyGameInstructions from './instructions/loader/LovelyGameInstructions';
import ReturnOfModdingGameInstructions from './instructions/loader/ReturnOfModdingGameInstructions';
+import GDWeaveGameInstructions from './instructions/loader/GDWeaveGameInstructions';
export interface GameInstruction {
moddedParameters: string,
@@ -28,6 +29,7 @@ export default class GameInstructions {
[PackageLoader.SHIMLOADER, new ShimloaderGameInstructions()],
[PackageLoader.LOVELY, new LovelyGameInstructions()],
[PackageLoader.RETURN_OF_MODDING, new ReturnOfModdingGameInstructions()],
+ [PackageLoader.GDWEAVE, new GDWeaveGameInstructions()]
]);
public static async getInstructionsForGame(game: Game, profile: Profile): Promise {
diff --git a/src/r2mm/launching/instructions/instructions/loader/GDWeaveGameInstructions.ts b/src/r2mm/launching/instructions/instructions/loader/GDWeaveGameInstructions.ts
new file mode 100644
index 000000000..911ed214a
--- /dev/null
+++ b/src/r2mm/launching/instructions/instructions/loader/GDWeaveGameInstructions.ts
@@ -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 GDWeaveGameInstructions extends GameInstructionGenerator {
+ public async generate(game: Game, profile: Profile): Promise {
+ return {
+ moddedParameters: `--gdweave-folder-override="${DynamicGameInstruction.GDWEAVE_FOLDER}"`,
+ vanillaParameters: "--gdweave-disable"
+ };
+ }
+}
diff --git a/src/r2mm/launching/instructions/instructions/loader/MelonLoaderGameInstructions.ts b/src/r2mm/launching/instructions/instructions/loader/MelonLoaderGameInstructions.ts
index 3b8914813..33196c343 100644
--- a/src/r2mm/launching/instructions/instructions/loader/MelonLoaderGameInstructions.ts
+++ b/src/r2mm/launching/instructions/instructions/loader/MelonLoaderGameInstructions.ts
@@ -9,9 +9,12 @@ export default class MelonLoaderGameInstructions extends GameInstructionGenerato
public async generate(game: Game, profile: Profile): Promise {
let moddedParameters = `--melonloader.basedir "${DynamicGameInstruction.PROFILE_DIRECTORY}"`;
- if (!await FsProvider.instance.exists(profile.joinToProfilePath('MelonLoader', 'Managed', 'Assembly-CSharp.dll'))) {
- console.log("Regenerating AGF")
- moddedParameters += " --melonloader.agfregenerate"
+
+ const mlZeroPointFiveAssemblyExists = await FsProvider.instance.exists(profile.joinToProfilePath('MelonLoader', 'Managed', 'Assembly-CSharp.dll'));
+ const mlZeroPointSixAssemblyExists = await FsProvider.instance.exists(profile.joinToProfilePath('MelonLoader', 'Il2CppAssemblies', 'Assembly-CSharp.dll'));
+
+ if (!mlZeroPointFiveAssemblyExists && !mlZeroPointSixAssemblyExists) {
+ moddedParameters += ' --melonloader.agfregenerate';
}
return {
moddedParameters: moddedParameters,
diff --git a/src/r2mm/launching/runners/linux/SteamGameRunner_Linux.ts b/src/r2mm/launching/runners/linux/SteamGameRunner_Linux.ts
index 08e273a0d..81cba2839 100644
--- a/src/r2mm/launching/runners/linux/SteamGameRunner_Linux.ts
+++ b/src/r2mm/launching/runners/linux/SteamGameRunner_Linux.ts
@@ -11,6 +11,7 @@ import LoggerProvider, { LogSeverity } from '../../../../providers/ror2/logging/
import { exec } from 'child_process';
import GameInstructions from '../../instructions/GameInstructions';
import GameInstructionParser from '../../instructions/GameInstructionParser';
+import { PackageLoader } from '../../../../model/installing/PackageLoader';
export default class SteamGameRunner_Linux extends GameRunnerProvider {
@@ -27,7 +28,9 @@ export default class SteamGameRunner_Linux extends GameRunnerProvider {
}
if (isProton) {
- const promise = await this.ensureWineWillLoadBepInEx(game);
+ // BepInEx uses winhttp, GDWeave uses winmm. More can be added later.
+ const proxyDll = game.packageLoader == PackageLoader.GDWEAVE ? "winmm" : "winhttp";
+ const promise = await this.ensureWineWillLoadDllOverride(game, proxyDll);
if (promise instanceof R2Error) {
return promise;
}
@@ -80,7 +83,7 @@ export default class SteamGameRunner_Linux extends GameRunnerProvider {
}
- private async ensureWineWillLoadBepInEx(game: Game): Promise{
+ private async ensureWineWillLoadDllOverride(game: Game, proxyDll: string): Promise{
const fs = FsProvider.instance;
const compatDataDir = await (GameDirectoryResolverProvider.instance as LinuxGameDirectoryResolver).getCompatDataDirectory(game);
if(compatDataDir instanceof R2Error)
@@ -90,7 +93,7 @@ export default class SteamGameRunner_Linux extends GameRunnerProvider {
const ensuredUserRegData = this.regAddInSection(
userRegData,
"[Software\\\\Wine\\\\DllOverrides]",
- "winhttp",
+ proxyDll,
"native,builtin"
);
diff --git a/src/r2mm/manager/ModLinker.ts b/src/r2mm/manager/ModLinker.ts
index 2f276e21d..cd0b45aca 100644
--- a/src/r2mm/manager/ModLinker.ts
+++ b/src/r2mm/manager/ModLinker.ts
@@ -128,7 +128,7 @@ export default class ModLinker {
"bepinex", "bepinex_server", "mods",
"melonloader", "plugins", "userdata",
"_state", "userlibs", "qmods", "shimloader",
- "returnofmodding"
+ "returnofmodding", "gdweave"
];
if (!exclusionsList.includes(file.toLowerCase())) {
diff --git a/src/r2mm/manager/PreloaderFixer.ts b/src/r2mm/manager/PreloaderFixer.ts
deleted file mode 100644
index 1250f1cba..000000000
--- a/src/r2mm/manager/PreloaderFixer.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import GameDirectoryResolverProvider from '../../providers/ror2/game/GameDirectoryResolverProvider';
-import R2Error from '../../model/errors/R2Error';
-import * as path from 'path';
-import FsProvider from '../../providers/generic/file/FsProvider';
-import ManagerInformation from '../../_managerinf/ManagerInformation';
-import LinkProvider from '../../providers/components/LinkProvider';
-import FileUtils from '../../utils/FileUtils';
-import Game from '../../model/game/Game';
-import { StorePlatform } from '../../model/game/StorePlatform';
-
-export default class PreloaderFixer {
-
- public static async fix(game: Game): Promise {
- if (![StorePlatform.STEAM, StorePlatform.STEAM_DIRECT].includes(game.activePlatform.storePlatform)) {
- return new R2Error(
- "PreloaderFix is not available on non-Steam platforms.",
- `The preloader fix deletes the ${path.join(game.dataFolderName, 'Managed')} folder and verifies files. You can do the same manually.`,
- null
- );
- }
- const fs = FsProvider.instance;
- const dirResult = await GameDirectoryResolverProvider.instance.getDirectory(game);
- if (dirResult instanceof R2Error)
- return dirResult;
-
- let exeFound = false;
- for(let exeName of game.exeName) {
- if (await fs.exists(path.join(dirResult, exeName))) {
- exeFound = true;
- break;
- }
- }
-
- if(!exeFound) {
- return new R2Error(`${game.displayName} folder is invalid`, `could not find either of "${game.exeName.join('", "')}"`,
- `Set the ${game.displayName} folder in the settings section`);
- }
-
- try {
- await FileUtils.emptyDirectory(path.join(dirResult, game.dataFolderName, 'Managed'));
- await fs.rmdir(path.join(dirResult, game.dataFolderName, 'Managed'));
- } catch(e) {
- const err: Error = e as Error;
- return new R2Error('Failed to remove Managed folder', err.message, `Try launching ${ManagerInformation.APP_NAME} as an administrator`);
- }
- try {
- LinkProvider.instance.openLink(`steam://validate/${game.activePlatform.storeIdentifier}`);
- } catch(e) {
- const err: Error = e as Error;
- return new R2Error('Failed to start steam://validate', err.message, null);
- }
- }
-}
diff --git a/src/r2mm/manager/SteamInstallationValidator.ts b/src/r2mm/manager/SteamInstallationValidator.ts
new file mode 100644
index 000000000..055c0a342
--- /dev/null
+++ b/src/r2mm/manager/SteamInstallationValidator.ts
@@ -0,0 +1,64 @@
+import GameDirectoryResolverProvider from '../../providers/ror2/game/GameDirectoryResolverProvider';
+import R2Error from '../../model/errors/R2Error';
+import * as path from 'path';
+import FsProvider from '../../providers/generic/file/FsProvider';
+import ManagerInformation from '../../_managerinf/ManagerInformation';
+import LinkProvider from '../../providers/components/LinkProvider';
+import FileUtils from '../../utils/FileUtils';
+import Game from '../../model/game/Game';
+import { StorePlatform } from '../../model/game/StorePlatform';
+
+
+export class SteamInstallationValidator {
+
+ public static async validateInstallation(game: Game): Promise {
+ if (![StorePlatform.STEAM, StorePlatform.STEAM_DIRECT].includes(game.activePlatform.storePlatform)) {
+ return new R2Error(
+ "This feature is not available on non-Steam platforms.",
+ "The feature deletes the contents of the game folder and verifies files. You can do the same manually."
+ );
+ }
+
+ const gameFolder = await GameDirectoryResolverProvider.instance.getDirectory(game);
+ if (gameFolder instanceof R2Error) {
+ return gameFolder;
+ }
+
+ // Sanity check we're about to delete something resembling a valid game folder.
+ let exeFound = false;
+ for (let exeName of game.exeName) {
+ if (await FsProvider.instance.exists(path.join(gameFolder, exeName))) {
+ exeFound = true;
+ break;
+ }
+ }
+
+ if (!exeFound) {
+ const message = game.exeName.length > 1 ?
+ `Could not find any of "${game.exeName.join('", "')}"` :
+ `Could not find "${game.exeName[0]}"`;
+
+ return new R2Error(
+ `${game.displayName} folder is invalid`,
+ message,
+ `Set the ${game.displayName} folder in the settings section`
+ );
+ }
+
+ try {
+ await FileUtils.emptyDirectory(gameFolder);
+ } catch(e) {
+ return R2Error.fromThrownValue(
+ e,
+ 'Failed to empty the game folder',
+ `Try launching ${ManagerInformation.APP_NAME} as an administrator`
+ );
+ }
+
+ try {
+ LinkProvider.instance.openLink(`steam://validate/${game.activePlatform.storeIdentifier}`);
+ } catch(e) {
+ return R2Error.fromThrownValue(e, 'Failed to start steam://validate');
+ }
+ }
+}
diff --git a/src/r2mm/mods/ProfileModList.ts b/src/r2mm/mods/ProfileModList.ts
index 9e9a6ee51..517797017 100644
--- a/src/r2mm/mods/ProfileModList.ts
+++ b/src/r2mm/mods/ProfileModList.ts
@@ -191,6 +191,11 @@ export default class ProfileModList {
}, "plugins");
}, "BepInEx");
tree.removeDirectories("MelonLoader");
+ tree.navigateAndPerform(gdWeaveDir => {
+ gdWeaveDir.removeDirectories("core");
+ gdWeaveDir.removeDirectories("mods");
+ gdWeaveDir.removeFiles("GDWeave.log");
+ }, "GDWeave");
// Add all tree contents to buffer.
for (const file of tree.getRecursiveFiles()) {
const fileLower = file.toLowerCase();
diff --git a/src/store/index.ts b/src/store/index.ts
index f3c69dd19..adb94f05e 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -11,6 +11,7 @@ import { FolderMigration } from '../migrations/FolderMigration';
import Game from '../model/game/Game';
import GameManager from '../model/game/GameManager';
import R2Error from '../model/errors/R2Error';
+import { getModLoaderPackageNames } from '../r2mm/installing/profile_installers/ModLoaderVariantRecord';
import ManagerSettings from '../r2mm/manager/ManagerSettings';
Vue.use(Vuex);
@@ -18,6 +19,7 @@ Vue.use(Vuex);
export interface State {
activeGame: Game;
isMigrationChecked: boolean;
+ modLoaderPackageNames: string[];
_settings: ManagerSettings | null;
}
@@ -32,6 +34,7 @@ export const store = {
state: {
activeGame: GameManager.defaultGame,
isMigrationChecked: false,
+ modLoaderPackageNames: [],
// Access through getters to ensure the settings are loaded.
_settings: null,
@@ -87,9 +90,19 @@ export const store = {
},
setSettings(state: State, settings: ManagerSettings) {
state._settings = settings;
+ },
+ updateModLoaderPackageNames(state: State) {
+ // The list is static and doesn't change during runtime.
+ if (!state.modLoaderPackageNames.length) {
+ state.modLoaderPackageNames = getModLoaderPackageNames();
+ }
}
},
getters: {
+ isModLoader: (state: State) => (packageName: string): boolean => {
+ return state.modLoaderPackageNames.includes(packageName);
+ },
+
settings(state: State): ManagerSettings {
if (state._settings === null) {
throw new R2Error(
diff --git a/src/utils/ProfileUtils.ts b/src/utils/ProfileUtils.ts
index e76d5e38a..7700227c1 100644
--- a/src/utils/ProfileUtils.ts
+++ b/src/utils/ProfileUtils.ts
@@ -125,6 +125,27 @@ export async function installModsToProfile(
export async function parseYamlToExportFormat(yamlContent: string) {
const parsedYaml = await yaml.parse(yamlContent);
+ if (!parsedYaml) {
+ throw new R2Error(
+ 'Failed to parse yaml contents.',
+ 'Yaml parsing failed when trying to import profile via file (The contents of export.r2x file are invalid).',
+ 'Ensure that the profile import file isn\'t corrupted.'
+ )
+ }
+ if (typeof parsedYaml.profileName !== 'string') {
+ throw new R2Error(
+ 'Failed to read profile name.',
+ 'Reading the profile name after parsing the yaml failed (export.r2x is missing the profileName field).',
+ 'Ensure that the profile import file isn\'t corrupted.'
+ )
+ }
+ if (!Array.isArray(parsedYaml.mods)) {
+ throw new R2Error(
+ 'Failed to read mod list.',
+ 'Reading mods list after parsing the yaml failed (Mod list of export.r2x is invalid).',
+ 'Ensure that the profile import file isn\'t corrupted.'
+ )
+ }
return new ExportFormat(
parsedYaml.profileName,
parsedYaml.mods.map((mod: any) => {
@@ -180,17 +201,21 @@ export async function populateImportedProfile(
}
}
-//TODO: Check if instead of returning null/empty strings, there's some errors that should be handled
-export async function readProfileFile(file: string) {
- let read = '';
+export async function readProfileFile(file: string): Promise {
+ let read: string | null | undefined;
if (file.endsWith('.r2x')) {
read = (await FsProvider.instance.readFile(file)).toString();
} else if (file.endsWith('.r2z')) {
- const result: Buffer | null = await ZipProvider.instance.readFile(file, "export.r2x");
- if (result === null) {
- return null;
- }
- read = result.toString();
+ await ZipProvider.instance.readFile(file, "export.r2x")
+ .then((value) => { read = value ? value.toString() : null; })
+ .catch(() => { read = null });
+ }
+
+ if (!read) {
+ throw new R2Error(
+ 'Error when reading file contents',
+ 'Reading the .r2x file contents failed. The contents might be empty or corrupted.',
+ );
}
return read;
}
diff --git a/yarn.lock b/yarn.lock
index 7594bdbac..8999f52b8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -13910,7 +13910,7 @@ vue-template-es2015-compiler@^1.6.0, vue-template-es2015-compiler@^1.9.0:
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
-vue@^2.7.1:
+vue@2.7.16, vue@^2.7.1:
version "2.7.16"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.7.16.tgz#98c60de9def99c0e3da8dae59b304ead43b967c9"
integrity sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==