Mod Bisect Tool

2026 Update: The project has expanded beyond Fabric to support (Neo)Forge under the name Mod Bisect Tool. It now features a native cross-platform Graphical User Interface built with Gio UI, and the search algorithm has been extended to IMCS-I to handle indeterminate test results.

Website & Downloads | Source Code

Anyone who has run a large Minecraft modpack has been there. The game crashes, the log is unhelpful, and you have a hundred mods to blame. The usual approach is to manually disable half your mods, try again, and repeat until you find the culprit. It works, but it is tedious and easy to get wrong. I wrote this tool to automate that process properly.

The tool supports Fabric, Quilt, and (Neo)Forge (including Sinytra Connector and Kilt configurations). Prebuilt binaries with automatic OS detection and comprehensive documentation are hosted on the project website, and the source code is on GitHub. Step-by-step guides for both versions are available online: GUI User Guide and TUI User Guide.

How It Works

The core of the tool is a bisection search. It splits the pool of candidate mods in half, asks you to run the game and report whether the issue occurred, then eliminates whichever half is clean. Each round cuts the search space in half, so even a modpack with hundreds of mods converges to a result in a handful of steps. The user interface walks you through each test one at a time, so there is no guesswork involved.

One thing that makes this more than a simple binary search is dependency handling. Mods often require other mods to be present, and naively enabling a subset of mods can produce crashes that are unrelated to the actual conflict. The tool includes a dependency resolver that automatically activates any required dependencies when a mod is selected for testing, keeping each test valid. It also reads bundled JARs so that if a conflict is caused by a library shipped inside another mod, the tool still points to the right culprit.

Some conflicts only surface when two or more mods are active together. The bisection engine handles this by continuing to narrow down the candidate set until it isolates the exact combination responsible. After a conflict is found, the tool can set those mods aside and resume searching the rest of the pool, finding any number of separate, unrelated conflicts in one session.

The algorithm described in the appendix (IMCS-I) is my own approach, designed specifically for this use case. It is optimized for situations where only a small number of conflicts exist within a large set of components, focusing on minimizing the number of required test runs by isolating one conflicting element at a time and handling secondary failures that produce indeterminate test outcomes. In contrast, delta debugging (ddmin) and QuickXplain (QXP) have a lot of overhead in such scenarios, but perform better when the number of conflicts is large.

Managing the Search

The tool gives you fine-grained control over which mods participate in the search. A mod can be force-enabled if it must always be present for the issue to reproduce, force-disabled if you already know it is safe to exclude, or omitted to remove it from the candidate pool while still allowing the resolver to activate it as a dependency for other mods. A history page logs every test and its outcome so you can review how the tool arrived at its conclusion, and a live log page exposes internal diagnostics for bug reports.

The tool also supports a dependency override file for cases where a mod's metadata is missing or incorrect. Overrides can be placed next to the executable, in the Minecraft config folder, or left to the tool's own built-in list, with a clear priority order between them.

Setup screen where the mods folder is loaded
The setup screen, where the mods folder is loaded and analyzed before the search begins.
Main screen showing the candidate mod list
The main screen, showing the current candidates and controls for starting a test.
Test in progress screen
The test screen, shown while a specific subset of mods is enabled and the game is running.
Result screen showing the conflicting mods
The result screen, listing the conflicting mods and offering the option to continue searching for further conflicts.

Graphical User Interface (GUI)

While the terminal user interface (TUI) provides power-user tools like keyboard shortcuts, a full mod management table, a test history page, and an internal log viewer, these features add unnecessary friction for everyday troubleshooting. The GUI is built to streamline the entire experience around what actually matters: isolating the broken mod with as few clicks and decisions as possible.

The GUI is built using Gio UI (gioui / gogio), an immediate-mode GUI library written in pure Go. Using Gio keeps the binary lightweight and dependency-free while rendering natively across operating systems:

  • Windows: Standalone executable (.exe) with native file dialogs.
  • macOS: Standalone application bundle (.app).
  • Linux: Portable, single-file AppImage.

Guided Pre-Search Wizard

Instead of burying candidate configuration inside nested menus, the GUI introduces a guided wizard right after folder setup:

  • Handling Already-Disabled Mods: Modpacks often contain mods that were disabled intentionally or temporarily. The GUI identifies these upfront and lets you decide whether they should remain permanently disabled throughout the entire bisect or participate in the candidate pool.
  • Excluding (Omitting) Safe or Disruptive Mods: You can exclude mods you already know are safe, or mods that actively get in the way of testing. A prime example is Crash Assistant: it displays an interactive GUI dialog whenever the game crashes, which slows down the search since frequent game crashes are an expected part of the bisection process. Omitted mods are removed from the candidate pool, but the dependency resolver can still activate them if another mod requires them.
  • Selecting Mods to Keep Enabled: Some issues only reproduce when a specific prerequisite mod is active. For example, if a visual glitch only occurs with shaders enabled, the shader mod (such as Iris) must remain forced-enabled. If it were disabled during testing, you would not be able to tell whether the issue was resolved or simply impossible to observe.
GUI setup screen where the mods folder is loaded and mod loader is detected
The GUI setup screen, with automatic mod loader detection and folder selection.
GUI main screen showing candidate progress
The GUI main screen, showing candidate progress and remaining candidate counts.
GUI test prompt screen with Works, Broken, and Can't Tell options
The GUI test screen, with options for Works, Broken, and Can't Tell (indeterminate).
GUI result screen showing isolated conflict sets
The GUI result screen, displaying the isolated conflict set and next action options.

Appendix

Algorithm: Iterative Minimal Conflict Search with Indeterminate Results (IMCS-I)

The Iterative Minimal Conflict Search (IMCS) algorithm is a novel (citation needed hahaha), highly efficient method for identifying a 1-minimal conflict set from a larger collection of components. Building upon the core principles of binary search and iterative component isolation, IMCS maintains stable and predictable O(p log n) performance. While sharing the same optimal theoretical complexity as QuickXplain (QXP), IMCS distinguishes itself by adopting a "lean start" strategy, precisely targeting individual conflict elements and avoiding QXP's upfront speculative tests. This results in superior practical performance and significantly lower variance for sparse problems, making IMCS ideally suited for real-world troubleshooting scenarios.

In practice, test outcomes are not always binary (GOOD vs. FAIL). A secondary issue, such as an undeclared dependency crashing the game before the target bug can be observed, leads to an INDETERMINATE result. IMCS-I extends IMCS to resolve these secondary conflicts automatically during bisection.

1. Objective

To efficiently identify a 1-minimal conflict set of size p from a larger superset of n components (C_all). A conflict set is defined as the smallest subset of components that causes a system failure (or a designated undesirable outcome) when tested together.

2. Core Principle & Indeterminate Handling

The IMCS algorithm operates on a "lean start, iterative isolation" principle. It fundamentally avoids the high overhead of speculative testing on large component sets. Instead, it executes a series of independent binary searches, each tasked with identifying exactly one new conflict element that contributes to the system's failure.

An INDETERMINATE result on StableSet ∪ C₁ indicates an undeclared dependency: a component in C₁ silently requires a component in C₂, and the split separated them. Because IMCS guarantees that test(StableSet ∪ CandidateSet) = FAIL on every recursive call, an INDETERMINATE outcome on StableSet ∪ C₁ guarantees that test((StableSet ∪ C₂) ∪ C₁) = FAIL for free without requiring an extra test run. When testing C₂ confirms it is clean (GOOD), folding C₂ into StableSet suppresses the secondary conflict across the entire recursive descent into C₁.

3. Algorithm Description

The algorithm consists of a main procedure, FindConflictSet, and a recursive helper, FindNextConflictElement.

Definitions:
C_all
The initial superset of all n components.
ConflictSet
The set of components confirmed to be part of the minimal conflict set.
CandidateSet
The set of components currently being considered for inclusion in the ConflictSet.
StableSet
A set of components that has been tested together in the current search context and found to be stable (i.e., does not cause a failure). This set serves as the baseline for subsequent tests.
test(S)
A black-box function that returns FAIL if the system exhibits the target failure when configured with set S, GOOD if clean, and INDETERMINATE if a secondary issue masks the observation.
Implicit Definitions:
ClearedSet
An implicit subset of StableSet (specifically, StableSet \ ConflictSet) comprising components that have been tested and found not to contribute to the current system failure in their respective search contexts.
Main Procedure: FindConflictSet
function FindConflictSet(C_all):
  ConflictSet  {}
  CandidateSet  C_all
  loop indefinitely:
    // Find the next single component that, in conjunction with the current ConflictSet, contributes to the failure.
    nextElement  FindNextConflictElement(StableSet=ConflictSet, CandidateSet=CandidateSet)

    // If no additional conflict element can be found, the process is complete.
    if nextElement is null:
      break

    // Add the found element to the confirmed ConflictSet and remove it from the candidate pool.
    ConflictSet  ConflictSet  {nextElement}
    CandidateSet  CandidateSet \ {nextElement}

    // Optimization: Test if the current ConflictSet is already a complete, minimal set.
    // If it causes failure, we can terminate early without searching for more components.
    if test(ConflictSet) is FAIL: 
      break

  return ConflictSet
Helper Procedure: FindNextConflictElement
function FindNextConflictElement(StableSet, CandidateSet):
  // Base Case 1: No more candidates to test.
  if CandidateSet is empty:
    return null

  // Base Case 2: Handles CandidateSet of size 1.
  if size(CandidateSet) = 1:
    let c be the single element in CandidateSet
    if test(StableSet  {c}) is FAIL:
      return c
    else:
      // GOOD: not a conflict element.
      // INDETERMINATE: c itself causes a secondary conflict; treat as non-element for this search.
      return null

  // Recursive Step: Divide and conquer.
  Split CandidateSet into two halves, C₁ and C₂.
  result₁  test(StableSet  C₁)

  if result₁ is FAIL:
    if size(C₁) = 1:
      return the single element in C₁
    return FindNextConflictElement(StableSet, C₁)

  if result₁ is GOOD:
    new_stable  StableSet  C₁
    if size(C₂) = 1:
      if test(new_stable  C₂) is FAIL:
        return the single element in C₂
      else:
        return null
    return FindNextConflictElement(new_stable, C₂)

  // --- INDETERMINATE: C₁ has a split-induced secondary conflict ---
  if result₁ is INDETERMINATE:
    result₂  test(StableSet  C₂)

    // Primary conflict element is in C₂. Proceed normally.
    if result₂ is FAIL:
      if size(C₂) = 1:
        return the single element in C₂
      return FindNextConflictElement(StableSet, C₂)

    // C₂ is confirmed clean. Fold it into StableSet and recurse into C₁ (no extra test needed).
    if result₂ is GOOD:
      return FindNextConflictElement(StableSet  C₂, C₁)

    // Both halves are INDETERMINATE (independent secondary conflicts on both sides).
    // Search both branches, each suppressing the other's secondary conflict.
    if result₂ is INDETERMINATE:
      // A practical alternative in this case is to just halt
      found  FindNextConflictElement(StableSet  C₂, C₁)
      if found is not null:
        return found
      return FindNextConflictElement(StableSet  C₁, C₂)

4. Handling INDETERMINATE Outcomes

Single-INDETERMINATE: Resolving with a Single Test

When a test on C₁ returns INDETERMINATE, an undeclared dependency inside C₁ was separated from its required mod in C₂. A naive troubleshooting approach might spend up to O(log n) additional tests trying to hunt down and restore the missing dependency.

IMCS-I avoids this entirely. Because the algorithm already guarantees that test(StableSet ∪ CandidateSet) = FAIL on entry, we only need a single extra test on C₂ (test(StableSet ∪ C₂)) to know exactly how to proceed:

  • If C₂ returns FAIL: The primary conflict element resides in C₂. Recursion proceeds into C₂ as normal.
  • If C₂ returns GOOD: C₂ is confirmed clean, meaning (StableSet ∪ C₂) ∪ C₁ is guaranteed to fail. We fold C₂ into StableSet to satisfy C₁'s missing dependency for the rest of the descent—requiring zero further tests.
Double-INDETERMINATE: Preventing Complexity Explosion

If C₂ also returns INDETERMINATE, two independent undeclared dependencies cross the split in opposite directions. Neither half has a clean baseline to lean on.

If the algorithm attempted to fork and search both branches, the recurrence relation would shift from T(n) = T(n/2) + O(1) to T(n) = 2·T(n/2) + O(1), causing the worst-case time complexity to explode from O(log n) to O(n). Because this situation is extremely rare, the tool's implementation avoids exponential branch explosion by simply halting the search, reporting the two conflicting groups, and letting the user resolve the missing dependency before resuming.

5. Complexity Analysis

Time Complexity: O((p + q) log n)

The algorithm's total cost is dominated by the p calls to the FindNextConflictElement procedure. Each call performs a binary search on a diminishing set of candidates (from n down to n - p + 1), with a baseline cost of O(log n) tests. When secondary conflicts occur, each isolated single-INDETERMINATE event costs exactly one extra complement test before clean logarithmic recursion resumes. Across p conflict elements and q single-indeterminate occurrences, the total test complexity is tightly bounded at O((p + q) log n).

6. Extension: Finding All Independent Conflicts (IMCS-Enumerator)

The core IMCS algorithm finds a single conflict set. The IMCS_Enumerator is a meta-procedure that extends this to discover all independent minimal conflict sets in a system that may have multiple unrelated faults.

A persistent, cross-iteration test cache (KnowledgeBase) is not used. Such a cache is unworkable in practice for two fundamental reasons:

  • A FAIL result is only relevant to its specific set of components; once a conflict element from that set is found and removed, that exact test can never be run again, rendering the cached result useless.
  • A GOOD result is context-dependent on the user's current focus; caching it could incorrectly mask a different, independent issue in a subsequent search.

Therefore, the only knowledge that can be safely persisted between iterations is the reduction of the candidate pool itself (CandidateSet \ newConflictSet).

Meta-Procedure: IMCS_Enumerator
function IMCS_Enumerator(C_all):
  AllConflictSets  []
  CandidateSet  C_all

  loop indefinitely:
    // Find the next conflict set using a fresh IMCS run. This ensures that
    // knowledge from previous runs does not incorrectly influence the current search.
    newConflictSet  FindConflictSet(CandidateSet)

    // If IMCS returns an empty set, no more conflicts exist among the candidates.
    if newConflictSet is empty:
      break

    // A new independent conflict has been found.
    add newConflictSet to AllConflictSets
    
    // The only safe and persistent knowledge transfer is shrinking the problem space
    // by removing the components of the just-found conflict.
    CandidateSet  CandidateSet \ newConflictSet

  return AllConflictSets

7. Capabilities and Limitations

The IMCS algorithm suite is highly optimized for a specific class of diagnostic problems.

Capabilities & Strengths
  1. Optimized for Sparse Conflicts: Exceptional O(p log n) performance and low variance when finding a small number (p) of conflict elements within a large set (n). This makes it ideal for real-world troubleshooting.
  2. Robust Indeterminate Handling: IMCS-I automatically resolves secondary issues caused by split-induced missing dependencies without discarding progress.
  3. Black-Box Operation: IMCS requires no internal knowledge of the system being tested. It operates purely on the GOOD/FAIL outcome of tests, making it universally applicable.
  4. Low Overhead & High Stability: The "lean start" strategy avoids wasteful speculative tests, and the iterative nature results in extremely stable, predictable performance with minimal variance, as confirmed by benchmarks.
  5. Complete Conflict Enumeration: The IMCS_Enumerator meta-procedure enumerates all separate, unrelated conflict sets efficiently.
Limitations
  1. Finding Non-Minimal Supersets: Designed to find only the smallest set causing a failure (1-minimal); does not report larger sets containing non-essential components.
  2. Conflict Prioritization: The algorithm finds conflict sets in an order determined by the binary search path, not by any measure of severity or probability.
  3. Dense Conflicts: Less efficient for dense problems where p is a large fraction of n, where algorithms like QuickXplain that leverage information reuse may perform better.
  4. Non-Deterministic Systems: Assumes deterministic outcomes. If identical component configurations produce contradictory results, IMCS may terminate with an incorrect conflict set.
  5. Side Effects & External State: The algorithm relies on mod toggling being a pure, reversible operation. If enabling or disabling a mod causes persistent side effects in game settings, the search can fail. This is quite rare in practice.

Comments

Loading...

No comments yet. Be the first!