Skip to content

Comments

Implement r2modmanPlus-style BepInEx install rules with marker-based cache validation#11

Draft
Claude wants to merge 4 commits intofeat/new-download-extraction-handlingfrom
claude/fix-bepinex-install-rules
Draft

Implement r2modmanPlus-style BepInEx install rules with marker-based cache validation#11
Claude wants to merge 4 commits intofeat/new-download-extraction-handlingfrom
claude/fix-bepinex-install-rules

Conversation

@Claude
Copy link

@Claude Claude AI commented Feb 7, 2026

Fixes "sometimes missing files" issue caused by stale extraction cache and adds support for all BepInEx route types (patchers, core, monomod) with proper nested folder handling.

Changes

Cache correctness (downloader.ts)

  • Marker file _extracted.ok gates cache hits—both extractPath and marker must exist
  • Cleans stale/partial extractions before re-extracting
  • Removes marker on any extraction failure
  • Logs cache hit/miss reasons (extractPath exists, marker exists, ignoreCache)

Nested folder support (mod-installer.ts)

  • resolveEffectiveRoot() handles SomeFolder/BepInEx/... zip layouts common on Thunderstore
  • Checks top level first, then immediate children (deterministic sort)
  • Logs resolution decision and chosen path

BepInEx routes (mod-installer.ts)

  • Installs all routes when present: plugins/, patchers/, core/, monomod/ (namespaced), config/ (merged)
  • Idempotent: removes destination folders before copying
  • Fallback preserved for non-BepInEx mods
  • Logs which routes existed and were installed with file counts

Uninstall (mod-installer.ts)

  • Removes BepInEx/{plugins,patchers,core,monomod}/<modId>
  • Config files untouched (shared between mods)
  • Logs removed paths and counts

Example

Before: A mod with BepInEx/patchers/ would only install plugins/, silently dropping patchers. If extraction was interrupted, partial cache would be reused.

After: All routes install correctly. Interrupted extractions trigger clean re-extraction.

// resolveEffectiveRoot handles this layout:
// ModName/
//   SomeFolder/
//     BepInEx/
//       plugins/
//       patchers/  ← now installed
//       core/      ← now installed

const effectiveRoot = await resolveEffectiveRoot(extractedModPath)
// Returns: /cache/ModName/SomeFolder (not /cache/ModName)
Original prompt

Goal

  • Make “generic BepInEx rules for all games” behave like r2modmanPlus for the common BepInEx layout, specifically fixing missed installs of BepInEx/core and BepInEx/patchers and eliminating “sometimes” caused by stale/partial extraction cache.
    Non-goals (for this first pass)
  • Implement the full r2modmanPlus rule engine (extension-based routing + best-fit rule matching across arbitrary folder layouts).
  • Track installed files in _state/*.yml like r2modmanPlus.
  • Handle game-specific extra routes (Assets, Sideloader, etc.). This is “generic BepInEx only”.

Specifications
A) Extraction cache correctness (marker-based cache hit)
Applies to: electron/downloads/downloader.ts
Marker file

  • Name: _extracted.ok
  • Location: /_extracted.ok
  • Contents: arbitrary (e.g. ok\n)
  • Semantics: “This extraction completed successfully.”
    Cache hit rule (new)
  • A cache hit is ONLY when:
    1. extractPath exists AND
    2. /_extracted.ok exists
      If either is missing, treat cache as invalid and re-extract.
      Re-extract rule (new)
  • Before extracting (when not a valid cache hit):
    • If extractPath exists, delete it recursively.
    • Recreate it.
    • Extract zip into it.
    • After extraction succeeds, write _extracted.ok.
      Failure handling
  • _extracted.ok must never exist for a failed or cancelled extraction.
  • If extraction fails, leave no marker; optionally delete extractPath to avoid partial junk (recommended).
    Rationale
  • Today you treat “folder exists” as valid cache; that’s vulnerable to partial extraction from a crash/cancel and is a primary source of “sometimes missing files”.

B) Effective root resolution (nested top-level folder zips)
Applies to: electron/profiles/mod-installer.ts
Definition

  • effectiveRoot is the directory that should be treated as the “package root” for installation.
    Algorithm
  1. If /BepInEx exists => effectiveRoot = extractedModPath
  2. Else list immediate children of extractedModPath:
    • Consider only directories
    • Sort deterministically (e.g. name.localeCompare)
    • If any child has /BepInEx => effectiveRoot = child (first match)
  3. Else effectiveRoot = extractedModPath
    Case handling
  • On Windows, treat BepInEx folder existence case-insensitively (practically pathExists(join(root, "BepInEx")) is enough if the FS is case-insensitive; no extra work unless you want to be explicit).
    Rationale
  • A common Thunderstore zip layout is SomeFolder/BepInEx/.... Without this, your installer falls back and misplaces everything under plugins.

C) Generic BepInEx route handling (r2modmanPlus-style routes)
Applies to: electron/profiles/mod-installer.ts
When effectiveRoot/BepInEx exists
Install these routes if present in the extracted content:

  1. Plugins (namespaced / “subdir”)
  • Source: /BepInEx/plugins
  • Destination: /BepInEx/plugins//...
  • Behavior: copy entire source folder contents into folder (preserve structure under plugins/)
  1. Patchers (namespaced / “subdir”)
  • Source: /BepInEx/patchers
  • Destination: /BepInEx/patchers//...
  1. Core (namespaced / “subdir”) — per your decision
  • Source: /BepInEx/core
  • Destination: /BepInEx/core//...
  1. Monomod (namespaced / “subdir”)
  • Source: /BepInEx/monomod
  • Destination: /BepInEx/monomod//...
  1. Config (merged / “none”)
  • Source: /BepInEx/config
  • Destination: /BepInEx/config/...
  • Behavior: merge copy (overwrite same-name files), not namespaced.
    Idempotency / reinstall semantics
  • Before copying any namespaced routes, delete the destination folders (if they exist):
    • BepInEx/plugins/
    • BepInEx/patchers/
    • BepInEx/core/
    • BepInEx/monomod/
  • Do NOT delete or “clean” BepInEx/config during mod reinstall/uninstall.
    When effectiveRoot/BepInEx does NOT exist
  • Fallback behavior (keep close to current):
    • Copy everything from effectiveRoot into /BepInEx/plugins//...
      Counting / return values
  • Today filesCopied is inconsistent (config increments per Dirent, not per file recursively).
  • Spec option:
    • Either keep as “best-effort” count, or change copyDirectory to return the number of files it copied (recommended so the UI reflects reality).

D) Uninstall logic must match install destinations
Applies to: electron/profiles/mod-installer.ts
Uninstall removes (new)

  • /BepInEx/plugins/
  • /BepInEx/patchers/
  • /BepInEx/core/
  • /BepInEx/monomod/
    Uninstall does NOT remove
  • Anything under /BepInEx/config (shared/untracked like r2modmanPlus trackingMethod: none)

Implementation Plan (step-by-step)

  1. Update downloader cache semantics
    Files: electron/downloads/downloader.ts
  • Add helper:
    • const markerPath = join(extractPath, "_extracted.ok")
  • Replace cache hit check:
    • from: if (!ignoreCache &...

This commit implements the complete specification for fixing BepInEx mod
installation to behave like r2modmanPlus, addressing the "sometimes missing
files" issue and adding support for all BepInEx routes.

Changes made:

A) Extraction cache correctness (downloader.ts)
   - Added marker-based cache hit validation using _extracted.ok file
   - Cache is now valid ONLY when both extractPath and marker exist
   - Cleans up stale/partial extractions before re-extracting
   - Writes marker after successful extraction
   - Removes marker on extraction failure
   - Added detailed logging for cache hit/miss scenarios

B) Effective root resolution (mod-installer.ts)
   - Added resolveEffectiveRoot() function to handle nested folder zips
   - Checks for BepInEx at top level first
   - Falls back to searching immediate children for BepInEx folders
   - Supports common Thunderstore layout: SomeFolder/BepInEx/...
   - Uses deterministic sorting for consistent behavior

C) Generic BepInEx route handling (mod-installer.ts)
   - Now installs all BepInEx routes when present:
     * plugins/ (namespaced under modId)
     * patchers/ (namespaced under modId)
     * core/ (namespaced under modId)
     * monomod/ (namespaced under modId)
     * config/ (merged, not namespaced)
   - Added idempotency: removes destination folders before copying
   - Config files are merged into shared config directory
   - Fallback behavior maintained for mods without BepInEx structure
   - Comprehensive logging of which routes were found and installed

D) Uninstall logic updated (mod-installer.ts)
   - Now removes all namespaced route folders:
     * BepInEx/plugins/<modId>
     * BepInEx/patchers/<modId>
     * BepInEx/core/<modId>
     * BepInEx/monomod/<modId>
   - Config files remain untouched (shared between mods)
   - Logs removed paths and file counts

E) Deterministic logging throughout
   - effectiveRoot resolution decision and reason
   - Cache hit/miss reasons (extractPath exists, marker exists, ignoreCache)
   - Which source routes existed in the mod
   - Which destination routes were written
   - File counts for each operation
   - Uninstall operations with paths and counts

This fixes the primary issue of "sometimes missing files" caused by partial
extraction cache and ensures all BepInEx route types are installed correctly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@Claude Claude AI changed the title [WIP] Fix BepInEx rules for reliable installation Implement r2modmanPlus-style BepInEx install rules with marker-based cache validation Feb 7, 2026
@Claude Claude AI requested a review from danielchim February 7, 2026 08:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants