Client-side coordinate disguise for Fabric 1.21.4 and 1.21.8.
A screenshot of your base is a search query. Several things in the frame are derived from where you are standing, some from the world seed and some from nothing but the coordinate itself, and each of them can be run backwards. That is how bases get found.
Hide Me re-draws every one of them, on your client only, from a secret that exists nowhere but your config file. What you see is a genuine sample of the same distributions Minecraft draws from, so it reads as an ordinary world and survives inspection, but it corresponds to no real location, so searching for it finds nothing.
| Derived from | Fixed by | |
|---|---|---|
| Block rotations | the coordinate alone | re-keying the rendering seed |
| Plant jitter | the coordinate alone | re-keying the model offset |
| Cloud phase | the coordinate alone | sliding the cloud origin |
| Bedrock | the world seed | re-rolling the gradient layers |
| F3 coordinates | nothing, it just says them | scrambling every reading |
The first three are the reason this mod is not called Bedrock Shroud any more.
Bedrock gives a base away only on a server whose seed has been cracked. Block rotation does not need a seed at all.
Vanilla picks the rotation of a stone block from MathHelper.hashCode(x, y, z):
long l = (x * 3129871) ^ (z * 116129781L) ^ y;
l = l * l * 42317861L + l * 11L;
return l >> 16;There is no world seed anywhere in that. The block at a given coordinate wears the same rotation in
every world that has ever existed, so a screenshot can be searched against the function directly with
nothing known about the server at all. Thirty-four vanilla blockstates ship a random variant list,
including stone, deepslate, dirt, grass_block, sand, sculk and every concrete powder;
netherrack ships sixteen variants rather than four. Each visible face is worth about two bits, so
roughly thirty blocks over-determines a position in a world thirty million blocks across. A wall of
stone in frame is more than enough. So is a roof of grass.
Worth stating precisely, because it decides how visible any of this is. Across all fifty-six random variant lists in 1.21.8, not one picks between different textures. Every choice is a rotation or a mirror of a single model with a single texture set. Vanilla has no "this block has four different looks" feature; it has "this block's one texture can be laid on four ways".
Deepslate is the block that most looks like a counter-example and is not one. It has two textures,
deepslate_top and deepslate, but that is the cube_column model rather than a random choice:
every deepslate block has both, and which faces show which is fixed by its axis property, a real
blockstate the server sends. What varies at random is the mirror and the 180° turn of the streaked
side texture — and that is exactly what gets re-keyed.
It does mean deepslate is the easiest block to see this on, because its side texture has strong directional grain where stone is undirected noise.
Two blockstates ship duplicate entries, which is vanilla's quirk rather than this mod's: deepslate
and infested_deepslate at axis=x list four variants of which only two are distinct, so those are
worth one bit rather than two. Naturally generated deepslate is axis=y, where all four differ.
It also punched a hole straight through the bedrock disguise. The chunk mesher asks this mod what block to draw, and then asks vanilla for that block's rotation using the real position, so every piece of deepslate drawn over a real bedrock block was still wearing the true grain of the coordinate it stood at. The bedrock disguise was not wrong; it was being read around.
Plant jitter is the same family and is worse per block. The shove vanilla gives grass, ferns,
flowers and saplings inside their block is hashCode(x, 0, z), with the Y term deliberately zeroed:
four bits pick the X offset and four more pick the Z, and the answer is identical the whole way down
the column. That is eight bits per column from a flowerbed, against two per block from a wall.
Clouds are the mild one and are nearly free to close. The cloud layer is one texture tiled twelve
blocks to the pixel, anchored to the world origin rather than to anything about the world, so with
vanilla's 256×256 clouds.png it repeats every 3072 blocks and is identical in every save. A
recognisable cloud edge in an outdoor screenshot fixes x and z modulo 3072, from a part of the frame
nobody thinks to crop.
All three are re-keyed rather than removed. The obvious alternative, and what the resource pack doing the rounds does, is to make the tells uniform: same rotation everywhere, or stone retextured to something rotationally symmetric. That works, and it announces itself in every screenshot, and it makes the game look like a bug report.
Re-keying is invisible instead. Measured over 26.2M positions through the real LocalRandom path
and the shipping Mix.rekey:
| variant 0 | 1 | 2 | 3 | |
|---|---|---|---|---|
| vanilla | 24.996% | 25.001% | 24.984% | 25.019% |
| re-keyed | 24.988% | 24.994% | 25.005% | 25.013% |
Netherrack's sixteen variants come out within 0.020 points of 6.25% each. So the disguised world is not merely plausible; it is drawn from the same distribution, and no count of rotations separates the two.
Two further checks, because matching a histogram is easy and matching noise is not. Changing a single bit of the key makes the two patterns agree on 25.008% of positions, which is what independent draws do. And two blocks one step apart in x agree on 25.001%, so the re-key introduces no structure along an axis for anyone to correlate against.
The cloud offset is a whole number of cloud cells, which matters for the same reason. Every cloud edge in an untouched client lands on a multiple of twelve blocks, so an offset that was not one would put every cloud edge in the sky at a coordinate vanilla could not produce. That is a visible tell rather than a statistical one, and a worse giveaway than the phase it was hiding.
AbstractBlockState.getRenderingSeed and getModelOffset are where all of this converges.
SectionBuilder, BlockRenderManager, the falling-block and piston renderers, and Sodium's own
BlockRenderer all reach them rather than reimplementing them; verified against Sodium 0.6.13 by
searching its classes for the intermediary names method_26190 and method_26226, which turn up in
render.chunk.compile.pipeline.BlockRenderer. So unlike the bedrock disguise, which needs a second
hook for Sodium's LevelSlice, this needs nothing renderer-specific and gets Iris for free.
Hooking the seed rather than a list of blocks also means every randomly-rotated block is covered, including ones other mods add and ones Mojang has not written yet.
Vanilla's answer is re-keyed, not replaced, because some blocks override it. A bed, a door and a tall flower each return the seed of a single half for both halves so the two pieces agree on a variant. Feeding a value they already agreed on through the same key keeps them agreeing; hashing the position instead would give the two halves of every door in the world different grain.
Bamboo keeps vanilla's jitter, because BambooBlock.getCollisionShape is derived from the model
offset. Moving it would give the client a hitbox the server does not have, and walking into it would
produce exactly the rubber-banding this mod refuses to cause. Pointed dripstone is excluded for a
milder reason: its collision shape does not follow the offset, so shifting the model alone would
leave the spike visibly beside the thing that hurts you. Everything else with an offset — grass,
ferns, flowers, saplings, mushrooms — has no collision at all, and its outline shape follows the same
offset, so the selection box stays where the flower is.
Vanilla places bedrock on a vertical gradient: a straight line of probability across five blocks, sampled once per position. At the Overworld floor the chance runs 1.0, 0.8, 0.6, 0.4, 0.2 going up from Y -64, hitting zero at Y -59. The Nether roof is the same line inverted, and the Nether floor matches the Overworld's. That is the whole rule, and it is why the bottom layer is solid and the four above it look speckled.
Hide Me re-rolls exactly those four layers per band, using the same probabilities, keyed by
SplitMix64 over (secret, dimension, x, y, z). The always-solid layer is left alone, since it is
bedrock everywhere in the world and carries no information.
Measured over 1.44M columns per layer, the output matches vanilla's frequencies to within about a part in a thousand, and two different secrets produce statistically independent patterns.
Re-rolling a layer means some real bedrock has to be drawn as something else, and choosing that something else turns out to be the subtle part.
The bedrock band is not uniformly deepslate. Tuff blobs run through it, iron ore veins carry their own tuff, and the Nether floor is basalt and blackstone wherever a delta sits on it. Replace bedrock with a single hardcoded stone per dimension and a bedrock block inside a tuff blob becomes one deepslate block surrounded by tuff, a block that could not have generated there. Every one of those is a signpost reading real bedrock was here, and it defeats every statistical argument above, because it is not a statistical tell at all. It is a visible mistake, and it hands back exactly the coordinates the mod exists to hide.
So the replacement is read out of the world instead: the commonest natural stone among the twenty-six blocks touching the position is what it becomes. The same reasoning runs the other way, so tuff is allowed to become bedrock. Otherwise synthetic bedrock could never appear inside a tuff blob and the blob's outline would read as a hole in the disguise.
Both renderers snapshot a 3×3×3 block of chunk sections, so a one-block step is always inside the snapshot, including from the margin the mesher reads for face culling. That also keeps the answer stable: two sections meshing the same boundary position see the same neighbours, so a block does not change appearance depending on which section was built last.
There is one residual case. If every one of the twenty-six neighbours is itself bedrock, there is nothing to read and the dimension's default stone is used. On the densest re-rolled layer, where four blocks in five are bedrock, that happens to well under one position in a hundred, and only produces a wrong block at all if that position also sits inside a patch of something other than the default.
Ores are never overwritten. Doing so would hide something worth seeing and would be the one change here a server could fairly call cheating; vanilla never generates ore inside bedrock either, so excluding them costs no realism.
Every other part of this mod exists because a screenshot leaks coordinates indirectly and has to be argued about statistically. F3 leaks them directly, in decimal, in the top-left corner. Any amount of care taken over the bedrock is wasted if the same frame has the answer written on it.
XYZ, Block, Chunk, Chunk-relative, Targeted Block and Targeted Fluid are rewritten.
Biome and Local Difficulty are redacted: a rare biome cuts a seed search down to a handful of
candidate regions, and local difficulty carries the chunk's inhabited time, so a high value says
"somebody lives here" even with every coordinate on screen removed.
The obvious design is a fixed secret offset: shift every reading by the same (dx, dy, dz) so
differences stay correct and the debug screen is still usable for navigation. That was considered and
rejected, because it fails in one direction only and it fails retroactively. The moment a single
screenshot is taken somewhere whose real coordinates are known — spawn, a public warp, anything a
viewer can recognise — the offset falls out of one subtraction and every screenshot ever posted
becomes readable at once.
So each position is scrambled independently through the key instead. There is nothing to subtract, no relationship between two readings to exploit, and one recognised landmark tells an attacker only that one landmark's fake coordinates.
The cost is real and is not hidden: walking one block changes the reading completely, so F3 cannot be navigated by. Hold the reveal key when you need the truth. That is the same bargain the bedrock disguise already makes.
The scramble is a function of the position rather than of the clock, so standing still shows a steady number instead of a flickering one. A reading that changed every frame would be unusable to look at, would obviously be a mod at a glance, and would leak the real position anyway to anyone who averaged a few seconds of it. The fake coordinates land inside a range a real player could be standing in, and the fake Y inside the dimension's actual build range, because an obviously impossible reading tells a viewer the numbers are disguised and sends them looking for another tell. A plausible one sends them digging somewhere else.
There is a backstop for lines this mod has never heard of, which is to say lines from another mod or from a Minecraft version newer than the list above: anything quoting one of the real coordinates as a standalone number is dropped. It occasionally costs an unrelated line whose number happens to collide, and that is the right way round. A redacted line about frame times is a nuisance; a leaked one is a base.
Hide Me is public, so the algorithm is assumed known and all of the secrecy lives in the key. That
makes its width the entire security margin, and the attack is worth being exact about: bedrock layers
are not fair coins (they run 0.8/0.6/0.4/0.2), so a screenshot leaks roughly 3.4 bits per column and
about nineteen columns is enough to confirm or reject a guessed key. Verification is therefore cheap
and the only real cost is the search. At 64 bits that is a few GPU-weeks for someone motivated, so the
key is 128 bits, generated per-install from SecureRandom on first launch.
The two halves are injected at different points of the hash chain rather than combined first. That detail is load-bearing: folding them into one 64-bit value before the position enters would leave a single intermediate state determining every output, and an attacker would brute-force that instead, putting the real strength back at 64 bits however long the stored key was. Verified by flipping a single bit of either half and confirming the pattern fully decorrelates.
Do not share config/hideme.json, and never ship one in a modpack. Anyone holding those two
numbers can reproduce every pattern you see and work out which bedrock in your screenshots was real.
A pack shipping a hideme.json would give all its users the same key, and breaking one would break
every one of them.
The disguise is shown to the things that draw and to nothing else. Rotations and jitter are choices about which of several identical-geometry models to use and where to put a plant inside its own block; clouds are scenery; bedrock is swapped only inside the immutable snapshot the chunk mesher builds geometry from.
- Collision, ray tracing, block breaking and sounds all use the real
ClientWorld. - Every packet the client sends is built from the real world, so no packet differs by a single byte from an unmodified client's. There is no desync to detect, because there is no desync. Only the picture differs, not the client's model of the world.
- Only bedrock and the natural stone of the band are ever exchanged, and only for each other. Air stays air, so player-dug holes in the Nether roof survive. Ores, fluids and placed blocks are untouched.
- Every block involved is an opaque full cube, so face culling, occlusion and lighting are identical. The mesh built from the disguise is structurally the same as the one built from the truth: no extra geometry, no relighting, no additional rebuilds.
- F3 is a client-side panel with no server in it at all.
Cost is one extra hash per block state the mesher asks for; only the ~1024 positions per chunk inside the bedrock bands are hashed for bedrock, and only the bedrock among those pays for a neighbourhood sample.
Particles are not server-side, because the server sends events rather than particles, so left alone they quietly report the truth the renderer is hiding. Mining a bedrock block drawn as deepslate throws off bedrock-black specks against a grey block, and a screenshot of that gives up the position as plainly as the bedrock would have. One block rather than a field of them, but the same category of leak.
Both paths are therefore disguised too:
- The crack particles thrown off while mining read
ClientWorld.getBlockStateat the top ofParticleManager.addBlockBreakingParticles, with no server involvement at any point. - The puff when a block actually breaks is the reverse case, and the one with a server in it: world event 2001 carries the broken block's state id, so breaking real deepslate drawn as bedrock would puff grey against a black block. The packet cannot be changed and does not need to be, since the client decodes it and spawns the particles itself.
This costs none of the guarantees above. A particle is decoration: nothing collides with it, nothing reads it back, and nothing about it reaches the server, so the client's model of the world is still untouched and there is still nothing to desync.
What it does not fix is that bedrock drawn as deepslate will not break and the mining overlay will not advance, however the particles look. Fixing that would mean lying to the interaction manager, which would mean invalid packets and a real desync, the one thing this mod refuses to do. The reveal key is the answer, and it is what it is for.
There is deliberately no "disguise active" badge. It would be captured by the very screenshots the disguise exists to make safe, and it would tell anyone looking that the world in frame is not to be trusted. An attacker who knows to discard the bedrock and the block grain simply goes looking for another tell instead, so the badge would cost protection and buy nothing.
The mirror image of that is worth having, for the same reason. While the reveal key is showing the true world, an amber glow breathes around the border of the frame with REVEALING TRUE WORLD above the crosshair. That is the one state where a screenshot is genuinely dangerous, and in every other state nothing is drawn, so a safe screenshot stays clean and carries no hint that the mod is installed.
Between them those two behaviours answer "is this real or fake" completely. A glow means the world in front of you is real; no glow means it is not. The absence is the other half of the signal, which is why it does not need one of its own.
It is a border rather than a banner because a banner is the wrong shape for the job twice over. It sits exactly where a player is looking, so it is in the way during the work the reveal key exists to allow; and being in the way is what makes it easy to stop seeing, which is a poor property for the one warning that has to be noticed. A border covers nothing, cannot be mistaken for part of the game, and stays in peripheral vision the whole time. The slow pulse is deliberate for the same reason: a static overlay disappears from attention within a minute and a moving one does not.
It matters most in toggle mode, where reveal stays on until the key is pressed again. Without it there is nothing at all to distinguish a revealed world from a shrouded one, and the failure is silent and one-directional: the screenshot is already taken and posted by the time it is noticed.
Holding the key turns off the bedrock disguise, the F3 scramble, the block grain, the plant jitter and the cloud offset, all of it.
Grain used to be excluded, on the reasoning that nobody navigates by block rotation so nobody needed the true one, and that re-meshing the view on every press of a held key was too high a price. That was wrong, and it is worth recording why.
A key called reveal has to reveal everything, because its real job is not navigation. It is the only way a player can ever check that the disguise is doing something, and a mod whose entire design rests on "you cannot see the difference" must leave exactly one way to see the difference. With grain excluded, holding the key showed identical rotations either way, which reads precisely like a mod that is not working. Being unverifiable costs far more than a repaint does.
The repaint is cheaper than it was, too. It schedules the sections in view through
scheduleChunkRenders rather than calling WorldRenderer.reload(), which is what it used to do.
reload() tears the renderer down and rebuilds it, dropping chunk storage, render lists and Sodium's
buffers, none of which have changed; scheduling simply puts the sections back in the rebuild queue,
which is the only work actually needed. That is the difference between reveal being usable on a held
key and not.
Supported. Sodium replaces vanilla's chunk mesher outright. It never builds a ChunkRendererRegion,
it copies each section into its own LevelSlice, so the bedrock disguise needs a second hook on
LevelSlice.getBlockState(int, int, int).
That method takes global block coordinates and is what Sodium's meshing task calls directly;
getBlockState(BlockPos) does nothing but unpack the position and delegate to it, so one hook covers
both. Verified identical in Sodium 0.6.13 and 0.7.3.
Hide Me does not compile against Sodium. The handler needs no Sodium type, only a block state and
three ints, so the mixin targets the class by name with remap = false, and a config plugin skips it
entirely when Sodium is not installed. One jar, either renderer, no pinned Sodium version. The
neighbourhood read goes through BlockRenderView, a Minecraft interface LevelSlice implements,
rather than a @Shadow of Sodium's own method. Both reach the same code, but an interface cast
cannot fail to resolve against a Sodium that has moved on.
The grain disguise needs none of this, because Sodium calls vanilla's AbstractBlockState methods.
Repainting after a settings change works too, though not obviously. MaskSession.refresh() calls
WorldRenderer.scheduleChunkRenders, which Sodium does not intercept, but that method is only a
loop, and the scheduleChunkRender(x, y, z, important) it calls for each section is overwritten,
straight into SodiumWorldRenderer.scheduleRebuildForChunk. So the request lands in Sodium's
rebuild queue without this mod needing to know Sodium exists. Checked against the 0.6.13 jar; it is
worth re-checking on a Sodium update, because nothing about it would fail loudly.
A grain change is a different matter: it moves something in every section in view, so there is no
smaller unit to invalidate and it calls WorldRenderer.reload(), the same work F3+A does. That only
happens on a settings change, which is why it is affordable.
The failure mode that matters is not a crash. It is a hook silently ceasing to match after an update, leaving the mod installed, configured, reporting itself as on, and doing nothing.
So the settings screen reports observation, not configuration: flags set by the injection
handlers the first time they actually rewrite something. The grain reading is the one to watch,
because that hook fires for every block in view — if it has not gone green within a few seconds of
loading a world, it has stopped matching. The bedrock reading only confirms once you are down at the
layers, because the hook never fires anywhere else, so On, not yet confirmed on the surface is
normal rather than a fault.
- Reveal key (default
B): shows the true bedrock and the true coordinates while held, for when you are actually working down there. Switchable to a toggle in settings, and rebindable either from the settings screen or from the vanilla controls screen. The settings row turns red if something else is already using the key. - New key: throws away the secret and re-draws everything. Worth doing if you think old screenshots have been analysed, but it will not match anything you posted before.
- Settings open from the kUI hub if kUI is installed, or from a second keybind, unbound by default, if it is not.
Both keys live under their own Hide Me heading in the controls screen rather than in misc, which is where a player with thirty mods installed goes to give up.
kUI is genuinely optional here: it is a suggests, not a depends. The disguise is a set of render
hooks and the settings screen is plain vanilla widgets, so nothing is switched off without it and
there is no "please install kUI" notice. It only adds the hub entry. That matters more than it sounds,
because the status line above is the only way to confirm the mod is doing anything, so it must not be
the part that goes missing when a library is absent.
The leaks above are the ones derived from position. They are not the only way a screenshot can be searched.
- Terrain itself. A distinctive ridge, a ravine, a structure or a biome edge is still searchable against a cracked seed, including the shape of a tuff blob in the bedrock layer, which is seed-derived and which this mod deliberately leaves alone.
- Sound. Still honest, and the remaining tell of this kind. World event 2001 carries the broken block's state and the break sound is taken from it, while hit and step sounds come from the real block, and bedrock, deepslate, tuff and netherrack all have distinct sound groups. It is fixable at the same points the particles were, but audio does not appear in a screenshot, so it has been left for now.
- Other mods' screen furniture. A minimap, a waypoint list or a coordinate HUD from another mod says exactly where you are, and Hide Me does not reach into them. The debug screen backstop catches extra lines added to F3, and nothing more than that.
- Chunk borders (F3+G) show x and z modulo 16, but you have to switch them on yourself.
One source tree, several Minecraft versions. Everything that differs lives in
gradle/targets/<version>.properties:
./gradlew build # the default target, 1.21.8
./gradlew build -Pmc=1.21.4
There is no Stonecutter and no second source set, because after the portability work no source differs between targets. The two places that used to were removed rather than branched:
- The reveal overlay was registered through Fabric's
HudElementRegistry, which does not exist before 1.21.6. It is now a mixin onInGameHud.render, which has been the same method with the same signature across every target. That is also the safer arrangement, and the reason is the usual one: had the Fabric API moved underneath it, the registration would have quietly stopped drawing, and the failure would have been the reveal warning silently disappearing while the reveal key still worked. A required mixin crashes on launch instead, which is the better of the two outcomes by a wide margin. - The settings screen scaled its status note through
DrawContext.getMatrices(), which changed fromMatrixStacktoMatrix3x2fStackin 1.21.6. The two-column card has room for the note unscaled, so the simpler drawing is also the portable one.
What remains is one dependency and one file. kUI does not publish for every target, so where it is
absent the build drops HideMeKui.java and HideMeClient reaches it reflectively — which is why
that call is reflective at all. Nothing else changes: the settings screen is vanilla widgets and
opens from its own keybind, so a target without kUI loses only the entry in the shared hub.
CloudRenderer.renderClouds takes two extra Matrix4f arguments on 1.21.4 than on 1.21.8. That
costs nothing, because the mixin names the method and Loom fills in the descriptor from whichever
target's mappings it is building against.
Verify the injection points against it first; do not merely drop a properties file in. The hooks, by Fabric intermediary name:
method_26190 |
getRenderingSeed — rotations |
method_26226 |
getModelOffset — plant jitter |
method_62168 |
renderClouds |
method_1835 / method_1839 |
the debug screen's two columns |
method_1753 |
InGameHud.render — the reveal overlay |
method_12254 |
BlockRenderView.getBlockState — bedrock |
method_3054 |
block breaking particles |
The first two and the last two exist unchanged from 1.20.1 to 1.21.11. renderClouds starts at
1.21.4. drawText (method_51745) is the debug-screen hook and exists across the whole range.
The hooks are solved; the remaining blocker is elsewhere.
1.21.9 rebuilt the debug screen as a registry of entry classes under client/gui/hud/debug/, which
removed getLeftText and getRightText. DebugHudMixin now hooks DebugHud.drawText instead —
the single private method both columns pass through, unchanged since 1.21.4 — so that break is
closed, and the hook is better than the one it replaced because everything drawn goes through it,
including lines added by other mods.
The same release moved the block-break puff from ParticleManager to
World.addBlockBreakParticles and restructured the mining crack particles away.
ParticleMixinPlugin picks the right hook per version and reports the crack particles as uncovered
on 1.21.9+ rather than pretending otherwise.
What still blocks it is ordinary API drift with nothing to do with the disguise, in exactly two files:
| change | used by |
|---|---|
KeyBinding's category is a KeyBinding.Category, not a String |
HideMeClient |
Screen.keyPressed(KeyInput) replaces keyPressed(int, int, int) |
SettingsScreen |
InputUtil.fromKeyCode(KeyInput) replaces fromKeyCode(int, int) |
SettingsScreen |
Two ways out. Extract the keybind registration and the rebinding handler into small shims and give those a per-version source directory, which is where a tool like Stonecutter finally earns its keep; or drop the in-screen rebinding entirely and send players to the vanilla controls screen, which removes the divergence at the cost of a convenience.
The mixin configs are required with defaultRequire of 1, so a hook that moved takes the game
down on launch rather than leaving a player unprotected. That covers a method that vanished. It does
not cover one that was renamed and its name reused for something else, which is the case to check by
hand.
./gradlew build
Requires JDK 21. kUI is compiled against but not bundled, so for the targets that use it, it must be
published to Maven Local (./gradlew publishAllToMavenLocal in the UI project) for the build to
resolve. The resulting jar runs with or without it.