One BUG Among Us
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.
介绍
One BUG Among Us
Turns a Minecraft crash into one sentence a person can act on.
Author’s Statement: Since this mod has a very high priority, please do not apply it to your important or active servers or save files. The contributors of this mod will not be responsible for any effects it may have on other components, or any other problems and impacts! By using it, you agree to accept the potential instability risks.

Contents
- What this is
- Supported versions
- How one error becomes a verdict
- Collection: logs and crash reports
- The mod index
- Attribution and confidence
- Rule set
- How false positives are kept out
- Cross-version support
- Situations it handles
- In-game interface
- The companion process
- Command line
- Building
- Layout
- Known limits
- License
What this is
A crashed modpack log is long, and the useful part is usually one Caused by: line plus a stack.
A human scrolls up, finds the at frames that mention a mod’s package, and works out which mod that
package belongs to. OBA turns those steps into a fixed pipeline: a rule recognises the kind of error,
the stack frames land on a specific mod, and the two become one sentence plus an issue list.
It does not guess. Every verdict carries its evidence: which rule matched, which log line, which
frame, which jar.
Supported versions
| Minecraft | Loader | Java | Artifact |
|---|---|---|---|
| 1.20.1 | Forge, 47 line, built against 47.4.26 | 17 | oba-mc1.20.1-forge-1.2.0.jar |
| 1.20.2 / 1.20.3 / 1.20.4 | Forge, 49 line, built against 49.2.13 | 17 | oba-mc1.20.2-1.20.4-forge-1.2.0.jar |
| 1.20.2 / 1.20.3 / 1.20.4 | NeoForge, 20.2 line, built against 20.2.93 | 17 | oba-mc1.20.2-1.20.4-1.2.0.jar |
| 1.20.6 (Forge never shipped 1.20.5) | Forge, 50 line, built against 50.2.14 | 21 | oba-mc1.20.5-1.20.6-forge-1.2.0.jar |
| 1.20.5 / 1.20.6 | NeoForge, 20.5 line, built against 20.5.21-beta | 21 | oba-mc1.20.5-1.20.6-1.2.0.jar |
| 1.21.1 | Forge, 52 line, built against 52.1.20 | 21 | oba-mc1.21.1-forge-1.2.0.jar |
| 1.21 – 1.21.x | NeoForge, 21.1 line | 21 | oba-mc1.21.1-1.2.0.jar |
| 26.2 | Forge, 26.2 line, built against 26.2-65.1.5 | 25 | oba-mc26.2-forge-1.2.0.jar |
| 26.2 | NeoForge, 26.2 line, built against 26.2.0.93 | 25 | oba-mc26.2-1.2.0.jar |
1.20.1 is Forge only. From 1.20.2 to 1.21.x every range ships both a Forge and a NeoForge track, and
26.2 ships two. Forge and NeoForge tracks in the same range share the engine code and differ only in
the adapter layer.
How one error becomes a verdict
- Collect: an appender on log4j2 catches ERROR/FATAL lines; new files in
crash-reports/come in too. - Index: scan
mods/*.jarand build two maps — class name to mod, package prefix to mod. - Match rules: 49 regex rules run over each line; one log line yields the most specific verdict.
- Resolve frames: every
atclass name is looked up in the index; if that fails, the package prefix gives a shortlist. - Score: distance from the root cause, rule severity, whether the mod id appears in the log text, whether several frames agree.
- Report: a one-line headline, a culprit card (mod, file, first frame, confidence), an issue list, advice. Console, chat, report file and companion window all read the same data.
Collection: logs and crash reports
Why an appender: reading stdout would need a second process, would miss lines written before a
server’s output redirect is set up, and would lose the log level. The appender sees the Level and
the logger name before the line is even written.
A few details were learned the hard way:
- Level tests cannot use
level.isInRange(Level.ERROR, Level.ALL). In log4j2 the severity integers run
backwards (FATAL=100, ERROR=200, INFO=400), so that expression means “ERROR through most verbose”
and counts INFO as an error. The code usesisMoreSpecificThan(Level.ERROR), and only
isMoreSpecificThan(Level.FATAL)counts as fatal. - Analysis must not run on the appender thread. One very long line combined with a backtracking regex
can stall all log writing; lines are cut at 4096 characters, the whole rule pass has a two second
budget, and analysis runs on a single-worker queue. - It does not read itself: lines starting with
[OBA]are skipped, otherwise the report would be
analysed as a new error. - Repeats are merged: the same line is analysed once per five seconds, the same verdict is announced
once per five minutes, and the repeat count is reported with the next announcement. - Crash reports: a file in
crash-reports/newer than the session start joins the timeline; if the
session ends before analysis, the shutdown hook writes a report from it.
The mod index
The index is what attribution stands on. Each jar is opened once and yields the modId, display name,
version, file name, and every .class name and package prefix inside it.
Three things are not optional:
- Do not trust only the loader’s file list. NeoForge hands out paths in a union file system, and
Files.isRegularFileis false for them; relying on it alone indexed 3 of 16 mods. So after the
loader list the scanner also walks<game dir>/mods/*.jarand de-duplicates by path. - Keep platform jars out. Anything under
libraries/, anything namedneoforge-,forge-,
client-extra,securejarhandler, is not a mod. Left in, the verdict points at the loader itself. - Do not block startup. Indexing runs on a background pool (
min(8, cores)); if the mods folder is
unchanged (name, size, modification time) it is not scanned again. - Read mods that live inside other jars. A jar-in-jar mod usually sits on a zip file system opened by
FileSystems.newFileSystem, wherePath.toFile()throwsUnsupportedOperationException. Everything is
therefore read as a stream, and a path is used only when it really is a local file; the entry points live
inModFiles(isPlainFile,contains,readAllBytes), and OBA never opens a zip file system itself.
Nested mods then count as their own source and can be blamed on their own.
Attribution and confidence
A frame lands on a mod by one of two routes: the full class name is in the index, or the package prefix
is. When neither works the verdict says “no frame belonged to a mod” and keeps only the rule’s advice,
which is better than pinning it on an innocent mod.
Confidence is a weighted sum, not a probability. A frame starts at 0.30 + 0.10 × depth, is multiplied
by the matched rule’s own confidence, and gets +0.85 when it is the root-cause frame (+0.35 for other
frames in the exception chain); frames in library code, or frames matched only by a package fragment,
are discounted by 0.45. Seeing the mod id or file name in the log text adds one more evidence entry.
A score of 2.0 or more reads as “certain”, 0.9 as “high”, 0.45 as “medium”, anything else “low”. The
percentage shown in the window is score / 2.5, capped at 99% — the score is accumulated weight, not a
probability, so it never claims 100%. Below the lowest band no culprit is named and the verdict says that
no frame belonged to a mod.
Rule set
Loader and dependencies (6)
| Rule id | Recognises | Points at |
|---|---|---|
neoforge.missing_dependency |
the Missing or unsupported mandatory dependencies lines |
names the missing mod |
neoforge.missing_dependency_generic |
the same dependency mismatch without the listing | same, guessed from context |
neoforge.incompatible_mod |
Mod ... requires ... which is missing |
version mismatch |
neoforge.dependency_version |
the range in requires <mod> <range> is not satisfied |
installed, but too old or too new |
fml.duplicate_mod |
Found duplicate mods |
one modId twice; lists both files |
fml.duplicate_mod_alt |
the older FML wording | same |
The mod file itself (3)
| Rule id | Recognises | Points at |
|---|---|---|
loader.invalid_mod_file |
InvalidModFileException / Missing required field ... in dependency / found an invalid mod file |
one bad jar stops the whole mods folder |
jar.corrupt |
zip END header not found / invalid CEN header |
half-downloaded jar |
jarjar.missing |
Unable to resolve ... inside jar |
a dependency missing inside a jar-in-jar |
Mixin (6)
| Rule id | Recognises | Points at |
|---|---|---|
mixin.apply_failed |
Mixin apply failed / MixinTransformerError |
which mixin, which target, who broke it |
mixin.apply_failed_alt |
the other wording | same |
mixin.transformer_error |
MixinTransformerError |
same, through the transformer path |
mixin.config_failed |
Failed to load mixin config |
the mixin config itself is broken |
mixin.crash |
org.spongepowered.asm.mixin.throwables.MixinException |
thrown from inside a mixin |
mixinextras.version |
MixinExtras version not satisfied |
MixinExtras too old |
Class loading and linking (4)
| Rule id | Recognises | Points at |
|---|---|---|
class.noclassdef |
NoClassDefFoundError: <class> |
which class is missing; attributed by package |
class.classnotfound |
ClassNotFoundException: <class> |
same, the other route |
class.nosuchmethod |
NoSuchMethodError |
a method that does not exist, usually a version mismatch |
class.nosuchfield |
NoSuchFieldError |
same for a field |
Registries and resources (4)
| Rule id | Recognises | Points at |
|---|---|---|
registry.missing |
Unknown registry key / Missing registry |
a missing entry, usually from a data pack or mod |
registry.duplicate |
Duplicate registration |
two mods claiming one id |
datapack.load_failed |
Failed to load datapack |
a data pack that will not parse |
resourcepack.load_failed |
Failed to load resource pack |
a resource pack that will not parse |
Saves and maps (6)
| Rule id | Recognises | Points at |
|---|---|---|
world.ticking_entity |
Ticking entity / Exception while ticking entity |
a broken entity, with type and coordinates |
world.ticking_block_entity |
Ticking block entity |
a broken chest, machine or spawner |
world.chunk |
Region file ... is corrupt/missing / has truncated header / has an invalid sector |
a broken region file |
world.dataversion |
Data version ... is newer / Unknown data version |
a save from a newer game version |
world.entity_data |
Failed to load entity data / Invalid entity NBT |
invalid NBT in an entity or container |
world.level_load |
Failed to load level data or datapacks |
the world never started; points at the packs or mods it references |
Rendering and graphics (10)
| Rule id | Recognises | Points at |
|---|---|---|
opengl.unsupported |
Unsupported OpenGL version or a version below 3.2 |
an old driver |
graphics.unsupported_driver |
GLFW error 65542 (Microsoft Basic Render) |
no GPU driver; software rendering |
graphics.glfw_init |
Failed to initialize GLFW |
no window or GL context |
graphics.context_lost |
GL context lost / Context lost |
the driver dropped the context |
graphics.shader_compile |
Shader compile error / Could not compile shader |
which shader failed |
graphics.shader_pack |
Failed to load shader pack |
a shader pack that will not load |
graphics.sampler_missing |
could not find sampler named |
a missing sampler; points at graphics mods or the pack |
graphics.framebuffer |
Framebuffer is not complete |
an incomplete framebuffer, mostly with shaders |
graphics.mixin_conflict |
Mixin conflict / already transformed |
two optimisation mods patching one method |
graphics.family_conflict |
the other wording for the same family clash | same |
Memory and stalls (2)
| Rule id | Recognises | Points at |
|---|---|---|
memory.outofmemory |
java.lang.OutOfMemoryError |
not enough heap; shows the current limit |
watchdog.timeout |
a server tick over the threshold (timed by OBA, not from the log) | which phase stalled, plus the last action |
Server and client (2)
| Rule id | Recognises | Points at |
|---|---|---|
server.client_class |
Attempted to load class net/minecraft/client/... for invalid dist DEDICATED_SERVER |
a client-only mod on a server; names the class owner |
server.client_class_notfound |
ClassNotFoundException: net.minecraft.client.* |
same, the other route |
Mod lifecycle (3)
| Rule id | Recognises | Points at |
|---|---|---|
mod.loading_failed |
Mod Loading has failed / Failed to create mod instance |
the loading phase failed; culprit found from context |
mod.construct_error |
Failed to complete lifecycle event CONSTRUCT |
a mod constructor threw |
manual.debug_crash |
Manually triggered debug crash |
not a bug; triggered with F3+C |
Sinytra Connector (3)
| Rule id | Recognises | Points at |
|---|---|---|
connector.intermediary |
intermediary names such as class_1234 / method_5678 |
a Fabric mod not remapped under Connector |
connector.failure |
Connector’s own errors | Connector version or compatibility |
connector.missing_mod |
Requires ... Fabric API and similar |
a missing Fabric-side dependency |
How false positives are kept out
- Library namespaces: a missing class in
org.jetbrains.annotations,kotlin,com.google,
org.apache,io.nettyororg.slf4jis not a symptom (plenty of mods forget aprovided
dependency). Those stay quiet unless the class starts withnet.minecraft.client.. - Platform jars: not in the index, so the loader is never the culprit.
- A sampler name is not a mod: when a shader reports
could not find sampler named Sampler2,
Sampler2is a name the shader chose. The rule keeps only the fact that a sampler is missing. - Warnings do not write files: a hit that is warning-level and names no mod is announced once on
the console and produces no report file, otherwise a per-frame graphics warning leaves one useless
file per launch. - One line, one verdict: before comparing, leading
Caused by:,Exception message:and
Suppressed:labels are stripped and digits are replaced with placeholders, so the same line with a
different coordinate or millisecond is not counted twice.
Cross-version support
How the tracks are cut: by “one engine build compiles and behaves the same”, not by patch version.
1.20.1 and 1.20.2 differ in both mappings and reobf; NeoForge reworked tick events and moved its
descriptor around 1.20.5; 1.21.1 and 26.2 differ in Java version and build plugin.
At compile time:
| Track | Target | Build plugin | Note |
|---|---|---|---|
| 1.20.1 Forge | Forge 47 line | ForgeGradle 6 | reobf required |
| 1.20.2~1.20.4 | Forge 48/49, NeoForge 20.2~20.4 | ForgeGradle 6 / NeoGradle userdev | reobf on the Forge side only |
| 1.20.5~1.20.6 | Forge 50, NeoForge 20.5~20.6 | same | Forge drops reobf; descriptor moves |
| 1.21.1 | Forge 52, NeoForge 21.1 | same | |
| 26.2 | Forge 65, NeoForge 26.2 | ForgeGradle 7 / ModDevGradle 2 | Java 25; ForgeGradle 7 rewrote the DSL |
Probed at run time (reflection instead of compile-time constants):
- Tick events:
event.tick.ServerTickEvent$Postthenevent.TickEvent$ServerTickEventare tried with
Class.forName; registration goes throughIEventBus.addListener(Class, Consumer)so nothing is
registered against the abstractEventclass, which throwsIllegalArgumentException. - Descriptor: three templates (
mods-forge.tomlfor Forge, which becomesmods.toml, andmods.tomlor
neoforge.mods.tomlon the NeoForge side); the build picks one. - Client or server: read
FMLEnvironment.distor the loader marker inMETA-INF. Never
Class.forName("net.minecraft.client.Minecraft")— on a server that tripsRuntimeDistCleaner, and
the resulting error looks exactly like “a client-only mod is installed on a server”.
Situations it handles
| Situation | What OBA does | How |
|---|---|---|
| A missing dependency | Names what is missing and which version range it wants | neoforge.missing_dependency* |
| A mod built for another version | Says whether it is too old or wants something newer | neoforge.dependency_version / incompatible_mod |
| Duplicate modId | Lists both files so the old one can be removed | fml.duplicate_mod* |
| One bad jar stops the whole mods folder | Names the jar and explains the mandatory field on 1.20.2~1.20.4 |
loader.invalid_mod_file |
| A half-downloaded jar | Suggests downloading it again, with the file name | jar.corrupt |
| A mixin that failed to apply | Points at the mixin class and the target, and at whoever broke it | mixin.* |
| Class, method or field not found | Attributed by package; library namespaces stay quiet | class.* plus the noise filter |
| Registry entries missing or duplicated | Names the registry key and the mods involved | registry.* |
| Data pack or resource pack failed to parse | Names the pack and the first parse error | datapack.* / resourcepack.* |
| A broken entity in the save | Entity type, coordinates, first line of the exception | world.ticking_entity |
| A broken block entity (chest, machine, spawner) | Block coordinates and the exception | world.ticking_block_entity |
| A corrupt region file (truncated, bad sector) | Says which .mca and offers restore-from-backup or delete |
world.chunk |
| A save from a newer game version | Explains the DataVersion mismatch; downgrading is not something a mod can fix | world.dataversion |
| Invalid NBT in an entity or container | Points at that entry and notes it usually follows a removed mod | world.entity_data |
| The world never loaded at all | Points at the data packs or mods the save references | world.level_load |
| Old GPU driver, software rendering | Says it is the driver, not a mod | opengl.* / graphics.unsupported_driver |
| Shader compile errors, missing samplers, incomplete framebuffer | Points at graphics mods or the shader pack | graphics.shader* / sampler_missing / framebuffer |
| Two optimisation mods patching the same method | Names both sides | graphics.mixin_conflict / family_conflict |
| Out of memory | Shows the current limit and suggests a larger -Xmx |
memory.outofmemory |
| A server that stopped ticking | OBA times ticks itself and reports the stalled phase | watchdog.timeout |
| A client-only mod installed on a server | Names the class and which mod owns it | server.client_class* |
| Fabric mods under Sinytra Connector | Resolves intermediary names back to the Fabric mod | connector.* |
| An MCreator workspace | Recognised by its directory and naming patterns, with its own advice | McreatorDetector |
| A crash before OBA itself is constructed | No window and no report; the next start reads crash-reports/ |
Shutdown hook plus startup read |
| A normal exit that did capture errors | The companion window still opens, caption “errors seen” | capturedErrors in the handoff |
| A crash inside a native library or a kill from Task Manager | Falls back to the crash report written this session | CrashReporter |
| A mod that carries other mods inside its jar (jar-in-jar) | The inner mod counts as its own source and can be blamed like any other | ModSource streaming plus ModFiles |
In-game interface
/oba is a command tree that mirrors the CLI: help, status, report, mods, mcreator,
analyze, crash, info, plus two troubleshooting entries: reindex drops the index and rescans
mods/, and threads writes the current thread dump into the report. Every clickable entry carries a ClickEvent, so one click runs it.
The chat font is not monospaced: a space is 4 px, a lowercase letter about 6 px, a CJK glyph 9 px and
bold adds 1 px per glyph. A 100-column terminal layout pushed into chat wraps into confetti, so the text
is reflowed by pixel width before it goes to the chat (GameUi.lines), and rule lines are collapsed to a
single glyph row.
The companion process
Some crashes leave no time to speak: a fault inside a native library, a kill from Task Manager, a
failure during mod loading. No line of the game’s code runs again, and the verdict printed to chat goes
with it.
OBA starts a second JVM at client startup with javaw -cp <the same jar>, main class
dev.oba.companion.CompanionMain — no separate executable, no unpacking the UI into a temp directory.
It does one thing: wait for the game process to disappear, read the handoff file, open a window.
| What happened this session | Caption | Window |
|---|---|---|
| No error, normal exit | clean exit | none |
| Errors captured, no crash | errors seen | opens |
| Crash (new crash report, or the handoff says so) | crash | opens and raises itself |
| Crash before OBA is constructed | — | nothing at all |
The handoff goes to logs/oba-companion/handoff-*.json: one-line verdict, culprit, issue list with rule
names, timeline, environment, full report. The window shows verdict, culprit and issues on the left;
report, timeline, environment and raw JSON on the right.
The window is frosted glass: a screenshot of what sits behind it, downscaled to a quarter, blurred, tinted, clipped to rounded corners and framed by a soft outer shadow plus a one-pixel highlight edge. Windows has no public backdrop-blur API, so the backdrop is a captured frame, re-taken whenever the window moves.
Switch: -Doba.companion=off|auto|always (or OBA_COMPANION), default auto. To see it without a
crash: java -cp <jar> dev.oba.companion.CompanionMain --demo.
Command line
java -jar oba-mc1.21.1-1.2.0.jar analyze <log or directory>
java -jar oba-mc1.21.1-1.2.0.jar mods <game directory>
java -jar oba-mc1.21.1-1.2.0.jar mcreator <project directory>
java -jar oba-mc1.21.1-1.2.0.jar watch <game directory>
java -jar oba-mc1.21.1-1.2.0.jar version
analyze with no argument reads logs/latest.log and crash-reports/ from the working directory.
Useful switches: --json for machine-readable output, --md for Markdown, --lang=zh|en, --game-dir
and --mods-dir, --pick to narrow the evidence to one mod, --menu for the interactive menu, and
--quiet to keep only the verdict. Output is coloured; --color=always|never|auto controls it, and
redirection turns colour off by itself.
Building
./gradlew jar # jar into dist/
pwsh tools/build-all.ps1 # every track
JDK 17 for 1.20.1 and the Forge 1.20.2~1.20.4 tracks, 21 for NeoForge 1.20.5+ and Forge 1.20.5+, 25 for
26.2. The first build downloads from maven.neoforged.net and maven.minecraftforge.net; behind a proxy
add systemProp.https.proxyHost/Port to gradle.properties.
Layout
engine/main/java/dev/oba/
├── core/ rules, attribution, reports, strings
├── mod/ in-game commands, chat layout, collector, companion launcher
├── cli/ command line entry point
├── companion/ the post-crash window (second JVM, same jar)
└── loader/ loader-agnostic interfaces
src/loader/{forge,neoforge,forge262}/ per-track adapters
versions/<track>/ self-contained build directory per track
Acknowledgements
- Special thanks to XinKo_TS for supporting and maintaining this mod
- McreatorDetector-1192 — Reference implementation of MCreator detection ideas
- Sinytra Connector — Bridge solution for Fabric mods on NeoForge
- NeoForged and MinecraftForge — Loader and event system
- Testers who provided real modpacks and crash log feedback
- Deepseek for multi-language translation
Known limits
- A crash before OBA is constructed (mod discovery or dependency checks) produces neither a window nor
a report; the next start readscrash-reports/and catches up. - Attribution needs frames or rules. An NPE whose four frames all sit in Minecraft’s own code gets
advice, not a mod name. - The watchdog depends on tick events. When neither
ServerTickEvent$PostnorTickEvent$ServerTickEventcan be found it logs one warning and disables hang detection; everything else keeps working. All nine tracks currently find one. - 26.1 and 26.3 are not packaged.
- A NeoForge 26.2 server logs its own appender errors (netty cannot find the kqueue/epoll native
library). OBA does not treat that as a crash, but the lines are in the log.
License
GPL-3.0-only
常见问题
One BUG Among Us 支持哪些 Minecraft 版本?
One BUG Among Us 支持 Minecraft Java 版 1.20.1–26.2,共 18 个版本,可用的加载器:Forge / NeoForge。下载时注意选择和你游戏版本一致的文件。
One BUG Among Us 怎么安装?
先用 PCL2 或 HMCL 启动器安装对应版本的 Forge / NeoForge,再把下载的 .jar 文件放进游戏目录的 mods 文件夹,重新启动游戏即可。开启了版本隔离的,mods 文件夹在 versions/版本名 下面。
评论
登录后评论和评分