How I Broke Hide-and-Seek (And Then Fixed It With 13 Lines of Java)
A Minecraft modding origin story nobody requested.
Let me set the scene.
It's a Friday night. Four friends, one LAN server, and the collective decision to play hide-and-seek in Minecraft. Beautiful. Wholesome. The kind of thing that makes you forget you're an adult with deadlines. Someone builds a map. Someone writes the rules. Someone hides behind a waterfall and feels genuinely clever about it.
Then somebody looks up.
Floating above every player's head, in crisp white text, rendered through walls, through mountains, through the entire tectonic geology of the Overworld — a nametag. Player185 is behind the waterfall. Player694 is in the basement. Player39 is standing in a hole pretending to be a cactus.
Hide-and-seek doesn't work when the game draws an arrow over everyone's head.
The "I'll Just Make a Mod" Phase
Now, a normal person would search for an existing mod that hides nametags. And I did. Briefly. There were a few for older versions — 1.19 and below — but nothing current for 1.21 on Fabric. The options were either outdated, Forge-only, or doing twelve other things I didn't need.
So naturally I thought: how hard can it be?
Famous last words. Tattooed on the headstone of every programmer who's ever opened a new project at 11 PM.
The timing, though, was perfect. I'd just started university in 2024, and my first module was Java. I could study for my degree and build a passion project in the same language. Two birds, one compiler. The stars had aligned.
First Contact With Fabric
If you've never modded Minecraft before, here's what the ecosystem looks like from the outside: a loose constellation of APIs, build tools, mapping layers, and community wikis that all assume you already know what a Mixin is.
I did not know what a Mixin was.
Fabric's documentation is good — genuinely good — but it's written for people who've already crossed the mental gap between "I write Java" and "I inject bytecode into a game engine at runtime." That gap is wider than it looks.
The setup alone took longer than I'd like to admit. JDK 21, Gradle 8.14, Fabric Loom, yarn mappings, a fabric.mod.json that needed to be exactly right or the entire thing would silently refuse to launch. At one point I deleted my .gradle folder, my .idea folder, my will to live, and reimported the project from scratch.
It worked.
The Actual Problem
Here's the thing I didn't know going in: Minecraft doesn't have a toggle for nametag visibility. There's no config option. No game rule. No event hook in Fabric's API that says "hey, a nametag is about to render, want to cancel it?"
The nametag is drawn inside a method called renderLabelIfPresent() on EntityRenderer — a single method responsible for rendering the floating text above every entity in the game. Villagers, mobs, item frames, armour stands, players — everything goes through it.
You can't override the method. You can't extend the class cleanly. And Fabric, despite being excellent at most things, doesn't ship a nametag-specific event.
Which brings us to Mixin.
SpongePowered Mixin, or: How I Learned to Stop Worrying and Inject Bytecode
Mixin is a framework that lets you modify Minecraft's compiled classes at launch time. Not by copying the source and editing it — by injecting new instructions directly into the existing bytecode. You pick an injection point, write your hook, and the framework weaves it in before the game starts.
It's powerful. It's surgical. It's also the kind of thing where a missing annotation parameter sends you on a two-hour debugging session because the error message says Mixin apply failed and nothing else.
After a lot of reading, a lot of trial and error, and one genuinely embarrassing moment where I realised I'd been targeting the wrong method signature for an entire evening, I got it working.
Here's the whole thing:
@Mixin(EntityRenderer.class)
public abstract class EntityRendererMixin {
@Inject(
method = "renderLabelIfPresent",
at = @At("HEAD"),
cancellable = true
)
private void hidePlayerNametags(
Entity entity, Text text, MatrixStack matrices,
VertexConsumerProvider vertexConsumers,
int light, float distance, CallbackInfo ci
) {
if (entity instanceof PlayerEntity) {
ci.cancel();
}
}
}
Thirteen lines. The @Inject fires at the HEAD of the method — before any original code runs. If the entity is a PlayerEntity, ci.cancel() kills the call entirely. No label computation. No vertex buffer writes. No draw call. Mob names, item labels, everything else renders as normal.
I stared at it for about ten minutes after it first worked. Not because it was broken — because it wasn't. The nametags were just... gone. All that setup, all that learning, and the actual fix was an instanceof check and a single method call.
The "Wait, That's It?" Moment
Every programmer knows this feeling. You spend days — weeks — climbing toward what feels like an impossibly complex problem, and then the solution turns out to be embarrassingly small. Not because the problem was easy, but because you did the hard part already: understanding the system well enough to know where to cut.
The mod is seven files. One mixin class, one client initialiser, one mod entrypoint, two JSON config files, a build script, and a licence. That's the entire project. It loads in milliseconds, adds zero overhead to the render loop (cancelled calls don't compute), and works on every server without server-side installation.
Shipping It
I put it on Modrinth and GitHub. Added a CI pipeline — every push triggers a GitHub Actions build on Ubuntu 24.04 with JDK validation, Gradle wrapper verification, and artifact capture. Nothing merges without a green build. Even for a thirteen-line mod, because habits matter more than scale.
The README includes a troubleshooting section, because I know exactly how many ways the Fabric dev environment can break and I don't want anyone else deleting their .gradle folder at midnight wondering where it all went wrong.
The Pause Button
Here's the honest bit.
My university course moved from Java to C#. And honestly? I prefer it. The workload is getting heavier, and splitting my head between two languages, two ecosystems, two entirely different ways of thinking about object lifetime — it wasn't sustainable. I was getting confused. Not in the fun "learning new things" way, but in the "accidentally writing Java syntax in a C# file and wondering why Rider is screaming at me" way.
So the mod is on pause until roughly 2027/2028. The codebase is open, documented, and MIT-licenced. The roadmap — command toggles for server operators, friend/team visibility lists, version ports from 1.20 upward — is still the plan. It's just a plan with a longer timeline.
What I Actually Learned
This was my first Fabric mod. My first real encounter with bytecode injection. My first time publishing something to a package registry that strangers might actually download and install into their game.
The technical takeaway is narrow: Mixin injection is the correct tool when the framework doesn't expose an event hook, and understanding where to inject matters more than how much code you inject.
The bigger takeaway is broader: small projects teach you more than you'd expect, specifically because they're small. There's nowhere to hide behind complexity. Either the architecture is right or it isn't. Either the mod works or it doesn't. Thirteen lines means thirteen lines of accountability.