Environment attributes and timelines
Verified against Minecraft 26.3 · Part IV · dusk falls over a taiga, and one value is resolved through a stack of layers — on the server for a mob, and again on the client for the sky.
Dusk on the overworld clock is a stretch and not an instant. Between tick
11867 and tick 13670 the sky over a taiga slides from its pale blue towards
black, and the sky over a pale garden slides from its grey towards black by
the same proportion; part-way through, at tick 12542, every mob that burns in
daylight stops being in danger of burning, and is not in danger again until
tick 23460. A player would never
connect the three, and in 26.3 they are one mechanism. An environment
attribute is a named, typed, registered property of the world —
EnvironmentAttributes puts 51 of them in
BuiltInRegistries.ENVIRONMENT_ATTRIBUTE — and the world answers one for a
position and an instant by running a short stack of layers over the
attribute’s default value: the dimension, the biome, the timelines, the
weather. The surprise is in what a timeline holds. The day timeline does not
know what colour a taiga sky is and never learns: its sky track’s keyframes
are not values but modifier arguments, and the night segment is a multiply.
Night does not set the sky’s colour — it multiplies whatever the
biome produced, which is how one data-driven curve darkens every overworld
biome correctly without being told about any of them.
The cast
Eight classes carry a value from the data pack to the sky. The first two thirds of this page is the machinery, object by object; the last third runs dusk through it twice, once on each side.
| class | what it decides | thread |
|---|---|---|
EnvironmentAttribute | the key: a type, a default, an AttributeRange, and four flags — positional, spatially interpolated, syncable, and the full-resolution biomes that EnvironmentAttributes.NATURAL_MOB_SPAWNS sets. It holds no value and no state | — (registry constant) |
EnvironmentAttributeMap | what one dimension or one biome contributes — a modifier and an argument per attribute, never a bare value | — (loaded from data) |
EnvironmentAttributeSystem | the baked per-level resolver: one EnvironmentAttributeSystem.ValueSampler for each attribute some layer mentions | built in the level constructor, read on that level’s thread; a generating region builds its own, on the worker |
Timeline | a clock, an optional period, one AttributeTrack per attribute, and the named instants on that clock | — (loaded from data) |
AttributeTrackSampler | one track baked against a clock, with a one-tick cache of the sampled argument | Server or Render |
ServerClockManager | the owner of day time — one ServerClockManager.ServerClockInstance per registered WorldClock, saved as world_clocks | Server |
ClientClockManager | the client’s copy: free-runs each clock forward between packets | Render |
EnvironmentAttributeProbe | the client’s smoothing layer, living on Camera: 216 biome samples a tick, a lerp every frame | Render |
The stack a value falls through
flowchart TD
DEF["EnvironmentAttribute.defaultValue enters here"]
DIM["1 — dimension: DimensionType.attributes, a Constant folded into the baked base"]
BIO["2 — biome: one Positional layer per attribute some biome mentions"]
TLS["3 — timelines: DimensionType.timelines, one EnvironmentAttributeLayer.TimeBased per track"]
WEA["4 — weather: WeatherAttributes.addBuiltinLayers, only where Level.canHaveWeather"]
ASK{"who built this stack"}
FLASH["two client-only layers, one attribute each"]
SAN["EnvironmentAttribute.sanitizeValue clamps to the AttributeRange"]
DEF --> DIM
DIM --> BIO
BIO --> TLS
TLS --> WEA
WEA --> ASK
ASK -- "the server" --> SAN
ASK -- "the client" --> FLASH
FLASH --> SAN
The four rungs a level’s value falls through, and the one branch that is not on every value’s path: weather is the last word on the server, and the client stacks two more layers above it, each bolted to one attribute — one lerps the sky colour toward the lightning flash, the other pins the sky light factor to 1. A client asking for anything else falls the same four rungs a server does, and a region being generated builds only the first two.
EnvironmentAttributeSystem.Builder.addDefaultLayers stacks those four in
that order and only that order. There is no priority number anywhere and no
ordering data: a biome cannot run before its dimension, and weather is the
last word on the server. The client’s two extra layers are not a fifth rung:
ClientLevel adds each with
EnvironmentAttributeSystem.Builder.addTimeBasedLayer against one named
attribute — EnvironmentAttributes.SKY_COLOR and
EnvironmentAttributes.SKY_LIGHT_FACTOR — so they are two entries in two
attributes’ stacks and are absent from the other forty-nine. Each is a no-op
unless ClientLevel.getSkyFlashTime is above zero. The stack is per
attribute, too — an attribute nothing
in the level mentions has no EnvironmentAttributeSystem.ValueSampler at
all, and EnvironmentAttributeSystem.getValue hands back its default. And it
is baked once: the level’s whole stack is built in the ServerLevel and
ClientLevel constructors and never rebuilt, the only writer being
ServerLevel.setEnvironmentAttributes, which is deprecated, marked for
testing and called only from TestEnvironmentDefinition. A data-pack reload
does not touch it.
Each rung earns its shape. The dimension’s is an
EnvironmentAttributeLayer.Constant, and
EnvironmentAttributeSystem.bakeLayerSampler walks off the front of the list
folding every leading constant into one baked base value, so a dimension
costs nothing at read time. The biome’s is an
EnvironmentAttributeLayer.Positional, added once per attribute that any
biome in the whole registry mentions — in vanilla, thirteen attributes across
sixty-seven biome files, with visual/sky_color in fifty-seven of them.
Timeline
layers are EnvironmentAttributeLayer.TimeBased, one per track, and so is
weather: one for each of the nine attributes named by
WeatherAttributes.RAIN or WeatherAttributes.THUNDER, blending rain in
first and thunder second. Rain is weighted at Level.getRainLevel minus the
thunder level, so a thunderstorm never counts twice. Level.canHaveWeather
wants sky
light, no ceiling and not the End, so a rain-free dimension does not carry a
weather layer that does nothing: it carries none.
ClientLevel adds two more after those four, both keyed on the lightning
flash that LightningBolt sets through Level.setSkyFlashTime. One lerps
EnvironmentAttributes.SKY_COLOR a fixed 22% toward a pale blue-white, the
other pins EnvironmentAttributes.SKY_LIGHT_FACTOR to 1 outright, and both
read the flash through the accessibility option Hide Sky Flashes,
which reports a flash time of zero while it is switched on — so the two
layers are still there and simply never fire. (The End’s sky flash is a
different thing
entirely — EndFlashState, read by the renderers rather than through the
stack; see lightmap, fog and sky.)
An attribute is positional unless its builder says otherwise — positional
meaning its answer is allowed to differ from block to block — and that flag
is what polices who may write it. Biome.getAttributes is read through
EnvironmentAttributeMap.CODEC_ONLY_POSITIONAL, so naming a non-positional
attribute in a biome file is a load error, and no biome can locally change
sky light level or lava speed. WorldGenRegion.environmentAttributes shuts
the door from the other side: a region being generated builds a stack of the
two static rungs alone, dimension and biome, so a feature or a spawn that asks
about the environment during generation gets the place and never the hour,
because generation must not depend on the hour.
Arguments, not values
EnvironmentAttributeMap is not a map of values. It is a map of
EnvironmentAttributeMap.Entry, and an entry is an argument plus an
AttributeModifier. EnvironmentAttributeMap.Builder.set is sugar for
EnvironmentAttributeMap.Builder.modify with AttributeModifier.override;
the interesting entries multiply, blend, maximise or or into whatever the
layer below produced. AttributeTrack is the same shape — a modifier plus a
KeyframeTrack of arguments — which is why Timeline.Builder.addTrack is
only the override case and Timeline.Builder.addModifierTrack is the general
one, and why the day timeline can say multiply sky light by 0.267 at night
rather than sky light is 4 at night. In a data pack the shorthand shows: an
entry written as the value itself means override, even for an attribute whose
value is an object, and any other entry is an object carrying a modifier and
an argument.
What a type allows
AttributeType is a record of a
value codec, an AttributeType.modifierLibrary of the operations legal on
that type and a codec for them, four separate LerpFunctions, one for
each way two values of it can meet, and two converters.
| lerp slot | used when |
|---|---|
AttributeType.keyframeLerp | between two keyframes of a timeline track that overrides the value — a track of modifier arguments uses the modifier’s own AttributeModifier.argumentKeyframeLerp instead |
AttributeType.stateChangeLerp | fading weather in and out |
AttributeType.spatialLerp | across a biome boundary |
AttributeType.partialTickLerp | between two client ticks, inside a frame |
AttributeTypes registers fifteen of them: two boolean kinds, three numeric,
two colour, six a data pack sets whole — a moon phase, a villager activity, a
bed rule, a particle, a piece of background music, the ambient sounds — and
two that combine, the ambient particles, a list that cross-fades, and the
mob-spawn settings, which overlay. One built by
AttributeType.ofNotInterpolated
gets a step function in all four slots, each with its own threshold, which is
how a MoonPhase snaps while a colour slides; AttributeType.toFloat and
AttributeType.toInt are nullable, and their presence decides whether a loot
table can read an attribute as a number, which the trace below reaches. Each
type publishes a
small, closed library of operations —
six logic gates for a boolean, six arithmetic ones for a float, five for a
colour, four of them ways to combine two colours — and a track or an entry
that asks for one the type does not publish fails where it is built,
AttributeType.checkAllowedModifier in the game’s own builders and the type’s
modifier codec for a data pack, so an illegal combination is a load error
rather than a runtime surprise.
Three codecs then decide who may write what:
| codec | used by | effect |
|---|---|---|
EnvironmentAttributeMap.CODEC | DimensionType.attributes | anything |
EnvironmentAttributeMap.CODEC_ONLY_POSITIONAL | Biome.getAttributes | rejects non-positional attributes |
EnvironmentAttributeMap.NETWORK_CODEC | DimensionType.NETWORK_CODEC, Biome.NETWORK_CODEC | drops every non-syncable entry before the wire |
Who owns the clock
WorldClock is a unit record. It holds nothing at all: it is an identity
token in the Registries.WORLD_CLOCK registry, and vanilla registers two,
WorldClocks.OVERWORLD and WorldClocks.THE_END. Every piece of state lives
in ServerClockManager.ServerClockInstance — a total tick count, a fractional
partial tick, a rate and a paused flag — and the manager holding those is a
SavedData under ServerClockManager.TYPE, saved once for the whole server
as world_clocks.
The four numbers, and the two records that copy them
The live instance is mutable, so its numbers exist twice more as records.
ClockState is the saved form and carries all four; PackedClockStates is
the map of them a save file holds. ClockNetworkState is the wire form and
carries three — total ticks, partial tick and rate — and the one it leaves
behind, the paused flag, is the whole of what the client does not get.
ClockManager is the one thing the two managers share, an interface with a
single method, ClockManager.getInstance: this clock’s ClockInstance —
its total tick count, partial tick, rate and pause. Everything a
reader of an attribute needs from a clock is behind that method, which is why
AttributeTrackSampler can be the same class on both sides, and why
ServerClockManager is the owner of day time in this book.
What moves a clock
MinecraftServer calls ServerClockManager.tick once per server tick,
inside the clocks profiler zone and only while the tick-rate manager runs
normally; the GameRules.ADVANCE_TIME check sits inside the method itself,
and gates every clock at once, whereas ServerClockManager.setPaused gates one.
Each unpaused instance then gains its rate, accumulating the fraction, so a
clock at rate 0.5 gains a tick every other server tick and one at rate 1000
gains a thousand — /time rate accepts anything from 0.00001 to 1000.
Naming an instant on a clock
A ClockTimeMarker is a named instant on a clock: ClockTimeMarkers.DAY,
NOON, NIGHT, MIDNIGHT, WAKE_UP_FROM_SLEEP, ROLL_VILLAGE_SIEGE.
Markers are declared inside timelines and collected onto the clock by
ServerClockManager.init, and Timeline.validateRegistry fails the whole
registry load if two timelines on one clock declare the same one. The subset
/time suggests is the one flagged ClockTimeMarker.showInCommands, though
it accepts any marker on the clock;
ServerClockManager.isAtTimeMarker is how VillageSiege asks whether the
siege roll is due, and ServerLevel.tick calls
ServerClockManager.moveToTimeMarker to jump the clock when enough players
are asleep. Everything TimeCommand offers is offered twice, once against
the source level’s DimensionType.defaultClock and once under /time of
against a clock the player names, because a command that says set the time
has to be told whose time it means.
The four timelines
| timeline | period | what it carries |
|---|---|---|
Timelines.OVERWORLD_DAY | 24000 | the whole day/night curve — sun, moon and star angles, sky and fog colours, sky light, and the gameplay flags that flip at dusk |
Timelines.MOON | 192000 — eight days | the moon phase, and the surface slime spawn chance riding the same steps |
Timelines.VILLAGER_SCHEDULE | 24000 | EnvironmentAttributes.VILLAGER_ACTIVITY and EnvironmentAttributes.BABY_VILLAGER_ACTIVITY |
Timelines.EARLY_GAME | none | one ramp that ands EnvironmentAttributes.CAN_PILLAGER_PATROL_SPAWN with false until tick 120000 |
Three of the four have a period and are cycles. The fourth is the interesting
one: a timeline with no period is not a cycle at all — its track runs once
against the clock’s total ticks and then holds its last value forever, which
is what makes Timelines.EARLY_GAME a switch, thrown when the overworld clock
passes 120000 ticks and closed again only if /time sets the clock back below
it, rather than something that comes round again.
All four run on WorldClocks.OVERWORLD; no timeline is bound to the End’s
clock, which the End’s dimension type names as its default only, the clock
/time sets there and the End’s flash reads. Which of them a dimension runs
is a tag on
DimensionType.timelines: TimelineTags.IN_OVERWORLD names the day, moon
and early-game timelines on top of TimelineTags.UNIVERSAL, while
TimelineTags.IN_NETHER and TimelineTags.IN_END name only the universal
one, which holds the villager schedule. That schedule is the clearest case of
a timeline driving something that is not a colour: Brain.setSchedule takes
an EnvironmentAttribute of Activity, Villager is the only caller —
adults read EnvironmentAttributes.VILLAGER_ACTIVITY and babies
EnvironmentAttributes.BABY_VILLAGER_ACTIVITY, two tracks on the one
timeline — and Brain.updateActivityFromSchedule samples it at the
villager’s own position, but only when more than twenty game ticks have passed
since it last looked. Where the activity it reads then sends a villager is in
points of interest.
What crosses the wire
The rules travel, never the resolved values. Registries.TIMELINE and
Registries.WORLD_CLOCK are in RegistryDataLoader.SYNCHRONIZED_REGISTRIES
— the timeline through Timeline.NETWORK_CODEC, so only syncable tracks go —
while Registries.ENVIRONMENT_ATTRIBUTE and Registries.ATTRIBUTE_TYPE are
built-in code registries that never go out at all.
Syncable is the third flag, and it draws the line between the two sides. Thirty-three of the 51 carry it: every one of the 24 visual/ attributes and all four audio/ ones, and five of the gameplay flags — sky_light_level, fast_lava, water_evaporates, piglins_zombify, creaking_active. The eighteen left behind are gameplay decisions the server makes alone: whether monsters burn, whether a raid can start, what a villager should be doing now. Dropping their entries and tracks before the wire costs the client almost nothing: the one place it asks for them is the bed rules, read through the sleeping code it shares with the server, and there it gets the defaults. So the client’s stack is shorter than the server’s, and identical everywhere else the client looks.
Clock state rides
ClientboundSetTimePacket: a game time — the world’s own tick count, which no clock owns — plus a ClockNetworkState — total
ticks, partial tick, rate — per clock in its map.
ServerClockManager.createFullSyncPacket fills that map on join, respawn and a change of dimension, and on a GameRules.ADVANCE_TIME change. Every mutator broadcasts a one-clock update. The routine broadcast from MinecraftServer.forceGameTimeSynchronization, once every twenty ticks, sends an empty map and nothing but the game time.
ClientClockManager.handleUpdates adopts what arrives and
ClientClockManager.tick free-runs the rest — which is why a paused clock
travels as rate 0: the client has no paused flag to receive.
Dusk, and the same question asked twice
A mob asks whether it should be burning, and the camera asks what colour the sky is. Both go through the stack above, and this is the machinery running.
sequenceDiagram
participant SL as ServerLevel
participant EAS as Environment<br/>AttributeSystem
participant EVS as Environment<br/>AttributeSystem.<br/>ValueSampler
participant Mob as Mob
participant ATS as Attribute<br/>TrackSampler
participant KTS as Keyframe<br/>TrackSampler
Note over SL,KTS: one server tick
SL->>EAS: invalidateTickCache — before the world border, before the weather
EAS->>EVS: the cached value is dropped, the cache tick id bumped
Mob->>EAS: getValue(MONSTERS_BURN, position)
EAS->>EVS: getValue — is any layer of this attribute positional?
EVS->>EVS: none is — start from the baked base, the default false
EVS->>ATS: applyTimeBased(value, cache tick id)
ATS->>KTS: sample at the overworld clock's ClockInstance.totalTicks
KTS-->>ATS: the argument — false at 12542, true again at 23460
ATS-->>EVS: the modifier is BooleanModifier.OR, applied to the value
EVS->>EVS: the weather layer is on nine attributes, not this one
EVS-->>Mob: false, clamped and cached for the tick
One attribute resolved once, on the server, inside one tick: the level’s only contribution is the first two arrows, which throw last tick’s answer away, and every arrow after them is the stack being walked because a mob asked. Nothing here is pushed — a value exists because something wanted it.
Read the arrows as decisions. EnvironmentAttributeSystem.invalidateTickCache
computes nothing: it drops each sampler’s cached value and bumps a counter,
and that counter is the identity every track sampler compares against.
AttributeTrackSampler.applyTimeBased keeps a one-entry cache of the sampled
argument and reuses it for every reader arriving with the same tick id, so
a thousand mobs asking EnvironmentAttributes.MONSTERS_BURN cost one
keyframe sample between them.
The step that reads oddly is the one where the sampler asks whether any layer
of the attribute is positional and finds that none is.
EnvironmentAttributes.MONSTERS_BURN is a positional attribute, and yet
its stack is one layer deep, because in vanilla nothing but the day timeline
mentions it: no dimension type, no biome.
EnvironmentAttributeSystem.ValueSampler decides by layers, not by the
flag, so with no positional layer present the position is ignored and the
whole answer is memoised for the tick. The flag still governs where the
attribute may be read: it makes EnvironmentAttributeCheck and
the two EnvironmentAttributeValue number providers declare LootContextParams.ORIGIN among the
parameters they read, and the default answers when a context has none, and it
makes EnvironmentAttributeSystem.getDimensionValue throw
in a development build if asked for a positional attribute at all. Only two
attributes are built EnvironmentAttribute.Builder.notPositional — sky light
level and lava speed — and three call sites read them that positionless way:
Level.updateSkyBrightness for EnvironmentAttributes.SKY_LIGHT_LEVEL, and
LavaFluid.isFastLava and Entity for EnvironmentAttributes.FAST_LAVA,
the two sites that between them decide how fast lava flows and
how hard it shoves. A fourth site names no attribute at all:
EnvironmentAttributeReader sends any non-positional attribute down this road
when a loot context asks for one.
The period matters when a KeyframeTrackSampler is built: for a periodic
track it bakes two extra segments, last keyframe to first on either side of
the loop, so a value interpolates across the wrap — tick 0, where the day
timeline’s period closes and ClockTimeMarkers.WAKE_UP_FROM_SLEEP sits —
instead of snapping, and KeyframeTrackSampler.sample reduces the clock’s
total ticks with a floor-mod before choosing one. EasingType supplies the
curve, and
the day timeline’s sun, moon and star angles share one symmetric cubic
Bézier whose two keyframes both sit at tick 6000, so the baked segment runs
noon to noon. The sun therefore turns slowest at its zenith — two thirds of
the linear rate — and fastest at midnight, at about six fifths of it. That is
why a Minecraft day is not two equal halves: the sun spends roughly 13,560
ticks above the horizon against 10,440 below.
The same value on the client
The client resolves the same stack and then does two things to it the server never does. Both of them are in the picture: one is about space, the other about time.
sequenceDiagram
participant Camera as Camera
participant EAP as Environment<br/>AttributeProbe
participant GS as GaussianSampler
participant SAI as SpatialAttribute<br/>Interpolator
participant EAS as Environment<br/>AttributeSystem
participant SR as SkyRenderer
Note over Camera,SR: one client tick
Camera->>EAP: tick(level, position) — once per client tick
EAP->>GS: sample around the camera
GS->>SAI: accumulate(weight, that biome's attributes) — 216 times
Note over Camera,SR: between ticks, once per frame
SR->>EAP: getValue(SKY_COLOR, partialTicks)
opt only on the first frame of the tick that asks for this attribute
EAP->>EAS: getValue(attribute, position, interpolator)
EAS->>SAI: applyAttributeLayer — weighted blend of every biome in range
end
EAP-->>SR: AttributeType.partialTickLerp between last tick's value and this one
The same stack, on the other side of the wire, with the client’s two kinds of smoothing around it. Read the two note bars as the boundary they mark: the 216 biome samples happen once a tick whatever the frame rate, and the opt box is the part of the frame nobody pays for twice — the stack is resolved by the first frame that asks and every later frame in the same tick only lerps.
The client resolves the same stack from the same data — it is never sent
a resolved value. What it adds is two kinds of smoothing the server never
does. In space, EnvironmentAttributeProbe.tick prunes, clears, then runs
GaussianSampler.sample over a 6×6×6 neighbourhood of quart-resolution biome
cells — the 4×4×4 blocks a biome is stored per — for 216 samples in all. The
weights come from a seven-entry kernel, 0-1-4-6-4-1-0, with each of the six
taps along an axis lerped between two neighbouring entries by the camera’s
sub-cell offset: the weights slide as the camera moves instead of snapping
when it crosses a cell. Those weights accumulate into a
SpatialAttributeInterpolator, whose
SpatialAttributeInterpolator.applyAttributeLayer applies each contributing
biome’s modifier to the base value and lerps the results together by
weight. Only the 21 attributes flagged
EnvironmentAttribute.isSpatiallyInterpolated are resolved that way; anything
else takes the single biome under the position. But that flag is not a gate on
the sampling: the probe runs its 216 samples every tick regardless, and the
test is applied afterwards, when the layer is applied. The server pays none of
it — it passes no interpolator at all. In time, each probed value keeps last tick’s
answer beside this tick’s and returns AttributeType.partialTickLerp between
them — and prunes itself, dropping any value nobody read during a tick. The
resolve itself is once a tick, not once a frame:
EnvironmentAttributeProbe.ValueProbe.tick nulls this tick’s answer, the first
frame that asks for the attribute fills it in by walking the whole stack, and
every frame after that in the same tick does nothing but the lerp. Only the
first frame in a tick that asks pays for the walk.
The probe lives on Camera, ticked from Camera.tick and emptied by
Camera.reset, and everything that draws the sky goes through it — the sky,
the lightmap, the two fog environments, the clouds and the music. That is why
lightmap, fog and sky never touches
EnvironmentAttributeSystem directly. The probe is not a wall, though: the
clock item reads sun_angle and moon_phase off
ClientLevel.environmentAttributes directly, and so does ClientLevel
itself for ambient particles.
The last difference is a tick wide, and it is the reason the two sides can
disagree about the sky for one tick at dusk. They invalidate at opposite ends
of the tick: ServerLevel.tick calls
EnvironmentAttributeSystem.invalidateTickCache before the world border and
the weather, then runs Level.updateSkyBrightness later in the same method,
once sleeping and weather have resolved; ClientLevel.tick does the reverse,
Level.updateSkyBrightness first and invalidation last, so the client’s
sky-darken value comes from the previous tick’s clock.
Questions players ask
Why does the nether have no night? It has no day timeline: the nether’s
and the End’s DimensionType.timelines both resolve to
TimelineTags.UNIVERSAL alone. Its environment is constants instead — the
dimension type pins gameplay/sky_light_level, gameplay/fast_lava,
gameplay/water_evaporates and twelve more, which
EnvironmentAttributeSystem.bakeLayerSampler folds into a base value that
never changes again.
Does setting the time in the overworld move the End? No: each clock keeps
its own ServerClockManager.ServerClockInstance. Every mutator does invalidate the
cache on every level at once, though — ServerClockManager walks
MinecraftServer.getAllLevels on each change, because a time jump must not
leave half a tick of stale sky behind.
Why do pillager patrols not show up on day one? Because
Timelines.EARLY_GAME is the timeline with no period, and its single
modifier track ands EnvironmentAttributes.CAN_PILLAGER_PATROL_SPAWN with
false until tick 120000 — a hundred minutes of play at the normal rate, sooner
for every night slept through — and with true after.
Nothing counts your days; the switch is one keyframe on a track that never
comes round again.
For a 1.21-era reader. Three sets of names moved. The gameplay booleans on
DimensionType— ultrawarm, bed_works, piglin_safe, respawn_anchor_works — are entries inDimensionType.attributesnow, and ultrawarm has become several,EnvironmentAttributes.FAST_LAVAandEnvironmentAttributes.WATER_EVAPORATESchief among them.BiomeSpecialEffectskeeps only the water, foliage and grass tints: sky and fog areBiome.getAttributes. And the villager Schedule class isTimelines.VILLAGER_SCHEDULE, a data-packTimelinelike any other.
Where to look
EnvironmentAttributes · EnvironmentAttribute.Builder · AttributeTypes ·
EnvironmentAttributeMap.Entry · EnvironmentAttributeSystem.Builder ·
EnvironmentAttributeSystem.bakeLayerSampler ·
EnvironmentAttributeSystem.invalidateTickCache ·
WeatherAttributes.addBuiltinLayers · Timelines · Timeline.createTrackSampler ·
AttributeTrackSampler.applyTimeBased · KeyframeTrackSampler.sample ·
ServerClockManager.tick · ClientClockManager.handleUpdates ·
EnvironmentAttributeProbe.tick · SpatialAttributeInterpolator.applyAttributeLayer ·
GaussianSampler.sample · TimeCommand
Rules: names, never code · how the system works, not how the code reads ·
newest version only · every backticked name passes tools/verify_names.py.