diff --git a/README.md b/README.md
index 6fbed59..b1ec8ac 100644
--- a/README.md
+++ b/README.md
@@ -64,6 +64,7 @@ You can rename `modorganizer-basic_games-xxx` to whatever you want (e.g., `basic
| Mount & Blade II: Bannerlord — [GOG](https://www.gog.com/game/mount_blade_ii_bannerlord) / [STEAM](https://store.steampowered.com/app/261550/Mount__Blade_II_Bannerlord/) | [Holt59](https://github.com/holt59/) | [game_mountandblade2.py](games/game_mountandblade2.py) |
|
| Need for Speed: High Stakes | [uwx](https://github.com/uwx) | [game_nfshs.py](games/game_nfshs.py) | |
| No Man's Sky - [GOG](https://www.gog.com/game/no_mans_sky) / [Steam](https://store.steampowered.com/app/275850/No_Mans_Sky/)|[EzioTheDeadPoet](https://eziothedeadpoet.github.io/AboutMe/)|[game_nomanssky.py](games/game_nomanssky.py)| |
+| Resident Evil Requiem — [STEAM](https://store.steampowered.com/app/3764200/Resident_Evil_Requiem/) | jfcarroll | [game_residentevilrequiem.py](games/game_residentevilrequiem.py) | |
| Slay the Spire 2 — [STEAM](s.team/a/2868840) | [Azlle](https://github.com/Azlle) | [game_sts2.py](games/game_sts2.py) | |
| S.T.A.L.K.E.R. Anomaly — [MOD](https://www.stalker-anomaly.com/) | [Qudix](https://github.com/Qudix) | [game_stalkeranomaly.py](games/game_stalkeranomaly.py) | |
| Stardew Valley — [GOG](https://www.gog.com/game/stardew_valley) / [STEAM](https://store.steampowered.com/app/413150/Stardew_Valley/) | [Syer10](https://github.com/Syer10), [Holt59](https://github.com/holt59/) | [game_stardewvalley.py](games/game_stardewvalley.py) | |
diff --git a/games/game_residentevilrequiem.py b/games/game_residentevilrequiem.py
new file mode 100644
index 0000000..dbb7db5
--- /dev/null
+++ b/games/game_residentevilrequiem.py
@@ -0,0 +1,310 @@
+import re
+from pathlib import Path
+from typing import Iterable
+
+from PyQt6.QtCore import QFileInfo
+
+import mobase
+
+from ..basic_features import BasicModDataChecker, GlobPatterns
+from ..basic_features.utils import is_directory
+from ..basic_game import BasicGame
+
+
+class ResidentEvilRequiemModDataChecker(BasicModDataChecker):
+ """Mod data checker for Resident Evil Requiem (RE Engine).
+
+ Mods are loaded at runtime by REFramework, which supports:
+
+ - loose files under `natives/` (LooseFileLoader),
+ - `.pak` mods placed in `pak_mods/` (IntegrityCheckBypass_LoadPakDirectory),
+ - Lua scripts and native plugins under `reframework/`.
+
+ Most mods on Nexus are packaged for Fluffy Mod Manager: the payload is
+ wrapped in one or more folders (usually the mod name) next to Fluffy
+ metadata (modinfo.ini, screenshots, readmes). Single-option archives are
+ unwrapped automatically. Multi-option archives are ambiguous, so they fall
+ through to the manual installer - but re-rooting to one option there gives
+ a layout that is valid without further fixing, because the manual
+ installer never applies `fix()`: Fluffy metadata is merely ignored, and
+ `.pak` files are valid at the mod root because the whole mod is cleaned
+ up on disk right after installation - see
+ `ResidentEvilRequiemGame._fix_installed_mod()`, which covers manual and
+ BAIN installs as well.
+ """
+
+ def __init__(self):
+ super().__init__(
+ GlobPatterns(
+ valid=[
+ "natives",
+ "reframework",
+ "pak_mods",
+ # Pak storage of installed mods:
+ # ResidentEvilRequiemGame.mappings() maps its content
+ # into pak_mods/ with a load-order prefix.
+ "paks",
+ "dinput8.dll",
+ # Moved into paks/ after install, see
+ # ResidentEvilRequiemGame._fix_installed_mod().
+ "*.pak",
+ ],
+ # Fluffy Mod Manager metadata: inert, so keep it - this way
+ # manually (re-)rooted mods are valid without any fixing.
+ ignore=[
+ "modinfo.ini",
+ "screenshot.*",
+ "*.txt",
+ "*.md",
+ "*.jpg",
+ "*.jpeg",
+ "*.png",
+ "*.url",
+ ],
+ move={
+ # The archive was created from inside the natives folder.
+ "stm": "natives/",
+ },
+ )
+ )
+
+ def _wrapper_dir(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None:
+ """Find the single directory the mod payload is wrapped in.
+
+ Fluffy mod archives usually wrap the payload in a folder named after
+ the mod. Returns the only directory of `filetree` that is neither
+ valid, movable nor ignorable, provided everything next to it is junk
+ (ignorable). Returns None otherwise (e.g. for multi-option Fluffy
+ packs with several wrapper candidates, which need a manual install).
+ """
+ rp = self._regex_patterns
+ wrapper: mobase.IFileTree | None = None
+ for entry in filetree:
+ name = entry.name().casefold()
+ if rp.ignore.match(name) or rp.delete.match(name):
+ continue
+ if rp.valid.match(name) or rp.move_match(name) is not None:
+ # Meaningful content next to a potential wrapper: the base
+ # checker already knows how to handle this level.
+ return None
+ if not is_directory(entry) or wrapper is not None:
+ return None
+ wrapper = entry
+ return wrapper
+
+ def dataLooksValid(
+ self, filetree: mobase.IFileTree
+ ) -> mobase.ModDataChecker.CheckReturn:
+ status = super().dataLooksValid(filetree)
+ if status is mobase.ModDataChecker.INVALID:
+ wrapper = self._wrapper_dir(filetree)
+ if wrapper is not None and (
+ self.dataLooksValid(wrapper) is not mobase.ModDataChecker.INVALID
+ ):
+ status = mobase.ModDataChecker.FIXABLE
+ return status
+
+ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree:
+ # Unwrap (possibly nested) Fluffy mod folders first.
+ while (
+ super().dataLooksValid(filetree) is mobase.ModDataChecker.INVALID
+ and (wrapper := self._wrapper_dir(filetree)) is not None
+ ):
+ # Everything next to the wrapper is junk (see _wrapper_dir), and
+ # the wrapper level disappears, so drop the junk with it.
+ for entry in list(filetree):
+ if entry.name() != wrapper.name():
+ entry.detach()
+ filetree.merge(wrapper)
+ wrapper.detach()
+ return super().fix(filetree)
+
+
+class ResidentEvilRequiemGame(
+ BasicGame, mobase.IPluginFileMapper, mobase.IPluginDiagnose
+):
+ def __init__(self):
+ BasicGame.__init__(self)
+ mobase.IPluginFileMapper.__init__(self)
+ mobase.IPluginDiagnose.__init__(self)
+
+ def init(self, organizer: mobase.IOrganizer) -> bool:
+ super().init(organizer)
+ self._register_feature(ResidentEvilRequiemModDataChecker())
+ organizer.modList().onModInstalled(self._fix_installed_mod)
+ return True
+
+ Name = "Resident Evil Requiem Support Plugin"
+ Author = "HomerSimpleton Returns Again"
+ Version = "1.0"
+
+ GameName = "Resident Evil Requiem"
+ GameShortName = "residentevilrequiem"
+ GameNexusName = "residentevilrequiem"
+ GameSteamId = 3764200
+
+ GameBinary = "re9.exe"
+ GameDataPath = "%GAME_PATH%"
+
+ # Where installed mods store their paks. The folder overlays into the
+ # game directory as an inert "paks" dir the game never reads - only the
+ # numbered virtual copies created by mappings() are loaded. (Hiding the
+ # folder with a .mohidden suffix breaks the explicit file mappings with
+ # usvfs 0.5.6, so the inert leak is deliberate.)
+ _PAK_STORAGE_DIR = "paks"
+
+ _PAK_NUMBER_PREFIX = re.compile(r"^\d{4} - ", re.ASCII)
+
+ # Fluffy Mod Manager metadata: only read by Fluffy itself, and colliding
+ # between mods, so not worth keeping in the installed mod.
+ _FLUFFY_METADATA_GLOBS = ("modinfo.ini", "screenshot.*")
+
+ @staticmethod
+ def _parse_modinfo(path: Path) -> dict[str, str]:
+ """Parse a Fluffy Mod Manager modinfo.ini (plain key=value lines,
+ no sections). Returns an empty dict if the file cannot be read."""
+ info: dict[str, str] = {}
+ try:
+ text = path.read_text(encoding="utf-8-sig", errors="replace")
+ except OSError:
+ return info
+ for line in text.splitlines():
+ key, sep, value = line.partition("=")
+ if sep and key.strip() and value.strip():
+ info.setdefault(key.strip().casefold(), value.strip())
+ return info
+
+ def _fix_installed_mod(self, mod: mobase.IModInterface) -> None:
+ """Clean up a freshly installed mod on disk: salvage MO2 metadata
+ from the Fluffy modinfo.ini, delete the Fluffy metadata files and
+ collect .pak files (mod root and shipped pak_mods/) into the mod's
+ pak storage folder, from where mappings() feeds them to
+ REFramework. Files are never renamed after installation.
+
+ The ModDataChecker cannot do this: neither the manual installer nor
+ the BAIN installer apply `fix()`, and multi-option Fluffy packs have
+ to go through one of those.
+ """
+ if self._organizer.managedGame() != self:
+ return
+ mod_path = Path(mod.absolutePath())
+ # Fill MO2's metadata from modinfo.ini before it is deleted - but
+ # never override what MO2 already knows (e.g. from a Nexus download).
+ modinfo_path = mod_path / "modinfo.ini"
+ if modinfo_path.is_file():
+ modinfo = self._parse_modinfo(modinfo_path)
+ version = modinfo.get("version")
+ if version and not mod.version().isValid():
+ mod.setVersion(mobase.VersionInfo(version))
+ url = modinfo.get("homepage") or modinfo.get("website")
+ if url and not mod.url():
+ mod.setUrl(url)
+ for pattern in self._FLUFFY_METADATA_GLOBS:
+ for junk in mod_path.glob(pattern):
+ if junk.is_file():
+ junk.unlink()
+ # Collect the paks into the hidden storage folder.
+ shipped_pak_mods = mod_path / "pak_mods"
+ paks = [
+ p
+ for pak_dir in (mod_path, shipped_pak_mods)
+ for p in pak_dir.glob("*.pak")
+ if p.is_file()
+ ]
+ if paks:
+ storage = mod_path / self._PAK_STORAGE_DIR
+ storage.mkdir(exist_ok=True)
+ for pak in paks:
+ # Strip a leftover load-order prefix: load order is handled
+ # by mappings() now.
+ target = storage / self._PAK_NUMBER_PREFIX.sub("", pak.name)
+ if not target.exists():
+ pak.rename(target)
+ if shipped_pak_mods.is_dir() and not any(shipped_pak_mods.iterdir()):
+ shipped_pak_mods.rmdir()
+
+ # IPluginFileMapper: REFramework loads every *.pak found in pak_mods/ in
+ # alphabetical order. The stored paks of all active mods are mapped into
+ # pak_mods/ with a global sequence number prefix, so that the load order
+ # follows MO2's mod order - the same scheme Fluffy Mod Manager uses with
+ # its own number prefixes. The mapping is virtual and rebuilt on every
+ # launch; nothing on disk changes. Within one mod, paks are ordered by
+ # their case-insensitive file name, so renaming the files in the mod's
+ # pak storage folder reorders them.
+
+ def _active_mod_paths(self) -> Iterable[Path]:
+ mods_parent_path = Path(self._organizer.modsPath())
+ modlist = self._organizer.modList()
+ for mod_name in modlist.allModsByProfilePriority():
+ if modlist.state(mod_name) & mobase.ModState.ACTIVE:
+ yield mods_parent_path / mod_name
+
+ def mappings(self) -> list[mobase.Mapping]:
+ pak_mods_dir = Path(self.gameDirectory().absolutePath()) / "pak_mods"
+ # Without a directory link on pak_mods/, the virtual pak files below
+ # are not enumerable - directory scans (like REFramework's) would
+ # not see them (compare game_finalfantasy7remake.py). Anchor that
+ # link with a dedicated, permanently empty plugin-data folder. No
+ # createTarget: writes into the virtual pak_mods/ should still land
+ # in overwrite via MO2's regular root mapping.
+ anchor = (
+ Path(self._organizer.pluginDataPath()) / "residentevilrequiem" / "pak_mods"
+ )
+ anchor.mkdir(parents=True, exist_ok=True)
+ mappings: list[mobase.Mapping] = [
+ mobase.Mapping(str(anchor), str(pak_mods_dir), True, False)
+ ]
+ number = 0
+ for mod_path in self._active_mod_paths():
+ storage = mod_path / self._PAK_STORAGE_DIR
+ if not storage.is_dir():
+ continue
+ for pak in sorted(
+ storage.glob("*.pak"), key=lambda pak: pak.name.casefold()
+ ):
+ if not pak.is_file():
+ continue
+ number += 1
+ mappings.append(
+ mobase.Mapping(
+ str(pak),
+ str(pak_mods_dir / f"{number:04d} - {pak.name}"),
+ False,
+ )
+ )
+ return mappings
+
+ def _reframework_installed(self) -> bool:
+ return QFileInfo(self.gameDirectory(), "dinput8.dll").exists()
+
+ # IPluginDiagnose: mods only load through REFramework, so warn when it
+ # is missing from the game directory.
+
+ def activeProblems(self) -> list[int]:
+ if self._organizer.managedGame() == self and not self._reframework_installed():
+ return [self._PROBLEM_NO_REFRAMEWORK]
+ return []
+
+ _PROBLEM_NO_REFRAMEWORK = 0
+
+ def shortDescription(self, key: int) -> str:
+ return "REFramework is not installed in the game directory."
+
+ def fullDescription(self, key: int) -> str:
+ return (
+ "REFramework provides the mod loaders this plugin relies on: loose files "
+ "from natives/, .pak mods from pak_mods/ and scripts from reframework/. "
+ "Without it, installed mods have no effect.\n\n"
+ "Download REFramework from "
+ "https://www.nexusmods.com/residentevilrequiem/mods/13 and extract it "
+ "(dinput8.dll) directly into the game directory:\n"
+ f"{self.gameDirectory().absolutePath()}\n\n"
+ "Install it directly into the game directory rather than as a mod, so "
+ "the loader DLL is always active regardless of the virtual file system."
+ )
+
+ def hasGuidedFix(self, key: int) -> bool:
+ return False
+
+ def startGuidedFix(self, key: int) -> None:
+ pass