minecraft7 min read
Minecraft rain is also a clock

In part one the rain gave away where a player is standing. It turns out the same rain also tells the time. Not the time of day in the game, something more useful: the game's internal tick counter.
What a tick is
Minecraft does not run smoothly. It runs in steps called ticks, 20 of them every second. Mobs move once per tick, crops grow on ticks, redstone updates on ticks. Somewhere in the game there is a counter that just goes up by one, twenty times a second.
You are not supposed to be able to see that counter from a video. With rain you can.
The rain is tied to the counter
In part one, every block's rain had its own speed. Here is the rest of that piece of the game, the part that decides how far the raindrop picture over one block has slid down. It is Java again, and the comments are mine:
// the game's tick counter, wrapped so it runs 0 to 131071
int tick = (int) (ticks & 131071L);
// a head start of 0 to 255 ticks, made from the block's x and z
int offset = (x * x * 3121 + x * 45238971
+ z * z * 418711 + z * 13761) & 0xFF;
// the block's speed, from part one
float speed = 3.0F + random.nextFloat();
// how far the picture has slid, in picture heights. partialTick is
// how far into the current tick the game is, and minus means down
float slide = -(tick + offset + partialTick) / 32.0F * speed;
// keep the number small, whole heights look the same anyway
float start = slide % 32.0F;As in part one, the names are tidied and the numbers are the game's.
Look at what goes in: the tick counter and the block's coordinates. If you already know where the player is (that was part one), then you know offset and speed for every column exactly. The only thing left that you do not know is ticks.
So if you can see how far each column's picture has slid, you can work the tick out.
One column is a bad clock, three is a better one
There is a catch. The raindrop picture repeats. Once it has slid down by one full picture it looks exactly like it did before. A column with speed 3.65 is back where it started every 8.77 ticks. So one column can only tell you the tick "give or take any multiple of 8.77", which is useless.
But every column has a different speed, so every column loops after a different number of ticks. It is like having several clock hands that all turn at different rates. One hand alone is ambiguous. All of them together only line up in one way.
Here are three columns as dials. A tick has been hidden, and the dashed marks show where it leaves each hand. Drag the slider until all three hands sit on their marks:
The three bars underneath show every tick where column A alone would fit, then where A and B both fit, then all three. One column fits about two dozen ticks. Two columns together fit two or three. All three fit exactly one.
The real counter runs from 0 to 131071 before it wraps around, which is 109 minutes. Three columns are not enough to pick one tick out of that many, but a video usually has fifteen or twenty readable columns, and that is plenty.
Finding the tick is another "try them all" search. This is my tool's side again, not the game's. rainSpeed is the function from part one:
// the game's lines as one function: how many picture heights
// the rain over block x, z has slid at a given tick
static double slid(double tick, int x, int z) {
int offset = (x * x * 3121 + x * 45238971
+ z * z * 418711 + z * 13761) & 0xFF;
return (tick % 131072 + offset) * rainSpeed(x, z) / 32;
}
// distance between two points on a loop, 0 = same, 0.5 = opposite
static double gap(double a, double b) {
return Math.abs((a - b + 1.5) % 1 - 0.5);
}
// seen[i] is where in its loop the video shows column i (0 to 1),
// x[i], z[i] is the block it stands on. try every 50th of a tick
for (int step = 0; step < 131072 * 50; step++) {
double tick = step / 50.0;
boolean fits = true;
for (int i = 0; i < seen.length && fits; i++)
fits = gap(slid(tick, x[i], z[i]) % 1, seen[i]) < 0.03;
if (fits) System.out.println("the game was on tick " + tick);
}Run that and it prints a little cluster of answers right around the true tick. The real version keeps the best one. It also has one extra wrinkle: the ground under each column can shift its picture by a quarter, so it has to try those too. The idea is the same.
Does it work
I made test clips where I set the counter myself. It got the tick right 8 times out of 8, to within a tenth of a tick.
Then the real test. The stream from part one has three separate moments where the player stands still. I ran each one on its own, with no knowledge of the others:
| Time in the video | Tick read from the rain | What it should be |
|---|---|---|
| 7.0 s | 14119.5 | |
| 28.5 s | 14549.5 | 21.5 seconds later is 430 ticks later: 14549.5 |
| 40.5 s | 14789.5 | 12 more seconds is 240 more ticks: 14789.5 |
Each of those is one answer out of 131072 possible ticks, found separately, and they land exactly twenty ticks per second apart.
There is a small bonus. The same measurement shows how high the player's eyes are above the block grid, and it came out as 0.62, 0.63 and 0.64. A standing player's eyes are 1.62 blocks above their feet, so that is what you would expect from someone standing on a full block.
Which counter you get depends on the version
This is the part I did not expect. The rain code is the same in 1.21 and in 26.x, but the ticks that gets handed to it is not the same counter.
In 1.21 it is a counter that belongs to your game alone:
// once every client tick the renderer bumps a counter of its own.
// the server never sets it
this.ticks++;
// and that private counter is what the rain code gets
this.weatherEffectRenderer.extractRenderState(
this.level, this.ticks, partialTick, cameraPos, state);In 26.x it is the age of the world:
// WeatherEffectRenderer: the rain code now gets the world's age
rainColumns.add(createRainColumnInstance(
random, level.getGameTime(), x, z, partialTick));
// ClientLevel: every client tick your game adds one by itself
private void tickTime() {
long gameTime = this.levelData.getGameTime() + 1L;
this.levelData.setGameTime(gameTime);
}
// ClientLevel: but the server's time message replaces that count
public void setTimeFromServer(long gameTime) {
this.levelData.setGameTime(gameTime);
}
// MinecraftServer: and that message goes out every 20 ticks
if (this.tickCount % 20 == 0) {
this.forceGameTimeSynchronization();
}So in 26.x your game keeps counting on its own, but once a second the server says "the time is this" and your game obeys. If your game had fallen a tick behind, the rain suddenly jumps a tick to catch up.
And you can see that happen. This is how far the rain's counter runs ahead of a perfectly steady 20 ticks a second, in three recordings:
The 1.21 line is flat. Over twelve seconds it never strays more than a hundredth of a tick. The 26.x lines drop by exactly one tick, or exactly two, for a second at a time, and then snap back when the next message from the server arrives. Those are the corrections.
What it is good for
Spotting cuts. In 1.21 the counter is steady and private to one game, so it works as a stopwatch nobody can see. If a video has been cut and stitched back together, the tick before the cut and the tick after it will not match the time between them. The hunger bar's shake leaks a tick counter in a similar way. Rain is a second one, and a much finer one.
Lining up videos. In 26.x the counter is the age of the world, and on a normal server it is the same for every player. Two people recording on the same server should be able to line their videos up to the tick from rain alone. I have not tried that yet.
A warning sign for part one. Those one-tick jumps also make the position harder to read on 26.x servers, because a jump in the middle of a measurement smears it. Short stretches between jumps still work.
Loose ends
- I have only checked 1.21.11 and 26.3.
- Snow has its own maths, which I have not looked at.
- Everything here needs the position first. No position, no clock.
Back to part one.