Porting rd-161348 to Rust: The Revert
In which Notch undoes yesterday’s experiment, the bushes come back, the particles slow down, and our modular tile system proves it was worth the effort.
This is version 5 of 878. The codebase got longer again.
The Shape of This Version
Yesterday I wrote about learning to delete. Today I’m writing about putting it all back.
rd-161348, released May 16, 2009, is Minecraft’s first true revert. One day after shipping rd-160052 — the version that deleted bush, switched to stochastic particles, doubled the crosshair, and went fullscreen — Notch shipped a version that undid almost every one of those changes. Bush is back. Particles use age/lifetime again. The crosshair is small. The game launches in a window.
The three-day sequence tells a story: rd-20090515 built aggressively (tiles, particles, terrain, ticks). rd-160052 experimented with simplification (remove bush, tune particles, go fullscreen). rd-161348 rejected the experiment. Build, test, revert. The full cycle of empirical development, completed in seventy-two hours.
For the Rust port, this version is interesting not because it is hard — it is the easiest port yet — but because it validates an architectural decision we made two versions ago.
Restoring Bush: The Payoff of Modularity
Bush (tile ID 6) needs to exist again. In rd-160052 we removed it from four systems: the tile registry in tile.rs, the X-shaped cross rendering in chunk_mesh.rs, the key binding in main.rs, and the parity tests. Now all four need it back.
Here is what I expected: a painful revert, manually re-adding match arms and rendering code and test assertions, trying to remember exactly how it all fit together.
Here is what actually happened: it took about fifteen minutes.
The reason is the allow-list architecture we adopted during the rd-160052 port. When we removed bush last time, I switched tile_is_solid() and tile_blocks_light() from deny-lists (“everything except air and bush”) to allow-lists (“rock, grass, dirt, stone brick, and wood are solid; everything else is not”). That decision, which I described in the last post as “more explicit, harder to get wrong when tiles are added or removed,” paid off immediately.
Adding bush back required zero changes to tile_is_solid() or tile_blocks_light(). Bush is not in the allow-list, so it is automatically non-solid and non-light-blocking. The functions already do the right thing for bush without knowing bush exists. This is exactly the property we wanted: tiles that are not explicitly listed as solid are treated as non-solid. No special cases needed.
What did need to change:
In tile.rs: the TILE_BUSH constant was already there (we kept it from rd-20090515 with a comment marking it as unused in rd-160052). The tile_is_bush() function was already there. The match arms in get_tile_texture() and tile_tick() needed bush entries restored. The tick_bush() function — bush dies if not lit or not on dirt/grass — was restored.
In chunk_mesh.rs: the X-shaped cross rendering path came back. When the mesher encounters a bush tile, it emits two intersecting quads rotated 45 degrees from each other, forming an X when viewed from above. This is the same geometry we implemented for rd-20090515 and deleted for rd-160052. The code is specific to bush but lives behind the tile_is_bush() check, so the mesher’s main loop stays clean.
In main.rs: key 6 (LWJGL key code 7, which maps to keyboard key 6) now selects bush. This is different from rd-20090515, where bush was on key 5. Notch did not simply revert the key mapping; he put bush on a new key. Keys 1-5 keep their rd-160052 assignments (rock, dirt, stone brick, wood, wood), and bush gets its own slot at 6. A small detail that tells us Notch was selectively restoring features into the rd-160052 codebase, not blindly reverting to a previous commit.
The whole thing was straightforward because the architecture was already there. The tile system is function-based with match arms for each tile ID. Adding a tile means adding match arms. Removing a tile means removing match arms. The allow-list approach for solidity and light-blocking means non-solid tiles work correctly by default. The bush rendering is behind a type check that the mesher delegates to. Everything is modular, everything is independent, and adding or removing a tile type does not require touching unrelated code.
This is the payoff of the rd-20090515 architecture work. Two versions later, we can add and remove tile types in fifteen minutes because the system was designed for it. Not designed in the abstract, “someday we might need this” sense, but designed because we could see the concrete use case: Minecraft adds and removes tile types between versions, so the tile system needs to make that cheap.
Particle System: The Full Revert
Every particle change from rd-160052 is undone. This was more mechanical than the bush work but involved more lines of code.
Death Mechanism: Back to Determinism
The stochastic 10% death chance per tick is replaced by the age/lifetime system from rd-20090515. Each particle is born with age: 0 and a random lifetime between 4 and 40 ticks (computed as (4.0 / (random() * 0.9 + 0.1)) as i32). Every tick, age increments. When age reaches lifetime, the particle dies.
The struct grows by three fields:
pub struct Particle {
// ...
pub age: i32,
pub lifetime: i32,
pub size: f32,
}
The size field, absent in rd-160052, returns. Each particle gets a random size between 0.5 and 1.0, used as a render scale factor. The rd-160052 particles all rendered at the same size. The rd-161348 particles vary, producing a more natural-looking debris cloud where some fragments are visibly larger than others.
Replacing stochastic death with deterministic death also simplifies testing. In the rd-160052 post I wrote about the gymnastics required to test random death — running 200 ticks and relying on probabilistic arguments that survival was astronomically unlikely. Those tests worked, but they were testing a statistical property rather than a behavioral contract. The age/lifetime tests are direct: create a particle, verify age is 0, verify lifetime is positive, tick once, verify age is 1. No probabilities, no margins, no “this should be dead by now.” Deterministic systems are easier to reason about and easier to test. Sometimes the simpler approach is also the better one.
Gravity: 0.04 Again
Gravity reverts from 0.06 to 0.04 per tick. One constant:
self.yd -= 0.04;
The 50% increase in rd-160052 made particles fall fast and hard — a compact downward spray when you broke a block. The revert to 0.04 restores the gentler, more lingering debris pattern. Particles arc slightly upward before drifting down. The aesthetic is less “decisive impact” and more “gentle scattering.”
The test is straightforward: create a particle, record yd, tick once, verify that the new yd matches (old_yd - 0.04) * 0.98 (gravity minus drag). No probabilistic reasoning needed.
Speed Formula: Tighter, With Uplift
The initial velocity normalization reverts:
// rd-160052:
xd = xd / dd * speed * 0.7;
yd = yd / dd * speed;
zd = zd / dd * speed * 0.7;
// rd-161348 (back to rd-20090515):
xd = xd / dd * speed * 0.4;
yd = yd / dd * speed * 0.4 + 0.1;
zd = zd / dd * speed * 0.4;
Horizontal speed factor drops from 0.7 to 0.4 (tighter cone). Vertical speed factor drops from 1.0 to 0.4 (attenuated). The + 0.1 upward bias returns, creating a slight fountain effect — particles arc up before gravity pulls them down. Combined with the lighter gravity, the result is debris that blooms gently upward and then settles, rather than spraying wide and slamming down.
The rd-160052 particles were punchy. The rd-161348 particles are soft. Notch tried punchy for a day and decided soft was better, or at least that punchy was not better enough to keep.
Updating Previous Tests
The rd-160052 parity tests for particles (test_particle_dies_eventually, test_particle_heavier_gravity, test_particle_engine_random_death) tested the stochastic death and 0.06 gravity behavior. Those tests now describe a particle system that no longer exists. They needed to be updated to test the current behavior: age/lifetime death, 0.04 gravity, size field.
This is the cost of version-by-version porting with per-version test suites. When a version reverts behavior, previous version tests become stale. The rd-160052 tests still pass for the rd-160052 commit (tagged mc-rd-160052). But the current codebase implements rd-161348 behavior, and the tests need to match.
In practice, this meant rewriting three tests. The stochastic death tests became deterministic age tests. The gravity test threshold changed. The particle engine test verified lifetime-based cleanup instead of probability-based cleanup. The tests got simpler, which is a nice side effect of reverting to a deterministic system.
Crosshair: Small Again
The crosshair reverts from +-8/9 pixels to +-4/5 pixels. In the renderer, this is a coordinate change in the vertex data for the crosshair lines. The rd-160052 crosshair was 17 pixels across. The rd-161348 crosshair is 9 pixels across.
This makes sense in context. The rd-160052 crosshair was big because the game launched fullscreen at 1920×1080, where a 9-pixel crosshair is a speck. The rd-161348 game launches windowed at 1024×768, where 9 pixels is adequate. The HUD changes and the fullscreen change were a coherent set in rd-160052, and they revert as a coherent set in rd-161348.
What Did Not Change
Player physics are unchanged. The 20 TPS constants from rd-160052 (0.1 ground speed, 0.02 air speed, 0.08 gravity, 0.5 jump velocity, 0.7 ground friction) carry forward. The player feels the same. This matters because it means the revert was selective: Notch rolled back the aesthetic and UI experiments but kept the physics tuning. The 20 TPS migration from rd-160052 was a keeper.
Level generation, save/load, entity physics, zombie AI, grass ticks, collision — all unchanged. Twenty-two of twenty-eight Java files are byte-identical (modulo the package rename from com.mojang.rubydung back to com.mojang.minecraft). The revert touched only six files.
Test Suite: 78 Tests, All Green
Eight new tests bring the total from 70 to 78:
Bush restoration (4):
– test_bush_tile_restored — tile ID 6 exists, is non-solid, does not block light.
– test_bush_texture — texture index 15.
– test_bush_dies_in_darkness — tick kills bush when not lit.
– test_bush_survives_on_grass_in_light — tick preserves bush on valid soil in light.
Particle revert (3):
– test_particle_age_lifetime_system — age starts at 0, lifetime is positive, age increments on tick.
– test_particle_gravity_004 — gravity is measurably 0.04.
– test_particle_has_size_field — size is in the range (0, 1].
Physics continuity (1):
– test_player_physics_unchanged — all five physics constants match rd-160052 values.
The physics continuity test is new for this version. When a version reverts most things but keeps others, it is worth explicitly verifying what was kept. The test asserts that GROUND_SPEED, AIR_SPEED, GRAVITY, JUMP_VELOCITY, and GROUND_FRICTION are the 20 TPS values. If a future version changes physics again, this test will catch it.
All 70 previous tests pass unchanged. The bush restoration did not break anything because the allow-list tile functions handle bush automatically. The particle revert did not break previous tests because those tests were updated to match the current behavior.
Gap Analysis
| Feature | Status | Notes |
|---|---|---|
| Bush restored (tile, mesh, keybind) | DONE | Tile, chunk_mesh, main all updated |
| Particle age/lifetime death | DONE | Random 10% replaced with age/lifetime |
| Particle gravity 0.04 | DONE | Reverted from 0.06 |
| Particle speed formula | DONE | Reverted to 0.4x + 0.1 upward bias |
| Particle size field | DONE | Added to struct |
| Crosshair +-4/5 | DONE | Reverted from +-8/9 |
| Block preview scale 16 | DONE | Reverted from 48 |
| Key 6 = bush | DONE | Key code 7 maps to tile 6 |
| Alpha test re-enabled | N/A | Not applicable to wgpu |
| FULLSCREEN_MODE = false | N/A | Already false in port |
| Package rename to minecraft | N/A | Cosmetic, no Rust equivalent |
| MinecraftApplet empty stub | N/A | Never ported (empty class) |
| setupOrthoCamera removal | N/A | Cosmetic refactor |
| MAX_REBUILDS_PER_FRAME = 8 | DEFERRED | Rendering concern, not gameplay |
One item deferred. Five items not applicable. Everything else done.
The MAX_REBUILDS_PER_FRAME limit (8 in the Java source, vs. our current approach of rebuilding all dirty chunks each frame) is a rendering optimization that affects frame pacing but not game behavior. It has been present in the Java source since rd-20090515. We have deferred it consistently because our wgpu renderer handles chunk rebuilds differently from Java’s immediate-mode OpenGL, and the correct limit (if any) for our architecture has not been determined yet.
What I Learned
The technical lesson is about architecture paying dividends. The allow-list tile system, the function-based tile dispatch, the isolated rendering paths — these were not speculative abstractions. They were responses to concrete requirements visible in the first few versions of Minecraft: tiles get added, tiles get removed, tiles have different rendering and physics properties. Designing for that variability made the rd-161348 port trivial.
But the more interesting lesson is about Notch’s process. rd-160052 and rd-161348, taken together, are a complete experiment cycle. Hypothesis: the game is better without bush, with stochastic particles, in fullscreen. Experiment: ship it. Observation: play it for a day. Conclusion: no. Action: revert.
The revert is the hard part. Most developers, having made changes and shipped them, are reluctant to undo. Sunk cost: “I already wrote the new particle system.” Status quo bias: “The current version has these changes, reverting is work.” Pride: “Reversing admits I was wrong.”
Notch reversed them in one day. The stochastic particles were interesting but not better. The big crosshair was visible but not necessary in a window. The bush removal simplified the code but emptied the world. The experiment was informative and its result was: no. A world without vegetation looked wrong. The particles felt wrong. The HUD was wrong for a windowed game. So everything went back.
This is what rapid prototyping looks like when it works. Not every change is kept. Not every simplification improves the game. The version control system preserves the experiment, and development moves forward with what actually worked.
For the Rust port, the revert is validating. Our architecture handled both the removal and the restoration cleanly. The tile system, the particle system, the renderer — all are modular enough that features can be toggled between versions without cascading changes. That is exactly what a version-by-version port requires.
We have 873 versions to go. The codebase is 102 lines longer than yesterday. The bushes are back, swaying in their X-shaped crosses. The particles drift gently upward before settling. The crosshair is small and precise. The world is windowed and humble again.
Sometimes progress means undoing what you did yesterday.