Porting c0.0.13a_03 to Rust: Through the Obfuscation Wall
In which the class names disappear, water fills the valleys, and bedrock seals the floor.
This is version 7 of 878. The decompiler still works. The output just stopped making sense.
The Shape of This Version
c0.0.13a_03, released May 21, 2009, is the first obfuscated Minecraft build. Every class is a single letter. Every method is a single letter. Every field is a single letter. The decompiler produces syntactically valid Java that reads like someone typing with their eyes closed: a.a(b.c, d.e, f) where you expect level.setTile(x, y, z, blockId).
The obfuscation is ProGuard, the Java obfuscation tool that Notch adopted sometime between c0.0.11a and this version. Its purpose was practical — smaller JARs meant faster applet load times over 2009 internet connections — but for us, trying to analyze the game seventeen years later, it means every class must be identified by structure rather than name.
The deobfuscation process is detective work. A class with width, height, depth fields and a method that writes bytes through GZIPOutputStream is Level. A class that allocates a FloatBuffer and counts vertices is the Tesselator. A class with a run() method that generates Perlin noise and fills a block array is LevelGen. You match shapes, not names. It adds no technical difficulty to the port — the code is the same code once identified — but it adds substantial analytical overhead. Every method call must be traced to its definition. Every field access must be mapped to a semantic name. The deobfuscation is a prerequisite to analysis, and analysis is a prerequisite to porting.
Once the obfuscation fog cleared, the actual delta from c0.0.11a was visible: liquids, bedrock, improved terrain generation, a GUI system, and fog effects. The port focuses on the gameplay-relevant changes. The GUI system (pause menu, save/load screens) and server-based level persistence are deferred — substantial infrastructure that does not affect core gameplay.
Liquid Blocks: Water and Lava
The headline feature is proper liquid support. Four new block IDs join the tile registry:
- Flowing water (ID 8, texture index 14)
- Still water (ID 9, texture index 14)
- Flowing lava (ID 10, texture index 30)
- Still lava (ID 11, texture index 30)
The tile property system gained two new functions to support these blocks:
pub fn tile_is_liquid(tile_id: u8) -> bool {
matches!(tile_id, TILE_FLOWING_WATER | TILE_STILL_WATER
| TILE_FLOWING_LAVA | TILE_STILL_LAVA)
}
pub fn tile_liquid_type(tile_id: u8) -> i32 {
match tile_id {
TILE_FLOWING_WATER | TILE_STILL_WATER => 1,
TILE_FLOWING_LAVA | TILE_STILL_LAVA => 2,
_ => 0,
}
}
Liquids are non-solid (entities pass through them), do not block light (light propagates through water), and are not opaque for face culling purposes. But they differ from air — they occupy a cell, have a texture, and affect entity behavior. The allow-list architecture from rd-160052 handles this automatically: liquid blocks are not in the tile_is_solid() or tile_blocks_light() allow-lists, so they are correctly treated as non-solid and non-light-blocking without any special cases.
The flowing/still distinction exists in the block IDs but is simplified in this port. Full liquid flow mechanics — where flowing water spreads to adjacent cells seeking the lowest available path — are complex and not fully exercised in c0.0.13a_03’s single-player gameplay. The terrain generator places water and lava as still blocks. Both flowing and still variants render identically. The flow simulation is deferred until a version where it becomes gameplay-relevant, tracked in the gap analysis as DEFERRED, not SKIPPED.
Liquid Rendering: The Third Mode
The chunk mesher now handles three distinct rendering modes, dispatched by tile type: solid blocks (standard cube with face culling against opaque neighbors), bush X-crosses (two intersecting quads at 45 degrees), and now liquid blocks. Each mode has its own geometry generation logic within the same build_chunk_mesh function.
Liquid rendering broke the solid-block assumptions in three ways.
Lowered top face. The top surface of a liquid block sits at y + 0.9, not y + 1.0. In the chunk mesher:
let top_h = 0.9f32;
// ...
push_quad(&mut vertices,
[xf + 1.0, yf + top_h, zf + 1.0], [xf + 1.0, yf + top_h, zf],
[xf, yf + top_h, zf], [xf, yf + top_h, zf + 1.0],
[u1, v1], [u1, v0], [u0, v0], [u0, v1], br);
This 0.1-unit recession transforms the visual quality of water. A lake with flush-top blocks looks like a grid of blue cubes. A lake with recessed tops looks like water. The recession catches light differently, creates visible edges where water meets land, and makes the surface read as a fluid plane rather than a stack of boxes. A small number change with a large visual impact.
Same-liquid face culling. Two adjacent water blocks do not render the face between them. Two adjacent lava blocks do not render the face between them. But a water block next to a lava block does render the shared face. And a liquid block adjacent to a solid block does not render that face either — no point rendering a water surface that is pressed against stone.
The culling logic uses a closure that checks whether a neighbor should suppress the current face:
let same_liquid = |nx: i32, ny: i32, nz: i32| -> bool {
tile_is_liquid(level.get_tile(nx, ny, nz))
|| level.is_solid_tile(nx, ny, nz)
};
A face is emitted only when !same_liquid(neighbor). This means liquid faces are visible against air and against different-type liquids, but not against same-type liquids or solid blocks. The result: continuous bodies of water with no internal grid lines, and clean edges where water meets open air.
Darkened rendering. Liquid blocks apply a brightness multiplier of 0.8 across all faces, with the standard per-face variation (1.0 for top/bottom, 0.8 for north/south, 0.6 for east/west) applied on top. The result: c_tb = 0.8, c_ns = 0.64, c_ew = 0.48. Liquids look denser and more opaque than air, giving the visual impression of translucency even though the renderer does not actually blend alpha.
Bedrock: The Indestructible Floor
Block ID 7, texture index 17. The simplest new block to implement, and the most consequential for world integrity.
pub const TILE_BEDROCK: u8 = 7;
Bedrock is solid, opaque, light-blocking — added to both the tile_is_solid() and tile_blocks_light() allow-lists — and indestructible. In previous versions, the bottom of the world was the lowest layer of generated terrain. A player could dig through it and fall into the void. There was no bottom.
Bedrock changes this. The terrain generator places it at y = 0 across the entire world, forming a continuous indestructible floor. No cave carving, no noise displacement, no variation. Flat, uniform, absolute. You cannot dig through it. The void below the world is sealed.
Terrain Generation: Water Table and Bedrock Floor
The terrain generator gained two significant features beyond the new block types.
Water level. A global water level is computed relative to the world depth:
let water_level = d * 2 / 3 - 2;
Any air block below the water level is filled with still water. This creates lakes in valleys and flooded low areas. The effect is a water table — a horizontal plane below which the world is submerged. Valleys fill naturally. Low ridges become islands. The world gains bodies of water that feel placed by geography rather than by arbitrary noise.
Bedrock floor. After all other terrain generation, y = 0 is overwritten with bedrock:
if y == 0 { id = TILE_BEDROCK; }
One line. The world has a bottom now.
The Java source also introduced an improved Perlin noise implementation (ImprovedNoise, OctaveNoise, CombinedNoise) for more varied terrain shape. We kept our existing noise implementation using the noise crate, which produces comparable terrain quality. The noise algorithm is a means to an end — what matters is that the generated terrain has varied elevation, valleys that fill with water, and a bedrock floor. Our terrain has all three.
What Was Deferred
GUI system. Seven files forming a self-contained UI framework: pause menu, save/load screens, button widgets. None affects core gameplay. Deferred until a version requires it for gameplay-critical functionality.
Server save/load. Level persistence via HTTP to minecraft.net. The server no longer exists. Local save/load continues to work.
Liquid flow mechanics. The flowing/still block distinction exists in the IDs, but the tick-based flow simulation is not implemented. Water and lava remain where the terrain generator places them. Deferred until observable liquid spreading becomes gameplay-relevant.
Test Suite: 97 Tests
Ten new tests bring the total from 87 to 97:
Tile IDs and properties (5):
– test_new_tile_ids — bedrock is 7, flowing water 8, still water 9, flowing lava 10, still lava 11.
– test_bedrock_is_solid — bedrock returns true for both tile_is_solid() and tile_blocks_light().
– test_liquids_not_solid — all four liquid tile IDs return false for tile_is_solid().
– test_liquid_detection — tile_is_liquid() returns true for all four liquid IDs and false for rock and air.
– test_liquid_types — tile_liquid_type() returns 1 for water variants, 2 for lava variants, 0 for non-liquids.
Textures (2):
– test_liquid_textures — water variants use texture 14, lava variants use texture 30.
– test_bedrock_texture — bedrock uses texture 17.
Terrain generation (3):
– test_terrain_has_water — a generated 128x128x64 level contains at least one still water block.
– test_terrain_has_bedrock — block at (32, 0, 32) in a generated level is bedrock.
– test_flat_level_no_water — flat test levels (used by other tests) do not contain water, ensuring test isolation.
All 87 previous tests continue to pass. The new tests operate on tile properties and terrain state — none require GPU context. The terrain generation tests use Level::with_save_path with /dev/null to avoid filesystem side effects.
Gap Analysis
| Feature | Status | Notes |
|---|---|---|
| Flowing water (ID 8) | DONE | Block ID, properties, rendering. Flow DEFERRED. |
| Still water (ID 9) | DONE | Block ID, properties, rendering. |
| Flowing lava (ID 10) | DONE | Block ID, properties, rendering. Flow DEFERRED. |
| Still lava (ID 11) | DONE | Block ID, properties, rendering. |
| Bedrock (ID 7) | DONE | Solid, texture 17, floor at y=0. |
| Liquid non-solid | DONE | Not in tile_is_solid allow-list. |
| Liquid lowered top face | DONE | Top surface at y + 0.9. |
| Liquid same-type face culling | DONE | No faces between same-liquid or against solid. |
| Liquid darkened rendering | DONE | 0.8x brightness multiplier. |
| Water level generation | DONE | Below-threshold air filled with still water. |
| Bedrock floor generation | DONE | y=0 overwritten with bedrock. |
| Improved noise (Java) | KEPT | Using existing noise crate; comparable terrain. |
| GUI system (7 files) | DEFERRED | Pause menu, save/load screens, buttons. |
| Server save/load (HTTP) | DEFERRED | Service no longer exists. |
| Liquid flow simulation | DEFERRED | Flowing blocks render but do not spread. |
| Obfuscated class names | N/A | Deobfuscated during analysis. |
Three items deferred. One not applicable. Everything else done.
What I Learned
The obfuscation was the surprise. Not its existence — we knew obfuscated versions were coming — but how thoroughly it changed the workflow. Every previous version was a conversation with readable source: open a file, read what it does, port it. c0.0.13a_03 is an interrogation: open a file, stare at single-letter identifiers, trace method calls through six levels of indirection, reconstruct what the original code must have looked like. The deobfuscation took longer than the port.
The liquid rendering was the technical highlight. The three-mode chunk mesher — solid blocks, bush crosses, liquid blocks — is the cleanest expression yet of a pattern that will recur across hundreds of versions: every new block type that does not render as a standard cube needs its own geometry path. The architecture handles this well. The mesher’s main loop dispatches on tile type, each rendering mode is self-contained, and adding a new mode does not disturb the existing ones. When glass, slabs, stairs, fences, and a hundred other non-cubic blocks arrive in future versions, the pattern is already established.
The lowered top face was the most satisfying change. One constant — 0.9 instead of 1.0 — transforms blue cubes into water. The same geometry, the same texture, the same face culling logic, but the top surface sits one-tenth of a block lower and suddenly the world has lakes instead of blue terrain. It is a reminder that the difference between “programmer art that reads as placeholder” and “simple graphics that read as intentional” is often one well-chosen constant.
We have 871 versions to go. The class names are gone, but the code is the same code it always was. Water fills the valleys. Lava glows at the bottom of the world. Bedrock seals the floor. The chunk mesher knows three ways to draw a block, and the tile system knows which way each block wants.
Somewhere under the obfuscation, the game is growing. We are growing with it.