A client-side utility mod for Minecraft 1.20.1, built with Kotlin and Java on the Fabric modding framework. This project demonstrates bytecode manipulation, event-driven architecture, real-time UI overlays, and modular software design.
Minecraft is a Java application, but its source code is obfuscated and not designed for extension. Modding requires:
- Deobfuscation mappings — Translating obfuscated names (
a,b,c) back to meaningful identifiers (PlayerEntity,render,health) - Bytecode manipulation — Injecting custom code into compiled
.classfiles at runtime using the SpongePowered Mixin framework - A mod loader — Fabric provides the toolchain for building, loading, and running mods
This is analogous to writing plugins for a closed-source application by patching its bytecode at load time.
flowchart TB
subgraph runtime [Minecraft Runtime]
direction TB
subgraph injection [Bytecode Injection Layer]
Mixins[Java Mixins]
MC[Minecraft Internals]
Mixins -->|"@Inject, @Redirect"| MC
end
Mixins -->|fires| Events
Events[Event System]
Events -->|notifies| Modules
subgraph Modules [Kotlin Modules]
KeyHud[KeyHudModule]
Balance[BalanceTracker]
AutoClick[AutoClicker]
More[...]
end
Modules -->|renders| UI[OWO Lib UI]
end
src/
├── client/
│ ├── kotlin/ # Business logic, UI, feature modules
│ │ └── dev/u9g/utils/client/
│ │ ├── modules/ # Feature modules (AutoClicker, KeyHud, etc.)
│ │ ├── events/ # Custom event definitions
│ │ ├── component/ # Reusable UI components
│ │ └── config/ # JSON-based configuration
│ ├── java/ # Mixins (must be Java for bytecode manipulation)
│ │ └── dev/u9g/utils/mixin/client/
│ └── resources/ # Assets, mixin config JSONs
└── main/
└── kotlin/ # Server-side code (minimal)
Mixins inject code into Minecraft's compiled classes at runtime. Example from ChatScreenMixin.java:
@Mixin(ChatScreen.class)
public abstract class ChatScreenMixin extends Screen {
@Shadow protected EditBox input;
@Inject(method = "init", at = @At("TAIL"))
private void moveInputBoxUp(CallbackInfo ci) {
this.input.setY(this.height - 12 - CHAT_Y_OFFSET);
}
@Inject(method = "render", at = @At("HEAD"), cancellable = true)
private void renderMoved(GuiGraphics graphics, int mouseX, int mouseY,
float partialTick, CallbackInfo ci) {
// Custom rendering logic...
ci.cancel(); // Prevent original method execution
}
}Key concepts:
@Mixin— Target class to modify@Shadow— Access private fields from the target@Inject— Insert code at specific points (HEAD,TAIL,RETURN)@Accessor— Generate getters/setters for private fields
Custom events follow the Fabric EventFactory pattern:
// Event definition
object GuiKeyPressedCallback {
val EVENT: Event<GuiKeyPressedCallback> = EventFactory.createArrayBacked(...)
}
// Registration (in a module)
GuiKeyPressedCallback.EVENT.register { screen, keyCode, scanCode, modifiers ->
// Handle key press
}Modules are initialized in shuffled order to prevent implicit cyclic dependencies:
listOf(
BalanceTrackerModule::init,
ChatScreenOverlayModule::init,
AutoclickerModule::init,
// ...
).shuffled().forEach { it() }Using OWO Lib's fluent builder API:
Containers.grid(Sizing.content(), Sizing.content(), 3, 3)
.child(createButton("Up"), row = 0, col = 1)
.child(createButton("Left"), row = 1, col = 0)
.child(createButton("Down"), row = 1, col = 1)
.child(createButton("Right"), row = 1, col = 2)
.positioning(Positioning.relative(75, 25))
.surface(Surface.VANILLA_TRANSLUCENT)The mod parses game state from multiple sources:
- Scoreboard sidebar — Currency balances, game mode detection
- Chat messages — Progress tracking via regex pattern matching
- Entity data — Health values, names for targeting logic
val BALANCE_REGEX = "\\| ((?:[\\d.]+)(?:E\\d+)|(?:[a-zA-Z]*)) (.+)".toRegex()
fun parseBalances(lines: List<Component>): Map<String, String> {
return lines.mapNotNull { line ->
BALANCE_REGEX.matchEntire(normalizeText(line.string))?.let { match ->
val (amount, currency) = match.destructured
currency to amount
}
}.toMap()
}"Gen 2" modules implement the Reloadable interface for runtime reinitialization:
interface Reloadable {
fun init(): () -> Unit // Returns cleanup function
}
// Usage
val gen2Modules = listOf(KeyHudModule, AutoMoveModule)
gen2Modules.forEach { cleanup.add(it.init()) }
// On screen change, reinitialize
OpenScreen.EVENT.register { _, _ ->
cleanup.forEach { it() }
cleanup.clear()
gen2Modules.forEach { cleanup.add(it.init()) }
}| Module | Description |
|---|---|
| KeyHudModule | On-screen HUD showing pressed keys (WASD, mouse, sprint) |
| AutoclickerModule | Automated input with configurable CPS and target filtering |
| BalanceTrackerModule | Parses scoreboard for currency tracking, detects game mode |
| ChatScreenOverlayModule | Command button grid + progress sidebar overlay |
| HighlightItemsInGuiModule | Visual item replacement in inventory UIs |
| KillHistoryModule | Tracks combat statistics |
| Technology | Purpose |
|---|---|
| Kotlin 2.3 | Primary language for business logic |
| Java 21 | Required for Mixin classes |
| Fabric Loader | Mod loading framework |
| Fabric API | Standard hooks and events |
| SpongePowered Mixin | Bytecode transformation |
| OWO Lib | Declarative UI components |
| Baritone | Pathfinding integration |
| Gradle + Fabric Loom | Build toolchain with remapping |
| GSON | JSON configuration persistence |
# Build the mod JAR
./gradlew build
# Output: build/libs/Utils-1.0-SNAPSHOT.jar
# Run Minecraft with the mod (development)
./gradlew runClientRequires Java 21. The build uses Fabric Loom, which handles deobfuscation mappings and JAR remapping automatically.
All Rights Reserved