One BUG Among Us
Disclaimer: Because this MOD has a very high priority, please do not apply it to your important or active servers or saves. OBA contributors will not take any responsibility for any issues caused! Using it means you agree to and accept the potential instability risks.

Crash diagnostics for Minecraft 1.20.1 through 1.21.x, on Forge and NeoForge alike.
His mission is to solve problems, and once the problems are solved, his task is complete, so there's no need to keep the mods folder anymore.
The game dies, the log is a few thousand lines of stack trace, and none of it says who did it. OBA boils that down to one sentence you can act on: which jar, which class, which line, and why that one is suspected.
Diagnosis only. This project never modifies a single file. It does not disable, move, rename or delete any mod. Everything it concludes is printed and written to a report; acting on it is your decision.
This is the overview README. It covers how the project is laid out, how to use it and how to build it. The source, the build config and the README for any single Minecraft version live on their own branch, named after the version directory — see Branches.
Contents
- What it solves
- What it does
- Branches
- Compatibility
- Quick start
- The interface
- Incident timeline
- Diagnostic reports
- Using it
- How it works
- How attribution works
- MCreator detection
- Graphics mods
- Sinytra Connector support
- Project structure
- Known limits
- Contributing
What it solves
When a mod breaks the game, the log usually offers a wall of stack trace and no answer. OBA turns that into one actionable sentence: which file, which line, and why.
| What you are looking at | What OBA answers |
|---|---|
| A crash and nothing but stack frames | The mod file, the class/method/line, and the full evidence weighting |
NoClassDefFoundError in the log |
Which class is missing, which mod should provide it, whether a dependency is absent |
| A freeze with no crash report | Captures a thread dump and names the mod that was executing |
| A few hundred mods and no idea where to start | Ranking by confidence, each suspect with its own reasons |
Mixin apply failed |
The mixin class and line, plus a conflict or version hint |
| The same mod installed twice | Every duplicate modId and all files declaring it |
| Wanting to spot MCreator mods | Multi-signal scoring, with every reason listed |
| A Fabric mod crashing under Connector | Still attributed to that Fabric mod's jar |
| An F3+C debug crash | Reported as deliberate, with no culprit to chase |
What it does
The short version: it turns "the game crashed and I have no idea whose fault it is" into "it is this jar, on this line".
When a crash happens you get the culprit mod file, the class, the method, the source file and line number, and the full evidence chain underneath — which log line, which rule matched, how many points it scored. If the verdict looks wrong you can walk that chain back and find the step that went astray. What each mod was doing in the minutes around the failure is laid out as a timeline, and for a freeze with no crash report at all it takes a thread dump and names the mod that was executing when things stopped.
Two copies of the same mod, a missing dependency, a Mixin conflict, a mod built by MCreator, a Fabric mod that died under Connector on NeoForge — each of those has its own evidence rather than a guess. For graphics mods it also knows the dozens of renamed Sodium and Iris forks, and catches both "two rival branches of the same family installed" and "shader loader with no renderer to drive".
There are only two ways to run it, and it is the same jar: drop it in mods/ and it is a mod, run java -jar and it is a command line tool. Reports, CLI output and in-game messages all switch between English and Chinese. It is read-only throughout — nothing written, changed or deleted, and a report is only ever a new text file under logs.
Branches
This repository is not "one source tree plus some build config". It is one engine plus several version tracks.
- The default branch — the one you are reading — holds the overview, the authoritative copy of the shared engine in
engine/, the loader adapters insrc/loader/, the build and verification scripts intools/, and the deliberately broken samples inexamples/. From here all eight jars can be built in one go. - Every Minecraft version range gets its own branch, carrying that version's
src/main,build.gradleand README. Switch to it and you can build, run and edit just that version.
| Branch | Minecraft | Loader |
|---|---|---|
26.2-forge |
26.2 | Forge 65 |
26.2-neoforge |
26.2 | NeoForge 26.2 |
1.21.1-neoforge |
1.21 – 1.21.x | NeoForge 21.1+ |
1.21.1-forge |
1.21.1 | Forge 52 |
1.20.5-1.20.6-neoforge |
1.20.5 / 1.20.6 | NeoForge 20.5 / 20.6 |
1.20.5-1.20.6-forge |
1.20.5 / 1.20.6 | Forge 50 |
1.20.2-1.20.4-neoforge |
1.20.2 / 1.20.3 / 1.20.4 | NeoForge 20.2 / 20.3 / 20.4 |
1.20.2-1.20.4-forge |
1.20.2 / 1.20.3 / 1.20.4 | Forge 49 |
1.20.1-forge |
1.20.1 | Forge 47 |
To read the source for a particular version, switch to its branch:
# only care about one version: name the branch while cloning
git clone -b 1.20.1-forge https://github.com/snowfish2871/One-BUG-Among-Us.git
# already cloned
git switch 1.21.1-neoforge
If what you want to change is the diagnostics themselves — the rules, the scoring, the report layout — edit engine/ on the default branch and re-run tools/sync-versions.ps1, which copies it into the four version directories. Those copies under versions/*/src/main/java/dev/oba/ are generated; editing them by hand only lasts until the next sync. For loader-specific work (@Mod entry points, event registration, tick adapters) the files are src/loader/neoforge/ and src/loader/forge/.
Compatibility
Releases are organised into version tracks: one track covers every Minecraft minor that shares the same API, and within a track the mod code needs no changes at all. The split exists because NeoForge changed APIs at specific points, and cramming everything into one jar would make the versions hold each other back.
| Track | Minecraft | Loader | Java | Built against | Status |
|---|---|---|---|---|---|
| 26.2 | 26.2 | Forge 65 line | 25 | 26.2 / 26.2-65.1.5 |
built, verified on client and server |
| 26.2 | 26.2 | NeoForge 26.2 line | 25 | 26.2 / 26.2.0.93 |
built, verified on client and server |
| 1.21.x | 1.21 – 1.21.x | 21.1 or newer | 21 | 1.21.1 / 21.1.256 |
verified in game on 1.1.0 |
| 1.21.1 | 1.21.1 | the Forge 52 line | 21 | 1.21.1 / 52.1.20 |
built, verified on client and server |
| 1.20.5 – 1.20.6 | 1.20.5 / 1.20.6 | the 20.5 / 20.6 lines | 21 | 1.20.5 / 20.5.21-beta |
verified in game on 1.1.0 |
| 1.20.5 – 1.20.6 | 1.20.6 | the Forge 50 line | 21 | 1.20.6 / 50.2.14 |
built, verified on client and server |
| 1.20.2 – 1.20.4 | 1.20.2 / 1.20.3 / 1.20.4 | the 20.2 / 20.3 / 20.4 lines | 17 | 1.20.2 / 20.2.93 |
verified in game on 1.1.0 |
| 1.20.2 – 1.20.4 | 1.20.2 / 1.20.3 / 1.20.4 | the Forge 49 line | 17 | 1.20.4 / 49.2.13 |
built, verified on client and server |
| 1.20.1 | 1.20.1 | the Forge 47 line | 17 | 1.20.1 / 47.4.26 |
verified in game on 1.1.0 |
1.20.1 runs on Forge, not NeoForge. NeoForge had not split off yet — the loader was still MinecraftForge with
net.minecraftforge.*packages, an entirely different codebase from today'snet.neoforged.*. That is why 1.20.1 gets its own Forge track. 1.20 is not supported at all: it predates both.
"Verified in game on 1.1.0" above means a real mod pack, not just OBA on its own: seven instances with 10 – 18 real mod jars each (picked from Modrinth by version, required dependencies pulled in recursively) all reached the title screen, with the index landing at 1802 – 4958 classes, 383 – 1150 packages and 68 – 186 ms. The loader's own jar and Minecraft itself are not mods and stay out of the index: counting them inflated the numbers to five digits and could point attribution at neoforge itself.
Two 1.20.3 quirks are worth writing down: a jar built for 1.20.4 makes the loader reject the whole mods folder (20.2 – 20.3 still require the
mandatoryfield in[[dependencies]]), and this machine's USB sound card only gets past audio init withALSOFT_DRIVERS=null. Neither changes the verdict, but both look like "no mods loaded".
The three Forge tracks for 1.20.2 and up all ship a jar in this release, and two of them have already run on a dedicated server. They were stuck for a long time, and the cause turned out to have nothing to do with mirrors: this machine's browser goes through the
127.0.0.1:7897system proxy while Java connects directly — direct access tomaven.minecraftforge.netruns at about 4 KB/s, while the same userdev file downloaded through the proxy in 2.7 s. After addingsystemProp.https.proxyHost/proxyPorttogradle.properties, the three tracks built in 2m38s, 108s and 73s.Two build-script bugs were fixed along the way: Forge dropped reobfuscation in 1.20.5, so the
reobfJartask no longer exists and the two later tracks now attachfinalizedByonly when the task is present; and 1.20.2 is the 48 line, soloaderVersionwas relaxed from[49,)to[48,), while 1.21.1's Minecraft range was narrowed from[1.21,1.22)to[1.21.1,1.21.2). Note that Forge never shipped a 1.20.5 build (its metadata has zero 1.20.5 versions), so1.20.5-1.20.6-forgereally covers 1.20.6 only.
The loader adapter layer
This section is about running the same diagnostic logic on both loaders.
The shared code references no loader classes at all; it only knows three interfaces in dev.oba.loader:
| Interface | Purpose |
|---|---|
LoaderEnv |
list mods, get the game directory, loader name |
TickHook |
hook the server tick (three different event classes across versions) |
Loader |
holds whichever implementation the current track installed |
Each track compiles one implementation on top:
| Directory | Used by |
|---|---|
src/loader/neoforge/ |
every NeoForge track from 1.20.2 up, including 1.21.x |
src/loader/forge/ |
the 1.20.1 Forge track |
A track's build.gradle adds only its own copy to the source set, so no jar ever contains both. The parts that cannot be abstracted — the @Mod entry point, event registration, the event classes themselves — live in each implementation, because the two loaders share no common superclass. All the actual work sits in the shared ObaLifecycle.
The 1.20.1 (Forge) track
That track builds with ForgeGradle on the official toolchain, and reobfuscation is part of the build, so it takes longer than the other three and needs JDK 17. The jar lands in dist/ like the rest.
One build snag worth writing down, in case another machine hits the same stall: the first ForgeGradle run downloads and deobfuscates all of 1.20.1, and
maven.neoforged.netis not reachable here. The other tracks get around that by rewriting repository URLs; ForgeGradle's ownMavenArtifactDownloadercannot be redirected the same way, so it sits inDownloadUtils.downloadFilewith accumulated CPU time flat and the cache no longer growing. Pointing it at the official CDN mirror fixed it.
Why 1.20.1 cannot simply reuse the 1.20.2 recipe: the NeoForge tracks build uniformly because from 1.20.2 on the loader packages are all net.neoforged.*. Forge on 1.20.1 runs SRG names in production (net.minecraft.commands.Commands.m_82127_) while the client jar itself is obfuscated (dt.a), so a full deobfuscation pipeline is unavoidable — hence ForgeGradle.
Why the tracks are cut where they are
The cut between 1.20.4 and 1.20.6 exists because 1.20.5 reworked the tick events:
| Version | What the tick event looks like |
|---|---|
| 1.20.2 – 1.20.4 | net.neoforged.neoforge.event.TickEvent$ServerTickEvent, carrying a Phase (START / END) — fires twice per tick |
| 1.20.6 and later | net.neoforged.neoforge.event.tick.ServerTickEvent$Post, split into Pre / Post subclasses — fires once per tick |
Not only the fully qualified name differs but the constructors and the class hierarchy too, so at compile time you can only pick one. The part that crosses that line is resolved at runtime by TickHook, which probes for whichever shape is present — so both tracks share one copy of the source.
Verified and not verified
| Status | |
|---|---|
| 26.2 + Forge 26.2-65.1.5 (Java 25) | verified on a real client and a dedicated server |
| 26.2 + NeoForge 26.2.0.93 (Java 25) | verified on a real client and a dedicated server |
| 1.21.1 + NeoForge 21.1.256 | verified on a real client |
| 1.20.2 + NeoForge 20.2.93 | verified on a real client |
| The 1.20.5 - 1.20.6 track | verified in game on 1.1.0 |
| 1.20.1 + Forge 47.4.26 | verified on a real client |
| Other minors inside a track | expected to work on the "same API within a track" argument, not verified one by one |
If OBA itself errors on some version, the log will contain [OBA] could not start up on this version, [OBA] could not register ... or [OBA] could not hook the server tick event. Report that line together with your version numbers and it can be looked at.
Verified in game
Four of the seven tracks were run on a real client: a test mod that deliberately throws in its constructor was dropped into mods, the client was started, and the question was whether OBA would name the culprit on its own and write the report into logs.
| Track | Client | Result |
|---|---|---|
| 1.21.x | 1.21.1 + NeoForge 21.1.256 | named CrasherMod.<init\> (CrasherMod.java:15), report written |
| 1.20.5 - 1.20.6 | 1.20.5 + NeoForge 20.5.21-beta | same, report written |
| 1.20.2 - 1.20.4 | 1.20.2 + NeoForge 20.2.93 | same, report written |
| 1.20.1 | 1.20.1 + Forge 47.4.26 | same, on an exact class name match |
Two problems only showed up by actually running it, and both are fixed now:
1. The descriptor has a different name in each version range
NeoForge 20.2 - 20.4 looks for META-INF/mods.toml; the rename to META-INF/neoforge.mods.toml only happened in 20.5. With the wrong name the loader does not even read the file, which shows up as "two files in the mods folder but nothing loaded" and a single found 2 invalid mod files line. Both descriptors now ship, and each track excludes the other.
2. The old dependency block has an extra required field
In 20.2 - 20.4 a [[dependencies]] block requires mandatory, and without it you get Missing required field mandatory in dependency. The field also has to come before type — the loader reads those keys as a stream, in order.
About 1.20
1.20 — meaning the original 1.20, not 1.20.1 — is not supported. NeoForge only started publishing on its own with 1.20.2, so that version range has no usable loader combination. Drop the NeoForge build of OBA into a 1.20.1 Forge instance and the loader rejects it outright:
Mod ID: 'neoforge', Requested by: 'oba', Expected range: '[20.2.0,20.5)', Actual version: '[MISSING]'
net.minecraftforge.fml.ModLoadingException: Mod 'oba' requires 'neoforge' '20.2.0 or above'
That instance carries net.minecraftforge:eventbus:6.0.5, net.minecraftforge:forgespi:7.0.1 and net.neoforged.fancymodloader:loader:47.2.2 — that is MinecraftForge 47.2.2, with net.minecraftforge.* packages. 1.20.1 therefore has its own Forge track producing its own jar, and the two lines never meet.
Why it works across minor versions
The mod depends only on the parts of the API that are stable across minor versions: the logging backend (log4j2), mod lifecycle events, the event bus, command registration and chat text. It never touches rendering, world data or registry contents, so it does not break when those change. The only place it did run into a version difference is the tick event, now isolated behind the adapter.
Initialisation is also isolated by design: the log collector, the class index, event registration, the command tree and the tick adapter each run in their own guarded block. If one of them throws on a version where an API shifted, only that one feature is disabled, and a line is left in the log to say so. A diagnostic tool has no business being the reason a game fails to start.
Adding a new track
When NeoForge moves to a new Minecraft version, three steps are enough:
- Copy
versions/1.20.5-1.20.6-neoforge/toversions/<new version>-neoforge/; - Change
minecraft_version,minecraft_version_range,neo_version,neo_version_range,archive_suffixand theparchment_*keys in itsgradle.properties; - Add the new directory to the lists in
tools/sync-versions.ps1andtools/build-all.ps1, then sync and build.
Shared code is copied out of engine/ by tools/sync-versions.ps1, so nothing is hand-duplicated and a new track picks up every engine change automatically. If the new version moves an API again, add another runtime adapter shaped like the TickHook implementations under src/loader/.
Quick start
In game
- Put
oba-mc1.21.1-1.1.0.jarinto.minecraft/mods/ - Start the game
From then on you do not type anything. When something breaks, the verdict appears in the log and in chat by itself, and the full report lands in logs on its own:
[OBA] Culprit: Awesome Machines [awesome-machines-2.1.0.jar]
[OBA] Failure location: com.example.awesomemachines.registry.MachineRegistry.register (MachineRegistry.java:143)
[OBA] Confidence: very high (99%)
[OBA] Report written to D:\MC\.minecraft\logs\oba-report-2026-10-10_11.31.58.txt
Type /oba report only when you want the full report on screen.
Command line
# find .minecraft, analyse the newest crash report, write the report into logs
java -jar oba-mc1.21.1-1.1.0.jar analyze
# point at a game directory (quoted or unquoted, paths with spaces are fine)
java -jar oba-mc1.21.1-1.1.0.jar analyze --game-dir "D:\my games\.minecraft"
# feed it one crash report
java -jar oba-mc1.21.1-1.1.0.jar analyze crash-reports/crash-2026-10-09_18.03.20-client.txt
# list mods, find duplicates, spot MCreator, mark Fabric mods
java -jar oba-mc1.21.1-1.1.0.jar mods --game-dir "D:\MC\.minecraft"
# follow the log while you play
java -jar oba-mc1.21.1-1.1.0.jar watch "D:\MC\.minecraft\logs\latest.log"
Try the bundled examples
examples/ is a deliberately broken mini game directory: six fake mods, three crash reports and a missing-dependency log.
# a NeoForge mod blows up
java -jar oba-mc1.21.1-1.1.0.jar analyze examples/crash-reports/crash-2026-10-09_18.03.20-client.txt
# a Fabric mod blows up under Connector
java -jar oba-mc1.21.1-1.1.0.jar analyze examples/crash-reports/crash-2026-10-10_11.35.02-client.txt
# an F3+C debug crash
java -jar oba-mc1.21.1-1.1.0.jar analyze examples/crash-reports/crash-2026-10-10_16.47.12-client.txt
java -jar oba-mc1.21.1-1.1.0.jar mods --game-dir examples
java -jar oba-mc1.21.1-1.1.0.jar mcreator --game-dir examples
Expected: the first names awesome-machines-2.1.0.jar at MachineRegistry.java:143; the second names sodium-extra-0.6.0.jar at MixinOptionsScreen.java:47; the third says the crash was deliberate; mods reports the duplicate sodium modId; mcreator flags coolstuff-1.0.1.jar.
The interface
There are two: the command line, and the in-game chat. Both share one look - a double-line title box with the version top right, the ◈ mark, section rules, and a highlight bar on the current item - and menu entries are clickable in both.
Command-line menu
Run it with no arguments:
java -jar oba-mc1.21.1-1.1.0.jar
╔════════════════════════════════════════════════════════════════╗
║ ◈ One BUG Among Us CLI OBA v1.1.0 ║
╚════════════════════════════════════════════════════════════════╝
Workspace D:\hmcl\.minecraft
Status ● Ready
Build One BUG Among Us 1.1.0
• Choose an action ──────────────────────────────────────────────
▶ [1] Diagnose the newest crash or log run
╭──────────────────────────────────────────────────────────╮
│ [2] List loaded mods run │
╰──────────────────────────────────────────────────────────╯
│ [3] Find MCreator generated mods run │
│ [4] Follow latest.log live follow │
│ [5] Capture and attribute a thread dump capture │
│ [6] Show process and index status run │
│ [7] Switch language switch │
│ [8] Open the newest report file open │
│ [9] Quit quit │
↑ ↓ move ENTER confirm number keys jump Q quit
↑↓move,ENTERconfirms,Qquits. Number keys1-9jump straight to an entry.- Coloured names are hyperlinks. In a terminal that supports them (Windows Terminal, iTerm2, most modern emulators), clicking one runs that single command in a new window, so the menu stays where it is. Under the hood each entry gets a one-shot launcher script and the script path is wrapped in an OSC 8 hyperlink; a terminal that does not support them ignores the control codes and shows plain text, so nothing breaks. The scripts live in a temp directory and are deleted on exit.
- To turn that off:
-Doba.clickable=false, or the environment variableOBA_CLICKABLE=false. - Piped, redirected, or run in CI (not an interactive terminal) it never shows the menu - it prints the usage instead, so control codes never leak into a log.
Colour is decided in the order -Doba.color → OBA_COLOR → "is this an interactive terminal". A non-empty NO_COLOR, TERM=dumb, or redirected output turns it off.
In game
/oba with no arguments is the clickable menu:
╔════════════════════════════════════════════════════════════╗
║ ◈ One BUG Among Us OBA v1.1.0 ║
╚════════════════════════════════════════════════════════════╝
• Choose an action ──────────────────────────────────────
█ ▶ [1] Show the newest full report █
[2] Process and index status run
[3] List loaded mods run
[4] Find MCreator generated mods run
[5] Capture and attribute a thread dump run
[6] Diagnose the newest crash report run
[7] Rebuild the mod index run
────────────────────────────────────────────────────────
• Commands that need an argument ────────────────────────
/oba info <id> a mod's description and metadata
/oba analyze <file> diagnose one file
Anything with a coloured marker can be clicked.
https://github.com/snowfish2871/One-BUG-Among-Us
The first row (the highlighted bar) is the recommended action. It is item 1 and it is clickable itself, so the list continues at [2].
Every line is clickable and runs its subcommand. Beyond the menu:
| Where | Click what | What happens |
|---|---|---|
the mod list from /oba mods |
a mod name | runs /oba info &amp;amp;amp;lt;id&amp;amp;amp;gt; for its description |
| a path in a report | the path | fills it into the chat box, ready to run |
/oba info &amp;amp;amp;lt;id&amp;amp;amp;gt; on the help page |
that command | fills it in for you to complete |
| the project URL on the help page | the URL | opens it in a browser |
The chat box does not render ANSI, so the in-game view is built directly from the same visual elements as a component tree: the same double-line title box, the same section rules and highlight bar, and the same semantic colours (frame, brand, emphasis, body, hint).
Widths work nothing like a terminal: the vanilla chat font is not monospaced — a space is 4 pixels, i is 2, a lowercase letter is 6, a CJK glyph is 9. Every in-game line is therefore measured in pixels before it is padded, with no trailing spaces and over-long lines wrapped by pixel width. GameUi carries a width table measured from the vanilla bitmap font and re-calibrates it against the client font metric once in game, so the three lines of the title box come out exactly equally wide and mixed Chinese/English never drifts out of alignment.
Report bodies are assembled by the engine for a 100-column terminal, so dropping them into the 320-pixel-wide chat would make vanilla wrap them on its own — and the leftovers look like garbage (a severity rule such as ── fatal ─────… breaks apart into several rows of dashes). Before anything reaches chat it is therefore reflowed to the chat width: lines that are mostly rule glyphs collapse into one full-width separator, everything else wraps by pixel width, and colours are carried per segment. A bold glyph takes one extra pixel, which is counted as well — otherwise a bold section header or highlight bar ends up exactly one pixel too wide and vanilla folds it onto a second line.
Incident timeline
Knowing the culprit is sometimes not enough — you also want to know what happened right before it broke. OBA arranges the log into a timestamped timeline, marking which mod each event came from:
[Incident timeline] (most recent event first)
time mod what happened
22:09:18 - + Failed loading villager cartographer registered at cartographer
22:09:10 - * Error loading class: net/caffeinemc/... (ClassNotFoundException)
22:08:04 - + registered internal shaders
22:07:54 kubejs + Key mapping key.kubejs.itemkillkey registered after event
22:07:49 modernui + Redirect registered font 'Source Han Sans SC' to 'modernui:...'
22:07:21 - * Failed to find class: java.util.zip.ZipException: zip END header not found
Reading it
- Read from the top. The first line is the moment closest to the failure; going down goes further back in time.
- The second column is the mod.
-means the line could not be attributed to a specific mod, usually because it came from vanilla or the loader. - The third column is a marker:
!!error,!warning,*rule hit,+lifecycle event,·plain information.
A deliberate trade-off
Logs run to tens of thousands of lines, so simply showing the last few hundred is useless — the tail is usually unrelated routine output. The timeline is therefore selected by importance: fatal and error events are always kept, rule hits come next, events attributed to a specific mod score higher, and events closer to the failure score higher still. The selection is then put back into chronological order.
A line repeated more than three times in a row is folded into a single notice, so one burst of spam cannot drown the timeline.
The timeline appears in all three output formats:
| Format | Presentation |
|---|---|
| Text | Aligned table, up to 40 rows |
| Markdown | Three-column table, up to 30 rows |
| JSON | A timeline array with time / modId / modName / type / event |
Diagnostic reports
Where they are saved
Always the game's logs folder, in game and from the command line alike.
| File | When it appears |
|---|---|
logs/oba-report-&amp;amp;amp;lt;timestamp&amp;amp;amp;gt;.txt |
On a captured error, a freeze, game exit, or a new crash report |
logs/oba-latest.txt |
Same content, fixed name, always the newest |
Timestamps look like yyyy-MM-dd_HH.mm.ss. Only the 10 most recent reports are kept; older ones are deleted so the folder does not fill up.
When it triggers
| Trigger | Detail |
|---|---|
| Captured error | As soon as an attributable error appears in the log. The same error (same culprit and exception) is written once |
| Freeze | After the watchdog captures a thread dump and attributes it. Routine vanilla heavy work (saving chunks, stopping the world, autosave) is not reported, to avoid noise |
| Game exit | If anything was captured this session, one final report is written. Besides the FML shutdown event there is a JVM shutdown hook |
| F3+C debug crash | This kind writes no log at all, only a file under crash-reports. On exit that folder is scanned and reports new to this session are diagnosed |
| Command line | analyze writes into the logs folder of --game-dir when it finds a problem; --output overrides the location |
Every report states up front why it was written:
OBA v1.1.0
Triggered by a captured error
Report only. No file was modified, disabled or deleted.
════════════════════════════════════════════════════════════════════════
What is in a report
In order: environment information, the verdict (culprit and failure location), the incident timeline, the suspect ranking, a detailed evidence chain per mod, the rule-hit findings, MCreator results, and the packages that could not be attributed.
Reading the verdict
[Verdict]
Most likely culprit: Awesome Machines (awesomemachines)
Mod file : awesome-machines-2.1.0.jar
SHA-1 : 0ea0b938b2eb0a092f37a38d95e8bcde8158840e
Confidence : very high (85%)
Failure location: com.example.awesomemachines.registry.MachineRegistry.register (MachineRegistry.java:143)
Exception : java.lang.NullPointerException: Cannot invoke "String.length()" because "name" is null
Failure locationis the line that matters:MachineRegistry.java:143maps straight onto the author's source.SHA-1confirms the file has not been altered. Attaching it to a bug report avoids the usual "wrong version" round trip.Confidenceis a band derived from evidence weight, not a probability. It answers "is the evidence solid", nothing more.Evidencelists every point awarded, so a wrong verdict shows which step went wrong instead of leaving you to trust or distrust a number.[exact class match]means the class really exists in that jar. When it instead reads[package match xxx (72%)], only a package prefix matched and the weight is discounted.
Using it
In-game commands
You do not need any command to get a diagnosis; these exist for when you want more.
| Command | Effect |
|---|---|
/oba report |
Show the most recent full diagnosis, and write it to logs |
/oba status |
Process state: memory, threads, deadlocks, indexed mods, report folder |
/oba mcreator |
List mods that look MCreator generated, with reasons |
/oba mods |
List loaded mods, versions, class counts, duplicate ids, graphics family |
/oba info &amp;amp;amp;lt;id&amp;amp;amp;gt; |
Show a mod's description and metadata |
/oba threads |
Capture a thread dump and attribute it |
/oba crash |
Analyse the newest crash report on disk |
/oba analyze &amp;amp;amp;lt;file&amp;amp;amp;gt; |
Analyse a specific log or crash report |
/oba reindex |
Rescan mods and rebuild the class index |
/oba help |
Command list |
Works in single player, on a LAN host and from a dedicated server console. No world state is ever changed.
Click a mod to read its description
Every row printed by /oba mods is clickable. Hovering shows a hint, and clicking expands that mod's description and metadata:
` Awesome Machines (awesomemachines) A multi-block machine that builds machines by itself. Needs FE to run. JEI shows the recipes; the config lives in config/awesomemachines-common.toml.
File awesome-machines-2.1.0.jar Version 2.1.0 Author Example Author License GPL-3.0 Loader javafml Classes 5 Size 1.6 KB SHA-1 0ea0b938b2eb0a092f37a38d95e8bcde8158840e Duplicate: 1 other file declares the same mod ID Looks MCreator generated (23 points) Graphics family: Sodium family (render optimizer) the main Forge/NeoForge port of Sodium `
The description comes from the mod's own metadata (the description field in mods.toml or fabric.mod.json); a jar that bundles several mods shows each one. Alongside it are the fields that actually matter when troubleshooting: SHA-1 (needed for bug reports), duplicate status, MCreator detection, and which graphics family it belongs to. A mod whose metadata has no description says so rather than showing nothing.
Chat cannot hold everything, so output is capped at 24 lines and then points at the report file.
Typing the command directly does the same thing:
/oba info sodium /oba info awesomemachines
Command line tool
oba analyze [log or crash report...] name the failing mod and the line
oba mods list the mods folder and check for problems
oba mcreator run only the MCreator detection
oba watch [latest.log] follow the log and report as it happens
oba threads [dump.txt] analyse a thread dump
oba version / help
| Option | Meaning |
|---|---|
--mods-dir &amp;amp;amp;lt;path&amp;amp;amp;gt; |
mods folder (auto-detected by default) |
--game-dir &amp;amp;amp;lt;path&amp;amp;amp;gt; |
game root containing mods/ logs/ crash-reports/ |
--format text|md|json |
output format, default text |
--output &amp;amp;amp;lt;file|dir&amp;amp;amp;gt; |
write the report to a specific path instead of logs/ |
--lang &amp;amp;amp;lt;en|zh&amp;amp;amp;gt; |
report language (default: system locale) |
--color |
force colours, even inside a pipe |
--no-color |
disable ANSI colours; wins over everything else |
--no-mcreator |
skip the MCreator check (faster) |
--thread &amp;amp;amp;lt;name&amp;amp;amp;gt; |
stalled thread to focus on, e.g. "Server thread" |
-q / -v |
terse output / print stack traces on errors |
Exit codes: 0 clean · 2 problems found · 64 bad arguments · 70 internal error.
Languages
Reports switch between English and Chinese, covering the engine, the CLI, the in-game messages and the log output.
Language resolution (highest priority first):
--lang <en|zh>on the command lineOBA_LANGenvironment variable, or-Doba.lang=zh- In-game mod: the language the player picked in the game options (client)
- JVM default locale (
user.language) - Fallback: English
So a Chinese client gets Chinese verdicts and any other client gets English, with no configuration.
java -jar oba-mc1.21.1-1.1.0.jar analyze --lang en --game-dir examples
OBA_LANG=en java -jar oba-mc1.21.1-1.1.0.jar mods --game-dir examples
All strings live in one table (engine/main/java/dev/oba/core/Lang.java) keyed in namespace.key form. Report text is stored as key plus arguments rather than a finished sentence and resolved when rendered, so a cached report still renders correctly after a language change.
The engine does not use Minecraft's own
assets/<modid>/lang/*.json, because the command line tool loads no game at all; one shared table is more reliable than maintaining two sets. Adding a third language means adding a constant, extending the branches indetect(), and adding a column to everyput(...). The self test fails if any key is missing.
How it works
In one line: line up three things -- what the game said, what the process was doing at the time, and which jar each class actually lives in -- then let rules and scoring pick the most likely culprit.
Where the three inputs meet
- The log. On startup it finds
logs/latest.log, follows it from the first line, and also registers an in-process log callback. Lines from both paths go through the same entry point and the same regex, which pulls out time, thread, level, logger and message. After that, "is this an error" depends only on the normalised text -- not on whether the message happened to come from[Render thread/WARN]. - The process. A daemon thread samples every thread's name, state and top frames every 250 ms and keeps only the snapshots that changed. Freeze detection reads it, and so does the incident timeline.
- The mods. It walks
mods/, reads each jar's metadata and class list, and builds a class-to-jar index. The index is tied to a fingerprint of that directory (name, size and mtime of every jar), so an unchanged directory means no rebuild.
From one error line to one finding
- Normalise. Strip timestamps, object addresses and thread ids, then use the rest as the dedupe key. One line repeating a hundred times still counts once.
- Match rules. Try the regexes in order and rank hits by how specific the wording is:
connector.intermediarynames a concrete loader and goes first, a vagueclass.noclassdefgoes later. Each log line yields at most one finding, otherwise a single crash would show up seven times in the list. A missing class in a library namespace is not reported:org.jetbrains,kotlin,com.google,org.slf4jand friends are usually a mod treating an integration as an optional soft dependency, and reporting them is pure noise. A mod's own package still reports. - Blame. Every stack frame is looked up in the index; a hit gives that jar a vote, and frames higher up or more specific count for more. Classes that resolve to nothing are listed separately so the missing family is obvious.
- Score and merge. Hits from the same rule against the same mod merge into one entry, the votes add up, and the result is multiplied by corroborating signals (was this mod already known bad, is it MCreator-generated, did it arrive through Connector).
- Render. Findings are handed to the renderer once they are final. The terminal lays them out in character columns; in game they are re-flowed to the chat HUD's pixel width. Same data, two layouts.
Why it works across versions
The diagnostics live in engine/ and touch no loader API at all: they read log text, thread dumps, jar files and stack strings. Only a thin layer is version-bound -- the loader entry point (@Mod, event bus, tick callback), the crash hook and the "am I the client" check, and the client-only font metrics and chat width.
Move to another major version and only that layer changes; the engine stays as it is. tools/sync-versions.ps1 copies engine/ verbatim into every track, so "change once, apply to nine tracks" has exactly one right way to happen.
Why startup is fast
The expensive part of startup is the index: open jars, read class names, tens of thousands of strings per pass. Four rules keep it off the critical path:
- Never block startup. The index runs in the background and the main thread keeps going. If something crashes before it finishes, the finding waits and is computed the moment the index is ready.
- Never run twice. An unchanged
mods/fingerprint returns immediately. - Scan in parallel. Jars are independent, so the scan runs on 2-8 threads picked from the CPU count and the results are reassembled in the original order.
- Never analyse on the log thread. A finding is queued onto OBA's own single thread; the log4j appender that handed over the line simply moves on. The game taught us this one: the analysis used to run inside the log callback, and a single oversized line sent one regex backtracking for tens of seconds, stalling every log write so the client looked frozen. Each line is now matched on at most its first 4096 characters, and the whole rule set gets at most 2 seconds per line before the rest is skipped.
How attribution works
1. Index: which class belongs to which jar
Each mod jar's entry table is read (directory listing only, nothing extracted or written), producing two maps:
- class name to jar: exact, confidence 1.0
- package name to jar: prefix match, at most 0.9, decreasing with depth
Package matching never falls back to a single-segment package such as com or org, otherwise any com.* mod would claim ownership of everyone else's frames. When two jars share a package, the one contributing more classes there wins.
Descriptor reading covers META-INF/neoforge.mods.toml, META-INF/mods.toml and Fabric's fabric.mod.json, so a mod's real name, id and version are known regardless of the loader it was built for.
2. Stack parsing
Real logs are messier than the textbook, so the parser handles:
at a.b.C.m(File.java:12)standard framesa.b.C$$Lambda$12/0x...lambdas and synthetic framesa.b.C$Inner.run(...)inner classesNative Method/Unknown Sourceframes without line numbers... 23 moreelision- multi-line exception messages, where the JVM prints the message raw across lines; the parser knows where to stop, otherwise the frames that follow get swallowed as message text
Caused by:/Suppressed:chains, linked root-cause-outward to matchThrowable.getCause()- real NeoForge frame format: a trailing
~[module@version/...] {re:classloading}, plus class-name prefixes likeTRANSFORMER/crasher@1.0.0/, both stripped correctly - prefixed log lines:
[18:03:20] [main/ERROR] java.lang.XxxException: ...
Prefixes that never belong to a user's mod are filtered out (java., net.minecraft., net.neoforged., org.spongepowered.asm., com.mojang., …), along with the internals of the Fabric loader and Connector, and OBA itself.
3. Scoring
| Evidence | Weight |
|---|---|
| Frame in the root cause chain | 0.30 + 0.10 × chain depth |
| Frame in an outer chain | the above × 0.45 |
| Exact class match | × 1.25 |
| Package match | × (confidence + 0.4) |
| First located frame of the root cause | +0.85 |
| Rule match, FATAL | +1.6 |
| Rule match, ERROR | +1.0 |
| modId mentioned in the text | +0.15 |
Anything below the noise floor (0.18) is dropped so the report is not drowned by unrelated mods. The weights are published so a wrong verdict can be traced to the step that produced it, rather than being a black box.
How freezes are judged
A freeze throws nothing, so the table above is useless. Instead:
- the watchdog measures the gap between server ticks;
- past 12 seconds it captures a thread dump (
ThreadMXBean, pure reads); - the stalled thread's frames are attributed to mods;
- deeper frames and the mod nearest the top of the stack are the most suspicious.
This answers "whose code was running when it hung", which is an inference rather than proof, and the report says so. /oba threads captures a dump on demand.
MCreator detection
Following the approach of McreatorDetector-1192 (metadata text, package names, the procedures package, file name) and extended with the structural fingerprints MCreator actually leaves in a compiled jar. Detection is multi-signal scoring, not a single condition.
Decisive signals (specific to MCreator; their presence is highly suspicious)
| Signal | Points | Note |
|---|---|---|
Package root is net.mcreator.* |
6 | MCreator's default package, the strongest signal |
A procedures package exists |
4 | Each visual procedure compiles into a class |
*ModElement classes exist |
4 | Mod element registration code |
| A resource carries "Generated by MCreator" | 5 | Bounded content probe over small files |
Metadata mentions mcreator |
4 | Name, description, author or link |
A variables package exists |
2 | MCreator's global variable system |
File name contains mcreator |
3 |
Supporting signals (only counted once a decisive signal is present)
| Signal | Points | Note |
|---|---|---|
Two or more *Model / *Renderer classes |
2 | Auto-generated entity or armour models |
Consolidated ModItems / ModBlocks style registries |
2 | Characteristic of generated code |
| Resource layout matches a generated export | 2 | models/ plus data/ plus paired procedure classes |
Below 6 points it is not considered MCreator. That threshold and the "supporting signals do not count on their own" rule were calibrated against reality: on a real 236-mod pack the earlier logic flagged 98 mods, including hand-written ones like Alex's Mobs, JEI and Xaero's Minimap — they merely use *Model, ModItems and similar ordinary Java naming that every mod has. After the change the same pack yields 2, and both genuinely carry a net.mcreator package, a procedures package and generator banners.
Every hit is printed, rather than a bare yes/no, so a false positive can be traced to the signal that caused it.
A hand-written mod whose file name contains
mcreatorand whose description says "Made with MCreator" will also be flagged. That is intentional: this detects generation traits, not authorship, and a hit says nothing about the mod's quality.
Graphics mods
Sodium, Iris and their relatives are where packs most often break, and their problems are invisible in a stack trace — they usually do not throw at all, they just crash the render thread, go black, or fail to load a shader pack. On top of that the family is full of forks that were renamed:
| Family | Members (the same thing under different names) |
|---|---|
| Sodium | Sodium, Rubidium, Embeddium, Xenon, Sodium Reforged, Sodium Forge |
| Iris | Iris Shaders, Oculus, NeOculus |
| Lithium | Lithium, Radium, Canary |
| Krypton | Krypton, Krypton Reforged |
| Phosphor | Phosphor, Phosphor Create |
OBA knows the relationships, so it catches two problems no amount of log reading would find.
1. Two mutually exclusive members of one family
[Graphics mods] (Sodium, Iris and their many forks)
! Two members of the Sodium family are installed at once
- embeddium-0.3.31.jar
- rubidium-0.7.1.jar
-> embeddium / rubidium are the same renderer under different names and are mutually
exclusive. Keep exactly one and remove the rest; having two is a common cause of a
black screen or a crash on the render thread.
2. A shader loader with no renderer to attach to
! oculus has no renderer to attach to
oculus-1.7.0.jar
-> oculus is a shader loader with no renderer of its own; it needs embeddium or rubidium.
iris pairs with sodium; oculus and NeOculus pair with embeddium or rubidium. OBA knows which member needs which.
Fabric builds loaded through Connector are recognised too
Sodium and Iris both ship Fabric builds, and those can end up on NeoForge through Sinytra Connector. The modId is still sodium or iris and the classes still live under net.caffeinemc., me.jellysquid.mods.sodium. and net.irisshaders., so family detection works identically for the Fabric and Forge builds with no extra configuration.
Rules for graphics failures
These logs look nothing like an ordinary error, so they get their own rules:
| Rule | Triggers on | Note |
|---|---|---|
graphics.shader_compile |
a shader that fails to compile | The pack does not match this Iris/Sodium build, or the driver rejected the GLSL |
graphics.shader_pack |
a shader pack that fails to load | Incomplete pack, wrong game version, or an option the loader does not provide |
graphics.sampler |
a missing shader sampler | The advice names the sampler that is missing |
graphics.framebuffer |
an incomplete framebuffer | A black screen and missing world rendering usually follow |
graphics.mixin_conflict |
mods patching each other's render classes | A frequent source of visual glitches and render-thread crashes |
graphics.unsupported_driver |
an old or unsupported driver | Sodium and Iris want a reasonably modern OpenGL 4.5 driver |
graphics.glfw_init |
GLFW failing to start | Happens before mods load, so it is a driver or launcher problem |
graphics.context_lost |
a lost GL context | Usually a GPU hang caused the driver to reset the device |
Add-ons are not mistaken for rivals
Add-ons like Sodium Extra or Sodium Dynamic Lights are supposed to sit alongside the main renderer, so conflict detection only considers the two roles that occupy the main slot (render optimizer and shader loader). This was calibrated on a real pack shipping embeddium plus sodiumdynamiclights: the earlier logic reported the dynamic lights mod as "another Sodium branch".
Also, the file named rubidium-extra actually declares the modId embeddium_extra, so each member carries a list of aliases — matching a single id would miss it.
Sinytra Connector support
Sinytra Connector lets Fabric mods run on NeoForge. With it installed a pack contains three kinds of thing at once: native NeoForge mods, Fabric mods, and Connector itself. OBA adapts to that mix in four places.
1. Fabric mods are identified properly
A Fabric mod's identity lives in fabric.mod.json, not neoforge.mods.toml. Both are read, so a Fabric mod keeps its name, modId and version instead of degrading to a bare file name.
2. Fabric mod frames are no longer discarded as loader code
The whole net.fabricmc. prefix used to be treated as "not the user's code". Under Connector that is exactly where a Fabric mod's own classes live, so the blanket rule meant a Fabric mod could never appear in a report. Now only the loader internals are excluded:
| Excluded | Not excluded (participates in attribution) |
|---|---|
net.fabricmc.loader.* |
net.fabricmc.fabric.api.* (Fabric API, a mod) |
net.fabricmc.mappingio.* |
A mod's own packages, e.g. me.jellysquid.mods.* |
net.fabricmc.accesswidener.* |
|
net.fabricmc.tinyremapper.* |
|
org.sinytra.connector.locator/transformer/util.* |
org.sinytra.connector.ConnectorMod itself |
3. Three Connector-specific rules
| Rule | Triggers on | Advice |
|---|---|---|
connector.failure |
Connector reporting a remap or transform failure | Check Connector and Fabric API versions, then whether that Fabric mod supports this game version |
connector.missing_mod |
Fabric mod xxx is not present |
Install the missing Fabric mod, or remove the mod depending on it |
connector.intermediary |
class_1234 / method_123 in the log |
Intermediary names were never remapped; usually the mod bypassed Connector or targets another version |
4. The environment is stated in the report
When Connector is found in the index, the report adds a note saying Fabric mods will show up under their own jars and that class names may have been remapped — so a wall of net.fabricmc does not look like a fault.
One caveat: after Connector remapping, a Fabric mod's class names may be the remapped ones and not match the author's source. Line numbers are usually accurate, so searching by line number is more reliable when the name looks unfamiliar.
Rule set
34 rules over real log text, plus 3 Connector-specific ones and 1 for deliberate crashes, each carrying concrete advice. Nine are new in 1.1.0: six cover worlds and maps (entity tick crash, block entity tick crash, chunk generation failure, DataVersion mismatch, corrupt entity data, world load aborted), two cover client-only classes on a server, and one covers the loader refusing a jar outright — a single bad descriptor keeps the whole mods folder from loading, which looks exactly like "mods installed, none active".
| Category | Rules |
|---|---|
| Dependencies | missing prerequisite, missing mandatory dependency, version mismatch, incompatible mods |
| Duplicate loading | several files for one modId, Found duplicate mod |
| Mixin | injection failed, config failed to load, transformation threw, MixinExtras too old |
| Class loading | NoClassDefFoundError, ClassNotFoundException, NoSuchMethodError, NoSuchFieldError |
| Registries | missing registry entries, registry conflict |
| Data and resource packs | data pack failed to load, resource pack failed to load |
| Worlds and maps | entity tick crash, block entity tick crash, chunk generation failure, DataVersion mismatch, corrupt entity data, world load aborted |
| Client and server | client-only class on a server, missing client-only class, hard reference to GUI classes |
| Loader / descriptors | descriptor refused, mandatory missing from [[dependencies]], whole mods folder not loading |
| File integrity | bundled library missing, corrupt jar |
| Runtime | out of memory, server frozen, graphics driver problem, mod failed to load or construct |
| Connector | transform failure, missing Fabric prerequisite, intermediary names leaked |
| Deliberate | F3+C manual debug crash (reported as intentional) |
Rules only fire on unambiguous text. The structural lines of a crash report itself (Description:,Time,Head,Mod List, …) are explicitly excluded, so "the report describes a crash" is never mistaken for "a crash happened".
Project structure
The engine is maintained in exactly one place, under engine/. Each version directory is assembled from it plus the matching loader implementation by tools/sync-versions.ps1, so no jar ever contains another loader's code.
oba/
│
├── README.md Simplified Chinese README
├── README.en.md This file (English)
├── LICENSE Full GPL-3.0 license text
├── dist/ All nine version jars land here (produced by tools/build-all.ps1)
├── docs/ Title banner used by the READMEs
├── examples/ Deliberately broken samples: fake mods, several crash reports
│
├── engine/ The authoritative copy of the diagnostic engine (each version's core/ is a synced copy)
│ ├── main/java/dev/oba/* Engine sources: core / loader interfaces / shared mod layer
│ ├── main/resources/ Descriptors and icons (three descriptors, each loader takes its own)
│ └── tools/ Engine self test and runtime harness
│
├── src/loader/ Loader adapter implementations, one directory per loader
│ ├── neoforge/ NeoForge loader implementation (1.20.2 and up, including 1.21.x)
│ └── forge/ Forge loader implementation (1.20.1)
│
├── tools/ Scripts for building, syncing, testing and real-client verification
│ ├── build-all.ps1 Builds all nine tracks into dist/
│ ├── sync-versions.ps1 Syncs the engine into each version
│ ├── run-engine-tests.ps1 Runs the engine self test and harness
│ ├── run-version.ps1 Launches a client for real-client checks
│ ├── mirror-neoforged.init.gradle Rewrites the dead maven host
│ └── crasher/ Test mod that crashes (one per loader)
│
└── versions/ One self-contained, directly buildable directory per version
├── 26.2-forge/
├── 26.2-neoforge/
├── 1.21.1-neoforge/
├── 1.20.5-1.20.6-neoforge/
├── 1.20.2-1.20.4-neoforge/
├── 1.20.1-forge/
├── 1.21.1-forge/
├── 1.20.5-1.20.6-forge/
├── 1.20.2-1.20.4-forge/
For the file listing inside each version directory, see that directory's own README.
This tree is what the default branch looks like: one engine, all nine version directories present, so every jar can be built in one pass. Each version also has its own branch holding just that version's self-contained project (
src/main,build.gradle, README) — see Branches.
Verification
| What | How | Result |
|---|---|---|
| Engine logic | pwsh tools/run-engine-tests.ps1 |
199 / 199 assertions pass |
| Engine coverage | indexing, attribution, cause chains, mixin failure, missing dependency, duplicate mods, thread dumps, MCreator detection, three report formats, exploded mods, languages, Connector mixed packs, real NeoForge frame format, log encodings, F3+C, paths with spaces, incident timeline, graphics families and conflicts | pass |
| Runtime behaviour | engine/tools/RuntimeHarness.java (real log4j2 + real mod classes) |
52 / 52 assertions pass |
| Runtime coverage | log capture, cause chain fidelity, automatic verdict, report written into logs, F3+C crash-report pickup, bilingual rendering, thread dumps, read-only guarantee | pass |
| One jar, two jobs | java -jar ... version / simulated game environment / -Doba.mode=cli |
all three dispatch correctly |
| Dedicated servers | eight real servers: Forge 1.20.1-47.4.28 / 1.20.2-48.1.0 / 1.20.6-50.2.14 / 1.21.1-52.1.20 / 26.2-65.1.5, NeoForge 21.1.262 / 20.2.93 / 26.2.0.93 | all load and stop cleanly; oba status / oba mods / oba report work from the console; a client-only mod is named in the report |
| Corrupt save on a server | truncated world\region\r.0.0.mca to 300 bytes |
reported as "the chunks in this save cannot be read", naming the .mca; the hundred near-identical lines collapse into one finding and one timeline row |
| Client-only mod on a server | a probe mod whose common class touches net.minecraft.client.gui.screens.Screen (checked on Forge 1.20.1 and NeoForge 20.2.93) |
the report written on shutdown names the probe mod with confidence high (55%), first finding "a client-only mod is installed on a server". The console shows nothing: it dies during mod loading, before OBA gets to speak |
| Real client | 1.21.1 + NeoForge 21.1.256, with a mod that throws during construction | correctly names CrasherMod.java:13, report lands in logs |
| Real modpack | 236 mods / 52,313 classes / 1 GB | scanned in 1 second; found a genuinely duplicated coroutil and placed 5 graphics mods into their families |
| Real GBK log | 61,380-line Chinese modpack log | read cleanly; 69 of 1,128 frames attributed |
| Paths with spaces | --game-dir "...my games..." (quoted and unquoted) |
both forms give identical results |
Known limits
Does
- attribute stack frames to a mod file with class, method and line
- build an incident timeline showing what each mod was doing around the failure
- recognise 34 unambiguous error patterns, plus Connector and deliberate-crash rules
- name who was executing when the game froze, without an exception
- detect MCreator generation traits and show its reasons
- keep Fabric mods attributable when Sinytra Connector is present
- detect log encoding from content, so both UTF-8 and GBK logs are readable
- accept paths containing spaces, quoted or not
- speak English and Chinese, and never write to your files
Does not
- obfuscated mods: line numbers usually survive, class names may not be attributable
- Fabric class names may not match the source: after Connector remapping, search by line number
- code outside the mods folder: Java agents, coremods and launcher injections are not indexed and show up under "packages that could not be attributed"
- a client crash on a multiplayer server: the client log or crash report has to be brought over
- "plugins": NeoForge has no separate plugin system like Bukkit/Spigot, so everything here is a mod
- attribution is not blame: the most suspicious mod is not necessarily the buggy one; A may only fail because B corrupted shared state first. The report gives the evidence chain, the final call is yours
- freeze analysis is inference: the mod at the top of the stack is the first thing to test, not proof of an infinite loop
Contributing
Issues and pull requests are welcome. To make a bug report easy to act on, please include:
logs/oba-latest.txt, if a report was already produced- the original log or crash report
- game version, NeoForge version, OBA version
- if OBA itself is erroring, the lines starting with
[OBA]
Credits
- Special thanksXinKo_TSSupport and maintenance for this mod
- McreatorDetector-1192 — reference implementation for MCreator detection
- Sinytra Connector — bridging Fabric mods onto NeoForge
- NeoForged and MinecraftForge — loader and event system
- The testers who provided real modpacks and crash logs
- Deepseek does multilingual translation

