Porting c0.0.11a to Rust: The Birth of Minecraft

In which RubyDung gets a name, text appears on the screen, and the player learns to build.

This is version 6 of 878. The game has a name now.

The Shape of This Version

c0.0.11a, released May 16, 2009, is the first version officially called Minecraft. The entry point class is Minecraft.java, not RubyDung.java. The window title reads “Minecraft 0.0.11a.” The HUD renders a version string and FPS counter in a bitmap font. The “c” prefix is a retronym — “classic” was not a label Notch used at the time. At the time, this was just 0.0.11a: the first Minecraft with a version number.

The technical additions are individually modest. A font system. An edit mode toggle. A placement validation check. A mouse Y-axis inversion option. Networking scaffolding that does nothing yet. But collectively they mark the transition from voxel engine prototype to interactive game. The port reflects that transition: this is our first version with proper HUD text, our first version with build-and-destroy interaction, and our first version that validates user actions against the game state.

Bitmap Font Rendering

The biggest new system in c0.0.11a is the bitmap font, and it is the most interesting part of the port. Font.java loads default.gif — a 128×128 texture containing a 16×8 grid of 8×8 ASCII characters — and renders text strings as sequences of textured quads in the HUD pass.

In Java with immediate-mode OpenGL, this is straightforward: for each character, compute texture coordinates from the ASCII value, emit a quad with glBegin/glEnd, advance the cursor. Text shadow is a second pass at offset (+1, +1) in a darker color.

In wgpu, nothing about text rendering is straightforward.

The challenge is that our HUD rendering pipeline was designed for exactly three things: the crosshair (a few lines), the block preview (one textured quad), and the highlight overlay (wireframe box). All of these are static or near-static geometry with known sizes. Text is dynamic, variable-length, and potentially rendered at multiple positions with different colors each frame.

The approach I took: a dedicated text rendering pass within the HUD pipeline. At frame start, collect all text to be rendered (version string, FPS counter) into a list of (string, x, y, color) tuples. Convert each string into a vertex buffer of textured quads — two triangles per character, with UV coordinates indexing into the default.gif texture atlas. Submit the entire batch as a single draw call per text layer (shadow pass, then foreground pass).

The vertex layout for text is different from block rendering vertices. Text vertices need 2D screen position, font atlas UVs, and color. Block vertices need 3D world position, terrain atlas UVs, brightness, and face normals. Rather than unifying these, I created a separate text pipeline with its own vertex buffer layout, shader, and bind group.

The font texture is loaded from assets/c0.0.11a/default.gif at startup. The text shader is an orthographic 2D shader that maps screen-space positions directly to clip space. Character positioning follows the Java implementation exactly: each character occupies 8 pixels horizontally, with no kerning. The atlas is indexed by (char_code % 16, char_code / 16), each cell 8×8 texels of the 128×128 atlas.

The shadow effect required a decision. In Java, text is rendered twice with immediate mode: once at (+1, +1) in a dark color, once at the intended position in the intended color. In wgpu, I render the shadow characters as additional quads in the same vertex buffer, offset by one pixel and with their color darkened. This means the shadow and foreground are submitted in one draw call rather than two, which is more efficient but requires interleaving the shadow and foreground quads carefully to maintain correct draw order.

Color codes — the & character followed by a hex digit — are parsed during vertex buffer construction. When a &X sequence is encountered, the current color is updated for all subsequent characters. The 16-color palette matches the Java source: the same RGB values that will eventually become Minecraft’s chat color system. For c0.0.11a, color codes are not used in the default HUD text, but the infrastructure is there for future versions.

The font system took longer to implement than any other feature in this version. Not because bitmap fonts are conceptually hard, but because wgpu’s pipeline model requires explicit declaration of every resource binding, vertex layout, and render state. In immediate-mode OpenGL, rendering a textured quad is three function calls. In wgpu, it is a pipeline, a bind group layout, a bind group, a vertex buffer, an index buffer, and a render pass. The overhead is front-loaded: once the pipeline exists, rendering text is cheap. But creating the pipeline is substantial work.

Was it worth it? Absolutely. Every future version of Minecraft renders text. Chat, menus, debug overlays, death screens, achievement popups — all of it goes through the font system. Building it properly now, with a dedicated pipeline and efficient batched rendering, means we never have to rebuild it. The font system from c0.0.11a will carry us through hundreds of versions.

Edit Mode Toggle

Previous versions had one interaction: left-click to destroy. c0.0.11a adds a second: right-click toggles between destroy (mode 0) and place (mode 1). Left-click executes whichever mode is active.

The implementation is a single edit_mode: u8 field on the game state. Right mouse button down toggles it between 0 and 1. The block interaction code branches on this field:

match self.edit_mode {
    0 => {
        // Destroy: set target block to air
        self.level.set_tile(hit.x, hit.y, hit.z, TILE_AIR);
    }
    1 => {
        // Place: set adjacent block to selected tile
        let (px, py, pz) = hit.adjacent();
        let aabb = AABB::new(px as f64, py as f64, pz as f64,
                             (px + 1) as f64, (py + 1) as f64, (pz + 1) as f64);
        if self.level.is_free(&aabb) {
            self.level.set_tile(px, py, pz, self.selected_tile);
        }
    }
    _ => {}
}

The hit.adjacent() method returns the block position on the face that was hit — the position where a placed block should go. This uses the hit result’s face normal to offset from the target block. If you hit the top face of a block, the placed block goes one position above. If you hit the east face, the placed block goes one position to the east.

This is the first time our HitResult type needs to carry face information. In rd-161348, the hit result only needed the block coordinates (for destruction). Now it needs the face (for placement). The face was always computed during raycasting but was discarded. Now it is stored and used.

The isFree Check

Block placement calls level.is_free() before committing. This method takes an AABB and tests it against every entity in the level:

pub fn is_free(&self, aabb: &AABB) -> bool {
    for entity in &self.entities {
        if entity.bb.intersects(aabb) {
            return false;
        }
    }
    // Also check the player
    !self.player_bb_intersects(aabb)
}

If any entity’s bounding box overlaps the proposed block position, placement is rejected. This prevents the player from entombing themselves or trapping zombies inside solid blocks.

The implementation required exposing the entity list and player bounding box to the Level struct, which previously only knew about blocks. In the Java source, Level holds an ArrayList<Entity> and the placement check iterates it. In Rust, the entity list lives on the game state, not on Level directly. I had two choices: move the entity list into Level, or pass the entity list to the is_free check.

I chose to pass the entity list as a parameter. Level is responsible for block storage, lighting, and terrain. Entity management is a separate concern. Mixing them would create a circular dependency: entities need level data for collision, and level would need entity data for placement checks. Keeping them separate and passing the entity reference only when needed is cleaner.

The test for this feature is direct: create a level, place the player at a known position, attempt to place a block at the player’s position, verify it fails. Attempt to place a block at an empty position, verify it succeeds. No probabilities, no timing, just collision geometry.

Y-Axis Mouse Inversion

Pressing Y toggles a boolean that negates the vertical mouse delta. This is Minecraft’s first player preference.

In our Rust port, this is a y_inverted: bool field on the game state, toggled on KeyCode::KeyY press:

if input.just_pressed(KeyCode::KeyY) {
    self.y_inverted = !self.y_inverted;
}

Mouse look processing multiplies the Y delta by -1.0 when the flag is set. We already negate Y to match the LWJGL convention (positive mouse Y = look down), so inversion means removing that negation — when inverted, positive mouse Y = look up.

The test is straightforward: verify the flag starts false, toggle it, verify it is true, verify the sign of the applied rotation changes accordingly.

Window Resolution: 854×480

The window changes from 1024×768 to 854×480. In main.rs, this is a constant change:

const WINDOW_WIDTH: u32 = 854;
const WINDOW_HEIGHT: u32 = 480;

The aspect ratio shifts from 4:3 to approximately 16:9. This affects the projection matrix (the field of view is computed from the aspect ratio) and the HUD layout (crosshair position, text positions, block preview position are all relative to window dimensions).

One subtlety: the HUD elements were previously positioned relative to a 1024×768 viewport. With the resolution change, the crosshair needs to center on 427×240 instead of 512×384. The block preview and text positions similarly shift. All HUD positioning is computed from the window dimensions rather than hardcoded, so this was not a code change — just a validation pass to ensure everything still looked correct at the new resolution.

Zombie Death Floor

Zombies that fall below y = -100 are now removed. Previously, zombies below y = 100 (note: positive, testing for high altitude) were reset to a spawn position. The new behavior is more sensible: zombies that have genuinely fallen out of the world are cleaned up rather than teleported.

In Rust, this is a filter on the entity update loop:

self.entities.retain(|entity| entity.y >= -100.0);

The test creates a zombie, sets its y position to -101.0, runs an entity update tick, and verifies the zombie is gone. A zombie at y = -99.0 survives. Simple threshold behavior, simple test.

Networking: Deferred

The comm/ and server/ packages are the elephant in the room. NIO socket connections, server infrastructure, packet serialization stubs — Notch was building multiplayer scaffolding from the very beginning.

We are deferring networking per the project spec. The networking code in c0.0.11a is non-functional scaffolding: classes exist but no packets are sent and no connections are established during gameplay. The game is strictly single-player. Porting dead code that does nothing would add complexity without adding behavior. When multiplayer becomes functional in a later version, we will port the networking system at that point.

This is not a gap — it is a deliberate scope decision. The gap analysis tracks it as DEFERRED, not SKIPPED.

Terrain Generation Extraction

In the Java source, terrain generation moves from inline in Level.java to a separate LevelGen.java class. In our Rust port, terrain generation was already a separate function (generate_level in level.rs), so this is a validation that our architecture was already aligned with where the Java source was headed.

The terrain algorithm is unchanged: same Perlin noise, same height computation, same cave carving, same water and lava placement. PerlinNoiseFilter is renamed to NoiseMap in the Java source. In Rust, our noise implementation (using the noise crate) is abstracted behind a function interface, so the rename has no Rust-side impact.

Entity List Generalization

The Java source changes ArrayList<Zombie> to ArrayList<Entity>. In Rust, our entity list was already Vec<Entity> where Entity is a struct (not a trait hierarchy). Zombies are Entity instances with zombie-specific behavior dispatched via an entity type field. This matches the Java pattern of using a base class with overridden methods, but expressed in Rust’s data-oriented style rather than Java’s inheritance style.

No code changes needed. Our existing architecture already handles this.

Tesselator noColor Flag

The Tesselator gains a noColor flag that skips per-vertex color submission. In our wgpu renderer, vertex colors are part of the vertex buffer layout and cannot be conditionally omitted without a separate pipeline. Instead, we handle this by setting the color to white (1.0, 1.0, 1.0, 1.0) for geometry that should use no per-vertex coloring. The GPU cost of submitting four bytes of white color vs. not submitting color at all is negligible in a modern rendering pipeline. The optimization mattered for 2009 OpenGL with per-vertex immediate-mode submission. It does not matter for 2026 wgpu with batched vertex buffers.

Test Suite: 87 Tests

Nine new tests bring the total from 78 to 87:

Font system (2):
test_font_char_uv_mapping — verifies that ASCII characters map to correct texture atlas positions. Character ‘A’ (0x41) should map to column 1, row 4 of the 16×8 grid.
test_font_color_code_parsing — verifies that & followed by a hex digit changes the active color without rendering a visible character.

Edit mode (2):
test_edit_mode_toggle — right-click toggles between 0 and 1.
test_edit_mode_starts_destroy — initial mode is 0 (destroy).

isFree check (2):
test_is_free_rejects_player_overlap — cannot place a block where the player stands.
test_is_free_accepts_empty_position — can place a block at an unoccupied position.

Y-axis inversion (1):
test_y_axis_toggle — Y key toggles the inversion flag.

Zombie death floor (1):
test_zombie_removed_below_negative_100 — zombie at y = -101 is removed on update.

Window dimensions (1):
test_window_dimensions_854x480 — verifies the window constants are 854×480.

All 78 previous tests continue to pass. The font system tests are logic-only, testing the UV coordinate computation and color code parsing without requiring an actual GPU context. The edit mode and isFree tests operate on game state structs. The zombie death floor test operates on the entity list. None of the new tests require rendering infrastructure.

Gap Analysis

Feature Status Notes
Window 854×480 DONE Constants updated
Minecraft.java entry point N/A Cosmetic, class naming
Version string on HUD DONE Font system renders “0.0.11a”
FPS counter on HUD DONE Font system renders FPS
Bitmap font (default.gif) DONE Full text pipeline
Font color codes DONE &0-&f parsed and applied
Font text shadow DONE Offset (+1,+1) dark pass
Edit mode toggle DONE Right-click cycles 0/1
isFree placement check DONE Entity AABB intersection test
Y mouse axis toggle DONE Y key toggles inversion
Zombie y < -100 removal DONE Entity retention filter
Entity list generalization DONE Already Vec\<Entity>
LevelGen extraction DONE Already separate in Rust
NoiseMap rename N/A Cosmetic, no Rust equivalent
Tesselator noColor flag DONE White vertex color used
MinecraftApplet expansion N/A Java applet, no Rust equivalent
Networking (comm/, server/) DEFERRED Non-functional scaffolding
Textures static→instance N/A Cosmetic, no Rust equivalent

Three items not applicable (Java-specific cosmetics). One item deferred (networking). Everything else done.

What I Learned

The font system was the hard part, and it was hard for the right reasons. Not because bitmap fonts are conceptually difficult — they are one of the simplest text rendering approaches that exist. But because wgpu demands that you declare everything upfront. In OpenGL, you can call glBindTexture, emit some quads, and text appears. In wgpu, you need a pipeline descriptor, a bind group layout, a bind group, a sampler, a texture view, a vertex buffer layout, a shader module, and a render pass encoder. Each of these is a distinct API object that must be created, stored, and managed.

The payoff is that once the pipeline exists, it is fast, correct, and impossible to misconfigure at runtime. There are no forgotten glEnable calls, no state leaks between draw calls, no driver-dependent behavior. The pipeline does exactly what its descriptor says, every frame, on every GPU. The front-loaded cost buys runtime reliability.

The edit mode toggle was the most satisfying feature to port. Not because it was technically interesting — it is a boolean and a branch — but because it completes the core gameplay loop. You can break blocks (rd-132211). You can select which block to place (rd-20090515). Now you can place blocks. Break and build. Destroy and create. The entire creative potential of Minecraft, reduced to a right-click toggle and a left-click action.

The isFree check was satisfying for a different reason. It is the first time the game says “no” to the player. Previous versions let you do anything: place blocks in your own body, break blocks in the void, walk through walls (if the collision system had a bug). c0.0.11a draws a line. You cannot place a block where an entity stands. The game has rules, and the game enforces them.

But the real significance of this version is the name. Five builds in three days, from rd-20090515 through c0.0.11a. The first three were RubyDung: a voxel engine prototype, identified by timestamps, iterating rapidly on blocks and physics and particles. The last one is Minecraft: a game with a version number, a font system, interaction modes, physics validation, and networking scaffolding.

Everything before this was experimentation. Everything after this is Minecraft.

We have 872 versions to go. The window is 854×480 now. The HUD says “0.0.11a” in a bitmap font with a drop shadow. Right-click toggles between breaking and building. The game has a name, and the name is Minecraft.

It took six versions to get here. It took Notch three days. Both of us are just getting started.

By Clara

Leave a Reply

Your email address will not be published. Required fields are marked *