Project / what was built, when, and why
Session log
The dated record of what each working session built, what it measured, and what it
deliberately did not build. It was CLAUDE.md’s
“status snapshot” until it grew to 2 900 lines and crowded out the
standing rules around it. Newest first. The reasoning captured here
is the point — most entries explain a decision that a later session would
otherwise re-litigate — but nothing on this page is authoritative about
current behaviour. For that, follow the page each entry names.
2026-09-13 — the night's findings turned into a locus, and a shove that lost to a pathfinder
The overnight watch built the previous evening produced its first readings, and almost everything below came out of them rather than out of anybody's judgement. That is the point of the thing and it is worth saying once: three of the four changes in this entry were prompted by a measurement, and the fourth was prompted by a measurement being impossible.
Owner-verified in-game, 2026-09-13
- Hydrophobic works, and it heads for an edge afterwards. “It seems like the horse moves for an edge after too?” - which contradicts what this project has been telling itself. §0-BT called the gene half-built: “the 'and then heads for the nearest shore' half is not implemented - the horse is simply dumped where it stood… I would treat this gene as incomplete rather than untested”. Watched, it does head out. The translator only ejects; the heading-out is vanilla's own behaviour for a horse in deep water, which the gene inherits for free. The note was written from reading the code and was wrong about the outcome.
- F8 toggles correctly (§0-CQ). The red herd leader has been seen before but wants a herd, so that pen stays.
- Molten hooves: the prints are oriented correctly, and the black allele's prints do not glow.
- Hot blooded. The thaw is seen. What is left of §0-BT's entry needs a frozen lake - "check that a horse on a frozen lake ends up swimming rather than falling through into a hole" - which a flat fenced pen cannot provide at all, so the pen is retired and that question moves to the overworld.
- Molten hooves' black prints do not glow. “The black hoofprints do indeed look like they're not glowing” - which is the whole point of that allele and the one thing about it that could not be checked from an icon.
- Single-colour ooze drip. “Looks GREAT.” The first of the intake genes confirmed as good rather than merely present.
- Verdant, all three alleles - re-confirmed. Its pens were already removed on 2026-09-12 when it was first confirmed, so there was nothing left to retire; recorded here because the owner asked for it a second time and the honest answer is “already done” rather than a fresh change.
- The fireproof lava crawl. “Both that you float and that the rider doesn't take damage as long as they're mounted.” The float was already recorded; the rider's immunity in lava specifically was not, and the clause that matters is as long as they are mounted — dismounting mid-lake leaves the rider standing in it, which is correct and worth knowing before somebody “fixes” it. Only the speed decision is still open (gap 179: whether a crossing that slow is acceptable, or worth the project's first mixin).
- Lethal foals die at birth. The first thing in §0b ever confirmed in play — the trait, size and health system has carried “NOT play-tested” since 2026-09-04, and this is the part of it with the least margin for being quietly wrong. A foal that should die and does not is a silent failure; one that dies when it should not is a ruined line. The other four bullets under that heading are untouched, including whether the death message resolves rather than printing its raw key.
The dryad became nine alleles
The night proved the gene works — a sapling at 00:00:03, six logs and
fifty-two leaves by 00:01:09, the first dryad tree in the mod's history. It also
proved a sixth of its output was permanent litter: the translator
picked a species from pos.hashCode() and one of the six was dark oak,
which vanilla grows only from a 2 × 2. A gene planting one sapling
at a time in a random spot will never make a square. A dark oak planted at 01:28 was
still a sapling at 02:04 while its oak neighbour had been a tree since 00:01, and one
position logged ten failed growth attempts in forty-five minutes.
The owner's fix was the better one: put the species on the allele, so a dark-oak horse plants only dark oak and its saplings accumulate, and let the translator cluster them toward each other so the square completes on its own. Then the locus may as well carry the rest of what a dryad could plausibly be — mushrooms, flowers, and bone meal — which makes it nine variants plus the wild type.
Incomplete dominance, and why halving is the whole design
The rule asked for: recessive to the wild type as before, but two different variants both express. The question that decides whether that is a design or an exploit is what “both” means, and the answer is each at half rate. If a mismatch planted two things at full speed, every breeder would keep their lines deliberately mixed and a matched pair would be strictly worse than the thing you get by accident — at a locus whose most interesting allele requires a matched pair. Halving makes it a real choice: specialise for one thing reliably, or mix for variety at the same total pace.
It needed no new machinery, which is the tell that the model was already shaped for
it. The interval is epigenetic and lives on the copy, so a heterozygote
reads copy(0) and copy(1) separately and doubles each
— and GeneEpigenetics.copy(int)'s own javadoc already said it
existed for “a codominant gene, where both copies contribute at once”.
One guard for a whole family of bugs
Adding mushrooms and flowers would have reproduced the dark oak bug twice over: a
mushroom in daylight pops off next tick, a flower on the wrong ground never exists,
and both look exactly like a broken gene. Every planting word now answers
BlockState.canSurvive before the block is written — one call,
the game's own rule, no light table duplicated in this repo. Note what it
does not cover: a dark oak can survive anywhere an oak can, it just
cannot grow. That is why the clustering is a separate mechanism, and the
distinction is worth keeping straight.
Bone meal, refused in 2026 and granted the same year
This gene's javadoc carried a refusal: “a horse which auto-farms every crop you own, which is an economy lever nobody asked for and very hard to walk back once players have it… recorded here so it is not quietly improved later.” The owner asked for the allele. The note did precisely its job - it made the reversal deliberate rather than silent, which is all a note like that can ever do.
And the objection was answered rather than overruled: the cover refuses
crops. What is left is a horse that hurries a wood along, which is the
flavour the allele was wanted for and none of the economy the old note feared. It
also rolls the block's own isBonemealSuccess, so it is no faster than a
player with a stack.
Intimidating: the first change measured against a baseline collected before it
The night said the gene does not do what it claims: six cows placed at three blocks, sixty-four readings, median nearest 4.2 blocks and 63 of 64 inside the eight the gene promises. But it also ruled out the boring explanation — the first reading moved them from 3.0 to 5.6 within a minute, so the aura was firing all along.
The mechanism was wrong, not the wiring. repel was a velocity shove once
a second, and a shove loses to a pathfinder: the cow's own navigation walks it
straight back, because nothing ever told the cow's AI anything. It issues a
path now, toward the far edge of the radius — which is the
mechanism this same file had already chosen for the mirror-image case, and
followHorse's comment already contained the argument for it. The shove
is kept at 0.2, not as the mechanism but as the reading: it is what makes
the moment look like being driven off rather than like a cow wandering away.
What makes this one different from every other behaviour change in the project is that there is a number to check it against, collected before the change was made, by a test that runs itself. That has not been true here before.
And one test that could never have worked
The holy ward produced zero spawns in three hours.
BaseSpawner.serverTick opens with isNearPlayer and
requiredPlayerRange is sixteen blocks, so a vanilla spawner only runs
while somebody stands next to it — the one thing an overnight run guarantees
nobody is doing. No code changed: the answer is to leave the character in the spawner
room, since every other pen is force-loaded and does not care where the player is.
Worth recording that §0-CW's control line is what caught it. Without
“if there are no monster spawned lines anywhere, stop there”,
three hours of no hostile spawns beside a warded horse reads as a triumphant pass.
2026-09-12 — the yard learns to work a night shift, and the log learns to take readings
Two asks, an hour apart, that turned out to be one piece of work. First: “clear the completed tests out of the horse dimension, and add in new areas of pre-tamed horses in their stalls to test anything open”. Then, while that was being built: “please put in a bunch of event listeners … to help you detect what happened from the log. I'm going to leave the game and the horse dimension running over night. Add as many time-reliant tests as possible, focusing on those that don't require direct intervention from me.”
The second one is a different design brief from the one the yard was built to, and it rules out more than it lets in. A pen earns its place now only if its test starts itself, runs on a clock and leaves something readable in the morning. The first two the yard already did. The third did not exist anywhere in the repo.
Why ActionTrace could not do this job
It logs what a person does — a right-click, a mount, a foal — and
that is the right design for reconstructing a play session. Every overnight test is the
opposite of it: a sapling that appeared, snow that stopped melting, eggs that piled up
and then stopped, a population that did or did not plateau. None of those is an
event anybody fires. They are differences between two readings, and nothing in
the repo took readings. So DebugWorldWatch: each pen registers itself as a
named box with the blocks worth counting in it, a scan every ten seconds logs only what
changed, and a census every two minutes prints everything whether it moved or not
— plus the entity count and the real milliseconds per tick, which is the safety
net under the whole thing, because an entity storm is the one failure here that damages
a save rather than looking wrong.
The event hooks she asked for sit underneath that and answer the question a delta cannot: why. The best of them is the pair on items. A count says the egg total went from five to eight; the hooks say when each was laid and, crucially, the age of every item that left — because a 6 000-tick despawn and a cap refusing to drop look identical in a count and opposite in a log. Sounds are counted rather than logged: five horses meowing on a 300-tick timer is seven thousand lines a night, which would drown the four that matter. And the hooks throttle themselves at 400 lines a minute, on the principle that the tool built to find a runaway must not be the thing that makes it worse.
The force-load, which is the difference between a test and a wasted night
Minecraft only ticks what is near a player. The yard is 110 blocks deep and its far end is out of range of its near end, so any place to stand leaves part of it asleep — and in the morning that reads exactly like the gene at that end being broken, which is the same trap that cost most of two sessions already (a gene that does nothing and a gene forbidden from doing anything are indistinguishable from inside the game). So entering forces the yard's chunks and leaving releases them, with a sweep at server start for any a crash left behind.
What went in, and what came out
Out: the spontaneous-breeding field and the pack leader's pen, by her call — “we're done with those tests”. In: egg layer (four in one pen, which is the only arrangement that can make the cap fire at all — gap 207), a sound herd of three meowing and two singers, and intimidating with six cows ringed round it. That last one is the design note worth keeping: a repel aura measured by counting penned cows proves nothing, because they cannot leave — four in the far corner and four round its feet are the same number and opposite results. The watch reports the nearest non-horse distance instead.
One question left with the breeding field and is recorded as dropped rather than
answered (gap 206): whether
LOCAL_CAP actually stops an automatic breeder. The breeding is confirmed;
the cap never was.
Three bugs found on the way, none of which anything could have caught
- The yard's stocked pens were untamed while the kit's own legend had been saying “the yard's horses come tamed” since the day they were stocked — so “breed a starburst pair in the yard” was another instruction nothing in the world could carry out. Same shape as the mares-only kit found yesterday, in the file that already carries three comments about it.
- Taming them would have walked the whole yard home.
evacuateTamedHorsestook every tamed horse in the plot to the overworld on the way out; the moment the scenery became tame, “tamed” stopped meaning “yours”. It takes tamed and owned horses now — a player's taming sets an owner, the yard'ssetTamed(true)does not, and that is the line between somebody's horse and the furniture. - Spontaneous breeding was claiming foals that were not coming. Read
out of her own log: two foals at 22:22:50 and then 212 identical
PAIRED lines with nothing behind any of them. The post-breeding
cooldown is
getAge(), andcanFallInLove()cannot see it —Animal.aiStepzeroesinLoveon every tick where the age is non-zero. So a pair that had just bred read as available, were put in love again on every beat, and had it cleared the following tick, for five minutes. The gene was working; its account of itself was the thing that was wrong, which is precisely the failure this handler already had three comments about.
2026-09-12 — a play session that answered five genes, and a test yard that learned to throw things away
The long tail of the previous entry: the owner played it. Five behaviour genes came
back confirmed — spontaneous breeding, pack
leader, and all three of verdant's alleles — and not
one of them had ever been broken. Every one had been unrunnable. The kit only
ever handed out mares, so the breeding gene could not have produced a foal on any
hotbar it shipped. The dimension deleted every non-horse mob on arrival, so the pack
leader's cows and the holy ward's zombies could not exist in the yard built to judge
them. And GeneAbilityHandler.spread returned early in that dimension, so
the spreading genes were silent no-ops in the one place designed for watching them.
That is a pattern worth naming, because it cost most of two sessions.
A gene that does nothing and a gene that is forbidden from doing anything look
identical from inside the game, and the instinct on seeing nothing happen is to go and
read the gene. The tell in all three cases was in the scaffolding: the kit,
the dimension rules, the floor of the pen. So the yard now stocks itself rather than
leaning on a hotbar, all three dimension restrictions are off behind
gap 204 with RESTORE BEFORE 1.0 at
each site, and each growing pen is floored for its own gene — because
convert() is a whitelist per cover and grass, famously, only converts
bare dirt, which makes the obvious test (a grass gene in a grass field) the
one test that can never show anything.
Then the yard threw the answered questions away. Verdant's three pens and the pack leader's are gone. What is left is only the tests that need time rather than attention: the dryad, about a game-day between plantings; a new hot-blooded pen floored with snow and a strip of ice, the ice because it melts to a water source and that is the half of the gene that could flood something; and the breeding field, re-signed from “does it breed” to does it stop. An uncapped automatic breeder is the classic way to kill a server, and no amount of watching answers it — only leaving and counting.
The holy ward got the thing it actually needed, which was arithmetic. Its whole claim is a negative, and a negative cannot be watched: standing in the spawner room counting zombies tells you the spawner works, not whether the ward moved anything, because the ones it stopped are the ones that never appeared. Every hostile spawn is now logged with its distance to the nearest horse whose ward is live — asked of the resolved ability list rather than the genotype, since a carried gene can still be running nothing. A working ward reads as a floor under that column. A broken one reads as spawns at two blocks.
Three bugs out of the same session, all in shipped behaviour rather than scaffolding: spontaneous breeding was putting same-sex pairs in love (hearts over both, never a foal, since the gene was written — and the only visible sign of the gene working is exactly what it looks like when it cannot), the F8 highlight crashed on a horse that had left its team, and pack leader's label promised followers it does not summon. One is still open: horses keep the F8 outline after the highlight is switched off, because it lights them by proximity and clears them by proximity later — and effect timers do not tick in unloaded chunks, so a group left behind holds the outline frozen until somebody walks back. Gap 205, with the fix written down.
2026-09-12 — three quarters of §10, three genes that use it, and one word that had to be neither of the obvious two
Three of the four things left in §10 shipped: erase-down, the expression preview, and glow intensity. The first two turned out to be one feature seen from two ends, which is why they are one commit. What remains of the section is per-region colour ranges.
The dial: naming the knob that means “how much”
The format could always express a gene varying in strength — declare a knob,
point every threshold at it — but nothing could find that knob,
because “this one is the important one” lived in the author's head. So the
creator could not offer a handle for it, and a mask could not default to it. One flag
fixes both: "dial": true, at most one per gene, per-horse, never a seed.
It changes nothing at paint time.
The word was the hard part, and it was got wrong twice before it was got
right. “Expression” is what the owner and the roadmap both call it
and it is what a horse person would say — but the format's
expressions is already the table of outcomes, so a file would
have read "expressions": […] beside "expression": true
meaning something else entirely. It was renamed to strength on that
reasoning, and strength turned out to be a parameter on every colour op
already (how hard it pulls) — found while editing an example gene that had
"strength": 75 three lines below the new knob. dial is
named after being a handle rather than after what the number means, which is the one
reading neither of the others collides with.
pointsMin: a marking that gets smaller, not fainter
A weakly marked horse has a smaller blaze, not a translucent one. Every field
mask could say that already (wind a threshold and it covers less); a
PATH could not, because a drawing is not a rule — so a drawn shape
was the same on every carrier and a gene built from one looked stamped.
pointsMin is that shape at the gene's weakest, one point per point, each
morphing to its twin as the dial falls.
Morph rather than crossfade, decided before any of it was written.
Blending two coverage fields is easier and imposes no authoring constraint, and it is
wrong: a hard-edged mark fading into a different hard-edged mark ghosts
through the middle rather than shrinking. So the arrays must be the same length, a
mismatch is a load error, and the lerp is read through the control points inside the
sampler — no second path evaluated, no morphed array allocated, so a
PATH with a minimal shape costs what one without costs.
Both shapes are drawn on the one canvas behind a Full / Minimal toggle, with the other as a dashed ghost, because what matters about the minimal shape is how much smaller it is and that is invisible in a second editor elsewhere on the page. Starting one seeds it by shrinking the drawn shape toward its own centre. Every structural edit happens to both arrays, since the loader requires them twinned.
With a minimal shape on prismatic's crescent and nothing else,
reversing the lerp in the creator's port left check-parity.mjs
completely green. A drawing on a flank is a few dozen texels and
the fixtures sample four per part; the two sets simply never met. The page had
warned about exactly this —
the contracts note says to check that a
probe case reaches the new thing — and the way it was established was to
break the port on purpose and watch the check stay quiet, which is
the only way to tell "covered" from "not reached".
There is an answer table now (SpecFixtureTool.pathSection, 101 checks)
comparing the morph where it happens, at five points along the dial, straight and
curved and filled. The same sabotage now fails it on the first row. Do the
sabotage for the next one too: a green parity check is evidence only about
what it reaches.
Glow became a level, which reverses an old call on purpose
Emissiveness was one bit per texel, cut at a coverage of 0.5, and
GeneSpec said outright that there was no half-lit. That cut was always a
stand-in: it existed so a soft mask would not bloom a glow two body units
wider than the shape that drew it, and it paid for that with a hard line across a soft
edge. The renderer turned out to make the better version free — the emissive pass
is BlendFunction.TRANSLUCENT over the coat, read out of
RenderPipelines.EYES rather than assumed — so an intensity written
as the second texture's alpha is a true dimmer rather than a darker
colour.
So CoatOverlay holds a float per texel, a layer's emissive is
a Value (true, a number, a $knob, the dial), and
the mask's coverage scales it — the author draws the falloff once, in the mask.
Two genes over one texel take the brighter rather than summing, since there is nothing
above full bright to spend an overlap on. Every glowing gene in the game
changed: fifty-odd layers fade at their edges now.
§0-CO names the five soft-masked
genes to look at and the control group that should be identical.
Particle intensity is deliberately not part of it (owner's call).
Particles are the emitter effect, a different system with its own rate and
count.
Two bugs found by looking rather than by being told
- The creator dropped a layer's glow from its export entirely. Load a
glowing gene, export it, and the light was out -
tidyrebuilt each layer from name, masks and op, andemissivewas in none of those. It had no control either, so there was no way to notice. Now written, validated (range, phase, thePIGMENT/LUMAmasks the loader refuses) and round-tripped, withwiki/tools/check-glow-control.mjsto keep it that way. That check exists because the creator's preview is a flat sheet with no lighting to be brighter than: where a control has no picture, the check is the feedback. CreatorMetadataRoundTripTesthad been red at 16 of 23.AbilityTypegrew seven verbs (sound,produce,teleport,summon,temper,bond,ward) and the all-verbs fixture did not follow. The creator's form offered all seven, so the drift the test exists to catch had not happened - only the fixture was stale.
And then somebody played it, which found four things no test had
The owner ran the client on this machine and worked through the kit. Everything below came out of about twenty minutes of that, and none of it was reachable from the repo: two confirmations, a wrong instruction of mine, a crash, and two tools that do not work.
The features are right. Starburst's emblem “does indeed look like a smooth variety of sizes” - the 45% floor gives a continuum rather than two sizes with a gap, which was the open question and the whole bet of the minimal shape. Lantern shows “different light levels” per horse, Tron has “a glowing diffuse painted along the body for both forms”, gamma, geode and angler read right, and suntouched - the control, which lights whole parts at full alpha and must look untouched - “does indeed look the same as before”. §0-CO is closed on that pair of readings: changed where the masks are soft, identical where the path is old.
The checklist I wrote for §0-CO named five genes to look at and picked them
by the wrong property: whether the glow sat behind a field mask rather than a part
list. That is not the same question as whether the mask has a soft edge.
Measured afterwards, ember veins' lit layers are 0.04 to 0.2 and data rain's are
soft bands multiplied by a SPECKLE - hard by construction, so
coverage-times-level is level-or-nothing and the change cannot show. The owner
found both in a minute.
The rule worth keeping: masks fold by MULTIPLY, so the hardest mask in a
layer governs its edge. Geode's glints are a WAVES at 0.09
inside an AXIS at 3.0 and the product is as hard as the 0.09. The
softest lit mask in the mod is gamma's SPOTS at 1.8, and gamma is the
gene that actually demonstrates the feature. On the
format page now, next to the other thing that session taught: a glow has no
colour of its own, so Tron's two haloes at an identical 0.25 read differently
because one sweeps 300° of hue and the other is flat.
F8 crashed the server, and had been lying about it for four minutes first
Reported as “F8 isn't toggling off, only turning on”, and then, minutes
later, “the game just crashed”. One cause. The horse
highlight puts herd leads on a scoreboard team and took every nearby horse
off it again - but only leads were ever added, and
Scoreboard.removePlayerFromTeam(name, team) throws for an entry that is
not on that team. It threw on the first ordinary horse it reached.
- On the OFF press it threw inside the packet handler, after the player had been removed from the on-set and before the "OFF" message - so the toggle silently un-toggled and the next press said ON again.
- Four minutes later the auto-off ran the same line from
ServerTickEvent.Post, where nothing catches it, and the server died with Exception in server tick loop. The gap between the last keypress and the crash is exactly why the two reports did not look related.
Fixed by asking the scoreboard first, and by wrapping what the overlay does so that
anything else going wrong in there switches the overlay off rather than ending
the tick loop. Worth being blunt about: this binding is registered
outside the production gate on purpose, because the testers asked for it, so
"it is only a dev tool" was never true of it and a released jar could hit this.
§0-CQ, with the API asymmetry on
api-notes - addPlayerToTeam is forgiving,
removePlayerFromTeam is not.
Two tools that do not work, found by using them
- The test kit only ever hands out mares. Every preset egg in it is
Sex.FEMALE, andHorseBreedingHandlercancels same-sex pairings - so batch 3's spontaneous-breeding test ("spawn FOUR in a wide fenced field, come back and count") cannot ever have produced a foal, and neither can the starburst breeding slot written the same day. A test that cannot pass is worse than no test: it was on the checklist looking like coverage. - "Make egg" in the custom spawn screen does nothing. No egg, no refusal, and - decisively - nothing in the log, where every refusal on that path logs at INFO. So the packet never left the client. The button is not greyed (the owner checked), which leaves a widget sitting on top of it eating the click: the same family as gap 139, where "Clear genes" and "Make egg" overlapped in a real window.
Both were fixed the same day. The kit now hands out both sexes wherever a batch breeds, with the rule written into the class so the next author inherits it rather than the bug. And the screen's right column pairs its last two buttons onto one row - six rows instead of seven - which is what pulls the top group off the pinned one at 270px; the designer's copy of those constants was changed with it (hard rule 5). The screen now also says in the log when the two groups collide, because the failure it produces is a button that looks alive, is alive, and cannot be pressed, and that leaves nothing to find. §0-CR.
Then three genes picked it up
A format nothing uses is a format nobody has tested, so the same session put the three features into shipped genes — one each, chosen because the gene was already describing the thing it could not say.
- Starburst scales. All
twenty-one of its paths are centred on the same point in units space, which is
what makes a minimal shape possible at all here:
pointsMinis each path scaled about that centre to 45%, so the rings stay concentric and the spikes stay registered to the core at every size. Thesizeknob's range is deliberately the scale itself (0.45 to 1), so the number a horse drew reads as what it means. - Lantern and Tron burn. Both were already a lit core beside an unlit halo — which is what you write when a glow is a bit and a halo can only be painted. The halo is lit now (22% and 25%), the core carries the gene's dial, and the falloff is free: a glow is scaled by its layer's coverage, and both haloes' masks already faded.
The haloes are flat constants rather than a share of the core, because the format has no arithmetic - "a fifth of the core" is not expressible - and a constant under the dial's own floor keeps the core the brighter of the two on every horse. That is a real limit found by using the feature, and it is the first thing a knob-driven glow wants that it cannot have.
Adding a knob to Starburst made DeadLayerTest fail on "black shards in
the spikes", and the obvious reading - the shrunken emblem starved the shards - was
wrong. Setting pointsMin equal to points,
so the geometry could not have changed, failed identically: what had happened was
that the extra knob re-rolled that gene's draw for the one horse the test seeds.
Measuring it settled what was actually true. That layer is a sparse
SPOTS field inside a shape a few units across, and over twenty horses
it painted nothing on five of them. It had always been a coin
flip; the test had only ever seen the one horse, which happened to land. So the
fix was the gene, not the dial: 2.8 / 0.5 instead of 3.4 / 0.4 lands on every horse
at every size and keeps the density it had at full size. A layer whose
existence depends on a seed is one re-roll from dead, and the only thing
that finds those is measuring across horses rather than looking at one.
What is not built
- The creator still cannot show a glow. Its preview is an unlit sheet, so there is nothing to be brighter than. Fixing it means shading in the preview, which is a different job from anything in §10 (gap 201).
- Per-region colour ranges are what is left of §10, and the
first question is whether a colour becomes a
Valueat all — every colour in the format is a literal today, while a hundred-odd genes already carry ahueknob feeding aRAMPaxis, so the answer may be a different shape from the one the roadmap assumes.
2026-09-11 — the two items at the top of the roadmap, both of them
One session, the roadmap's first and second entries. Both were long-standing, both turned out to be smaller than the page implied once the groundwork underneath them had shipped, and both are now waiting on a person rather than on code (§0-CL, §0-CM).
The PATH drawing canvas
A PATH mask's control points are handles on a view of the horse now.
Click to add, drag to move, a hollow dot mid-span to insert, right-click or
Delete to remove, arrows to nudge; the textarea survives, folded under
the numbers, and the two write the same array. It changed no format, no
engine and no parity fixture — which is exactly what the entry predicted when
it argued for shipping the mask first and the surface after, and is worth recording
as a case where sequencing a job that way paid.
Two decisions inside it are the ones a later session might undo without knowing why.
The silhouette is the model's own body-space boxes, part by part,
because that is precisely the set of (u, v) the mask can reach — a
prettier horse drawn by hand would be a drawing of a horse rather than a map of where
a point lands. And both axes are drawn at one scale in body units, so
the top and front planes letterbox rather than stretch: the
alternative fits the box better and makes every circle an ellipse on the animal. The
curve is SpecPainter's own Catmull-Rom, reached through two new
spec-engine exports rather than a second copy of the maths.
Every mask parameter is wrapped in a <label>, which forwards a
click anywhere inside it to the first control it contains. The canvas's toolbar
sits inside that label, so every click on the horse was also pressing
Reverse and the drawing came out backwards one point at a time. It would
have been baffling to hit by hand and was obvious from an automated click
sequence that printed the array after each step. Controls made of several widgets
get a plain block now, not a label.
Verified by driving the real page in headless Chrome — 26 checks covering click placement, drag isolation, insert ordering, deletion, the textarea round-trip, the foal mesh, and one-scale mapping in all three planes. The driver lives in the scratchpad rather than the repo; landing it as a checked-in tool was offered and not taken up, so it is a candidate rather than a gap.
The Java gene API, and an open registry
TwoAlleleGene answers seven of Gene's eight methods from a
fluent declaration; AbstractNaturalGene and
AbstractMagicalGene leave the subclass restrict() or
tint(). The short path is the
write-up.
The part worth keeping: AbstractAbilityGene was moved onto the
same skeleton. It is where this shape was first noticed, seventeen genes
ago, and folding it in is the difference between a generalisation and a second copy
of one — it kept its positional constructor so all seventeen call sites were
untouched. Mushroom (natural) and
healer (magical) were converted as worked examples, and
both coat goldens are unmoved, which is what says the conversions
preserved behaviour rather than a reading of the diff.
Genes.register takes any Gene now, validates the namespaced
key, then freeze()s. The freeze buys no speed — the
orderings were already computed once and cached — and the entry that asked for
it implied otherwise; what it actually buys is a named failure instead of horses whose
loci have all shifted by one segment, which reads as a breeding bug and is not one.
Two things deliberately not built
AbstractPatternGene. §8 said it would absorb "theBodyNoiseboilerplate and the pull-N-knobs-off-the-expressing-copy helper". Neither is boilerplate: reading epigenetics is the single linectx.epigeneticsFor(KEY), andBodyNoiseis a static call whose arguments are the design of the pattern using it. A base wrapping those would hide the trivial half and constrain the interesting one. Write it from a third pattern gene's duplication, if ever — not from the entry.- Proof that another mod can register a gene.
RegisterHorseGenesEventfires fromFMLConstructModEvent's deferred work queue, which is the one moment when every mod has been constructed and nothing has parsed a genotype code. That sequence was read off the loader's ownconstructModsrather than assumed, and it compiles — but it has never run with a second mod present, and no test in this repo can make it. Flagged in the class note and on §0-CM, which carries a cheap log-line check that needs no second mod.
Also: making-a-gene.html documented
geneCarrotHomozygous() as defaulting to false; the code
changed to true on the owner's call the day before and the page had not
followed. And the designer's wasm was re-baked — common/ moved, so
the browser had been running the previous build.
2026-09-11 — a playtester session, and the roadmap stops being fiction
Two halves. The first was a sweep of the roadmap against the code, which found that a large part of it described work already done. The second was a live bug queue from outside playtesters — the first time this mod has had reports from people who did not write it — and most of those turned out to be missing feedback rather than missing behaviour, which is a pattern worth naming.
Three things that were never broken, only silent
“The diet gene doesn't appear to work.” It expresses on about one horse in eight. What it never did was report: healing a horse already at full health is a no-op and the bond award declines silently at max bond or the daily cap, so feeding a healthy, well-bonded horse spent the item, played the sound, showed the hearts and told the player nothing. Indistinguishable from a gene that does nothing. The food is still always eaten — it was never the refusing that was wrong — and the player is told when both halves declined.
“A horse randomly got bigger, and it tracks bond.” Nothing
in the mod grows a horse, and the negative is provable: one write to
Attributes.SCALE, a setBaseValue so it cannot accumulate, over
inputs fixed at birth, guarded to run once. It was a foal reaching adulthood. The bond
correlation is a shared cause: feeding awards bond and does not cancel the
interaction, so vanilla's feed runs too, and on a baby vanilla's feed is
ageUp. Hand-feeding a foal for bond accelerates its growth with the same
click. Gap 196.
“An aura of glowing entities round a horse.”
MobEffects.GLOWING is applied in exactly one place in the mod — the
F8 highlight, which lights every horse within 96 blocks, in a dimension that is wall to
wall with pens. The effect carries no particles and no icon and refreshes for as long as
it is on, so left on by accident it is indistinguishable from a bug. It times out now
and says how to stop itself.
Name tags, and a gate asked on the wrong side
The one report that was a plain defect. HorseInteractionHandler runs on both
sides and cancels the name-tag interaction, but only when
HorseRecords.hasRealRecord says the horse is ours — and that asks the
attachment, which is never synced. Client-side it was always false, so the client
never cancelled and did what vanilla does for a tamed horse plus an item: ride it.
The rename window opened and the player was put on the horse at the same time. The client
does know which horses are ours, in ClientHorseRecordCache, so the question
is asked in a Dist.CLIENT subscriber. The stick and clock branches in that
same file carry a comment saying to cancel on both sides for exactly this reason.
What the audit found
Part 1 of the roadmap is finished. Of its five “genes drawing the wrong thing”,
one was real, one was real and misdiagnosed, one was a decision and two were
already built; the attribute verb it called inert has been dispatched
for some time, with the class javadoc still saying otherwise. Epigenetic drift — item
two of five in “where to start” — is built, written up on
breeding, and had only never been deleted. Bareback
steering was listed as unbuilt while a playtester was confirming it works. The bond heart
particle likewise. On the gaps page, 38 of 183 entries are already marked closed and 41
more need play rather than code, so the real backlog is nearer 97 than 183.
The one that got away
Changing CarrotEffect's shape broke web/DesignerApi, and
nothing noticed for eleven commits: :web is not a dependency of
:neoforge-26.1.2:build, so every build was green, the tests were green,
and two commits of common/ shipped with a stale wasm beside them. It
surfaced only because the end-of-session routine runs
:web:bakeDesignerAssets by hand. Gap
199 - the check that would have caught it does not exist, and
gap 45 assumes somebody looked at
git status.
Also built
- Magic zebra is codominant, tuned off a rendered dose ladder rather than guessed - 0.18 read as black on a bay and was indistinguishable from the homozygote.
- Particle stops lying about carriers (owner's call): two variant alleles that are not partners now show nothing, where the lower-ranked one used to show and make a cross-family heterozygote look exactly like a homozygote.
- The foal sclera - the rects were 2 texels wide where the eye is 4, on the strength of a comment claiming the foal template has no sclera. It has two columns of one. All 528 foal golden rows moved; no adult row did.
- The family tree runs both ways - Ancestors, Descendants, Overview, with Back and Home, on data the client was already being sent.
- A gene carrot names two alleles, which is stage one of the
allele-paper design and fixes a limitation
nothing had recorded: the pair came from
alleles().get(0), so the forty-allele particle locus could only ever be spliced to dust. - Horses come to food, and run for their favourite - the only way the preference locus is visible before you have fed it.
- Gene and allele pickers on My horses, listing only what the roster contains; /bond for testing the tiers; F8 in production with the herd lead in red; a portal every 25 pens in a corridor that had one way out in seven thousand blocks.
2026-09-11 — three coat defects, and a roadmap that had stopped being true
The session began as “go through the whole roadmap and clear it out”. It turned into something more useful: auditing it against the code first, because a large part of Part 1 turned out to describe work that was already done. Three real defects were fixed on the way, and the page lost two whole entries that had shipped and never been deleted — which is the exact failure its own banner warns about.
What the audit found
Of the five rows under “genes drawing the wrong thing”, one was a
real defect, one was a real defect misdiagnosed, one was a design decision, and two were
already built. Particle’s carrier case
was correct, documented in its own javadoc and asserted by a passing test;
light’s glowing hooves are wired end to end —
the gold tint and markEmissiveLowerLeg through to
EmissiveCoatLayer. Under “behaviour that silently does nothing”,
the hurt-mare denial message shipped on 2026-09-10 and was still listed as missing, while
sitting in the same page’s own Settled section. Only the inert
attribute verb survived the sweep.
The larger find is that epigenetic drift is built — item two of
five in “where to start”, whose stated problem was that a foal copies its
parent’s seed exactly. EpiDrift nudges every scalar at every breeding
on a two-sided exponential, re-rolls seeds and categories on a chance, and clamps to the
hard safety bound rather than the design range, so a long-bred line may reach somewhere no
wild horse has been. Its own javadoc opens “Before this, inheritance was
verbatim.” It shipped without the per-gene opt-in flag the roadmap proposed
for the job, and it is written up on breeding, so the
write-up had happened and only the deletion was missed. The section is gone, and the
Settled entry that still said “verbatim” now says what is true.
Magic zebra is codominant
One Mzeb copy drew the whole pattern, so the locus had nothing between a
plain horse and a finished one. It follows natural
zebra now: one painter taking a strength, two outcomes, so a dusky
parent and its black-striped foal cannot end up wearing different bands.
Mzeb/Mzeb passes 1.0 and is byte-for-byte what it always drew;
Mzeb/n is a new dusky outcome scaling the subtraction and the
opacity together — scaling only the colour would put a dusky band at full opacity
over the template, which on a dominant white is a grey stripe as strong as the black one.
The strength is 0.09 because it was picked off a rendered dose
ladder, not reasoned about: the first guess of 0.18 read as black on
a bay and was not distinguishable from the homozygote. Three base coats by three genotypes
in one image settled it in one look, which is the cheapest thing in this repo and is still
under-used. 48 Mzeb/n rows of the pipeline golden moved and the 6
Mzeb/Mzeb rows did not.
Particle stops lying about what a horse carries
The owner reopened this one deliberately. Two variant alleles that were not codominant
partners used to show the lower-ranked of the pair, which meant a horse trailing dust was
either Dst/Dst or Dst over any of the other thirty-nine
— identical to look at, and unguessable. The locus asks for
agreement now: the same allele twice, or two of one family, and anything else is two
carriers and a plain horse. It cost nothing in the wild, because the founder table already
excluded exactly these pairs for exactly this reason. The rank rule went with it;
declaration order survives only to keep AllelePair canonical so
copy(0) and copy(1) address a codominant pair in a fixed order.
The foal sclera — and what it was not
The owner-reported “the coat overwrites the sclera” was real, and both of the
roadmap’s suggested causes were wrong: the composer already redraws the eyes after
both paint phases, and the coordinates were on the eyes.
EYE_RECTS_BABY was 2 texels wide where the eye is 4, on the
strength of a comment in the file asserting that the foal template has no bright sclera.
It has two columns of one beside each pupil. So every white painter and every magical gene
painted over the whites of a foal’s eyes and the redraw put back the black alone.
Worth recording how it was settled, because reasoning about body space has been wrong here
before: the fix was checked by dumping the template’s actual pixels and by asking
HorseSkinGeometry which columns the head’s faces own, rather than by
deriving it. An intermediate claim that the adult right eye was misaligned came
from feeding mistyped coordinates to a probe and was withdrawn on reading the source
— the adult has been correct since 2026-09-06. Every one of the 528 foal rows in the
pipeline golden moved, and not one adult row did, which is what a foal-only fix should
look like.
CoatBakeGoldenTest stayed green through the whole zebra change, and
correctly: it walks every gene but pairs each variant allele with itself
(new AllelePair(variant, variant)), so it never bakes a heterozygote and
is structurally blind to a codominance defect. That is not a fault in it — it
exists to catch a gene whose paint moved — but it is worth knowing which half of
the space it covers before trusting a green run to mean a coat did not move.
2026-09-11 — a test kit you can finish, and a mask for liquids
A play session run as a conversation: the owner asked to be set up to test, tested, and reported back, several times over. What the day produced is a test kit that is worked through rather than looked at, three defects found by that testing and fixed, and one new mask in the drawing engine. Nothing below has been seen in a client since the last change; the checklist is the authority on which parts the owner confirmed as they went.
The kit: one hotbar, and a batch is deleted when it is done
- The kit filled all 36 slots and printed a 30-line legend. The owner: “SUPER
overwhelming. Let's start with one hotbar's worth of tests at a time.” It is
themed batches of at most nine now, batch 1 given on login and the rest behind a
dev-only
/testkit n, so moving on costs a line of chat rather than a rebuild and a fresh world. A batch is deleted once it is worked through and the rest move up, so batch 1 is always the next thing to test - the breeds, size and rebuilt-intake batches all went that way during the day. - Every batch that tames carries a stick and every batch that breeds carries golden carrots, on the owner's reminder. Preset eggs carry a genotype and no epigenome: a stored one made every horse from one creative egg the same horse, which is why a stack of trillium all stood in the same place.
- Files the kit cannot make were prepared in the dev run's
run/phc/- drop-in breeds (a dark-forest paint, a night-only cremello, one naming a missing gene, one duplicating the Friesian), a drop-in gene, and abreed-spawning.tomlmoving the Friesian and switching the Morgan off - and a dedicated server confirmed all of them load. - By the end the owner asked for tests that are not “does this look pretty in-game”, so the front two batches are now §0-BT's own ranking of what fails badly rather than visibly: spontaneous breeding left in a field for days and then counted, a ward beside a working spawner farm, pack leaders in one stable, a dryad left half an hour; then ender echo under a rider, hydrophobic, food preference, ocean-born.
Three defects the testing found
- The debug stick did not count as taming. It called
tameWithNamebare, which never firesAnimalTameEvent, soGeneDiscoveryHandler.onTamenever ran: a stick-tamed Morgan and Friesian stayed “???” in survival's breed book, with no genes discovered and no tame-a-mare tick. Hand-taming had the same bug once (gap 154) andCrouchFeedGoalfires the event first; the stick does now. This is why two breed-book checks looked broken and were not. - Vanilla's
minecraft:villagePOI tag had not loaded since 2026-09-07. It namedhorsegenetics:cowboy_hitch, which stopped being a POI when the hitch and the table were split, and a tag with one missing entry fails whole - taking vanilla's own village POIs with it. Found in the log while checking something else. Waiting: §0-CD. - Bare stripes along the spine on both drip genes:
WAVESdisplaces the whole band, so a far edge at 1.05 with a 0.2 amplitude dips to 0.85 of the barrel once per wavelength.
Drawings that stand somewhere, and a gene that lied about itself
- Six SVG barrel drawings - taper flame, barred wing, candelabra, inkcoil, trillium,
uraniid - all placed their viewport at
originU0.27, so every horse wore its drawing in the same spot.originUis a per-copy knob now (posU0.12-0.40): shoulder to hip, checked on a side-view render at six seeds each, and it inherits with the allele copy. - Taper flame was “a very very narrow white streak” - as traced the flame is 150 by 752 in its viewBox, three texels wide on the horse, with an outer skin under one texel. Every mask widens it 2.6× about its own centre line now; 3.2 was tried and reads as an egg.
- A preset egg names only the loci it is given and leaves the rest at their defaults, which comes out black - so the black barred wing and the nightbell foxglove were invisible in the kit. Both spawn on a palomino.
- Rainbow Drip is Ribbon Drip. Two horses in three carry the pale
allele, whose rim is grey to white, so the name argued with the horse in front of you:
“the name makes it SUPER confusing”. The key stays
horsegenetics.rainbow_drip- it is the genotype code's spelling - and the coat golden confirms no horse changed.
GOO: the first mask for a liquid
Ooze drip “looks like triangles”, and needed to be “more irregular and
curved”. That is not a tuning problem: WAVES is one periodic function,
so every lobe is the same length, width and spacing, and at the two or three texels a lobe
gets on a barrel a sine is a zigzag. A liquid is not periodic - the whole read of
a drip is that one ran further than its neighbour. The owner gave permission to extend the
engine for it.
- A distance field, not a displaced line: a half-plane for the band, a
capsule per drip, a disc for each tip, unioned with a polynomial smooth-minimum so the
joins fillet like a meniscus. Every drip draws its own length, width and place from the
seed;
chanceleaves cells empty;sagbites an arc out of the edge between two drips, because a ruled line with beads on it reads as a wire. Onlyfromandtoare in the band's space - every other length is body units, because a drip has to stay round. - An end face is carried round the corner. The rump has one value of the band's own axis over its whole surface, so it took a single drip's answer for the lot and came out a slab, which is what the owner saw. The pattern continues round from each flank and the two meet in the middle of the face.
- All five places a mask touches:
SpecSchema,SpecPainter, the creator'sschema.jsandspec-engine.js, and both docs tables - plus aprismatic.jsonlayer so parity reaches it. It was watched go red: a port returning zero fails 43 checks and a missing corner wrap fails one, but a 2% change to the field and a changed sag bite both stay green. Gap 194 records exactly that, because “parity is green” is worth only what the probe can discriminate.
A log line instead of a chore
Checking that a breed carries only the disorders on its sheet meant taming horses one at a
time and reading their genes, which the owner declined - reasonably. Every breed founder
the server rolls now logs a [breed-health] line of its health genes against its
sheet, with a WARN ... NOT ON ITS SHEET for anything the sheet does not list.
Two dozen Quarter Horses and two dozen Thoroughbreds later: three HYPP carriers, no
warnings, and the answer came out of a grep rather than out of an evening.
Owner-verified in-game
- The custom egg's Breed list starts at Feral Mixed and carries drop-in breeds; gene hover pictures.
- A drop-in breed registers, fills its Breeds row when tamed in survival, shows its About
text, and its herds spawn wild in the biome its file names - the
breed_herdsbiome modifier, which nothing had tested. - A moved Friesian's entry gives the new biome and its herds are found there; a night-only
breed's herds appear only after nightfall (9 founders after
/time set night, none before); a switched-off Morgan keeps its label and its entry, and has no egg in the creative tab. - A drop-in gene loads from
phc/genes/and draws. - Size by copy count, all of it: Clydesdale
Big/Big, ShireBig/n, FalabellaSmall/Small, ConnemaraSmall/n, and two Connemaras throwing all three kinds of foal - including the ordinary-sizedn/nat exactly 1.00x. - The rebuilt-disjoint intake marks: corolla, agate eye, barred wing, uraniid, trillium, foxglove - and, after the fixes, the widened taper flame, the drawings moving along the barrel, and the dark forms on a palomino.
2026-09-11 — every folder in phc/
The owner, after 0.5.000: “change the folder situation to all be in
agreement.” Drop-in genes move from config/horsegenetics/genes/ to
.minecraft/phc/genes/, beside breeds/ and the
three settings files, and nothing of the mod's is in config/ any more
(ModGeneSpecs.FOLDER is now ModBreedSpecs.ROOT + "/genes",
resolved against the game directory). Both folders' READMEs name the other and the
settings. Gap 192 closed.
Shipped as 0.5.001, on the owner's
call that a folder move is worth its own release; the notes carry the warning. It was
not in the 0.5.000 jar, which is why the warning matters: a
gene left in the old folder is not loaded, and a saved horse's segment for it is
dropped on load like any unregistered gene's - the player moves the file and restarts
before opening the world, or loses that gene from every horse that carried it.
The dev run had no drop-in genes, and its stale run/config/horsegenetics*
files were deleted (their values had already been copied into run/phc/).
2026-09-11 — 0.5.000: a world decides which breeds it has
The owner's brief for the release: the big thing is custom breeds; before shipping,
move the settings somewhere sensible relative to the breeds folder, and let a world set
the spawn rate and location of every shipped breed - down to switching all of them off,
Feral Mixed included, with a horse that has nothing to be simply not spawning. Commit
94332fb; tagged 0.5.000. Waiting in
game: §0-CC.
One folder
.minecraft/phc/holds everything a player is told to touch: the breeds folder and three settings files. NeoForge resolves a config's file name againstconfig/with a plainPath.resolve, so../phc/server.tomllands beside the breeds (ModBreedSpecs.configFile; api-notes). Read from the FML source, then seen on a dedicated server - including the file watcher re-reading an edit through the...- The gene folder was not moved (gap 192, closed the same day - see the next entry up): a drop-in gene is a segment in every genotype code, so moving its folder under a tester is a different kind of change from moving a settings file.
breed-spawning.toml
- Generated from the registry in the mod constructor, after the
breeds load: a section per shipped breed (
enabled,spawn_weight,biomes,spawn_time, each defaulting to the breed's own file),shipped_breeds_enabled, and aferal_mixedsection (enabled,biomes,herd_weight). Drop-in breeds are not listed; they are already files. - COMMON, not SERVER: the herd biome modifier runs at server start and needs it, and a SERVER config is synced to clients whose spec was built from their own breeds. The price is that it is per install (gap 190).
- It works by changing what the registry publishes.
Breedskeeps each breed as its file says and publishes copies with the settings applied (BreedSpawnSettings, incommon/, tested byBreedSpawnSettingsTest), swapped whole because the file watcher re-reads off the server thread. A switched-off breed loses every source but keeps its entry, so a horse's label still means something. Herds, the biome modifier, the cowboy, stables, egg loot and the breed book needed no change beyond asking the same registry. - Nothing to be, nothing spawned.
BreedSpawnHandlercancels a natural, chunk-generation or spawner spawn whenBreeds.anythingMaySpawnsays no;HerdManagerdiscards what gets past (a lone horse where Feral Mixed is off)./summonis left alone - a player asked for it. - The first boot crashed, which is why the server was booted at all:
defineInListwith aList.ofthrows on NeoForge'scontains(null)probe while writing a fresh file.
Old worlds
- Diffing the gene bundles against the
v0.4.1tag found one retired allele set: Cleave's four, from when it became two loci. A strict parse of a saved horse carrying one throws, and from the entity tick that is a world that will not load. Saved codes now go throughGenotype.readableStoredat the codec (records, transfer papers, seed jars), which drops what it cannot read the wayparsealready drops unknown genes.parseitself stays strict. Unverified on a real 0.4.1 world (gap 193).
Tests run: BreedSpawnSettingsTest, GenotypeTest, the breed
classes, creator parity; :neoforge-26.1.2:build green; a dedicated server
booted clean twice. The full :common:test was not run - it would still need
to confirm that nothing else reads Breeds in a way the published copies
change. The debug world kit was not re-aimed: nothing in §0-CC is an item.
2026-09-11 — a breed is exactly its sheet; the designer draws, asks and explains
The owner's fourteen-point list after walking the new designer, and one principle that fell out of it: now that breeds are designed, magic in a breed is a design decision. Everything is waiting in game.
No stray magic, no background disorders
BreedFounderforces every magical gene and every disorder the breed does not name to wild. The geometric magic draw,magic_chance, the white/blacklists andhardyare gone from the record, parser, writer and all 49 files;BreedFounder.platewent with them, since a founder is now what the plate was. Feral Mixed - the unbred population - is the only place random magic or an unlisted disorder comes from (breeds).- Nine breeds relied on the background rate for disorders their sheets name, so they now list them at the wild carrier rates: HYPP, PSSM1 and HERDA on the Quarter Horse and Paint, SCID, CA and LFS on the Arabian, GBED and HERDA on both Mustangs, PSSM1 on the Percheron and Shire, ACAN on the Miniature and Friesian, megaesophagus on the Friesian. Gap 68 (“disorders are global”) closed by the same move.
- Stables were left alone: a generated stable still adds its own extra magic to the breeds it places (stables). That is a separate ladder, and whether it should go too is the owner's call.
- The spawn egg's breed list starts at Feral Mixed instead of
“(none)”, in the screen and the horse designer. Drop-in breeds were
already in it -
Breeds.all()includes them on the client.
The designer
- Health, speed and jump are always asked, and every number on
those steps is the game's own curve (
BreedStatCurve.factor, exported asstatFactor) turned into hearts, m/s and blocks, with a 1-10 table and what each score means next to vanilla's range. - The eye step is swatches (
eyeHuesJson): iris, sclera, wedge and wedge colour, glow and third eye, with “same in both eyes”. Iris and sclera start named at brown and white. Above them, a strip of two dozen rolled founders' eyes -breedFounderJsonnow returns whatEyes.resolvepaints - so champagne's amber and a white pattern's blue appear on their own, and the gene responsible is named. The page does not know which genes do that;Eyes.forcealready did it. - Ticking a gene names one pair: the outcome measured to cover the most, on its homozygote where it has one; eye genes at their wild type. Check / uncheck all and “every pair with X” on genes of three or more alleles.
- Epigenetics are always open under a chosen gene, one row per value: wild, within a range (two sliders) or the same on every horse. A colour triple is one colour picker; a hue is a hue slider; a seed is “same pattern” with a shuffle.
- Colour search: type any CSS colour and see only the genes whose
numbers can be a colour (
colourableJson, derived from each schema - three_r/_g/_bchannels, or a hue scalar spanning 0-360); adding one names its coloured form and locks the numbers to it. - Size is ticked by default; biomes are a grouped checklist plus a box for modded ids; the last step's button is Export breed.
The jar
- The gene pictures go in as 64-pixel copies written at build time
(
shrinkGeneIcons), nearest-neighbour - a smooth resize made gradients PNG compresses worse than the originals, and the jar grew. The*Toolclasses (wiki bakes) and the creator's example genes are no longer packed. 3.96 MB, of which the pictures are half a megabyte. Nothing else fromwiki/was ever in the jar; the pictures are the one thing the game draws. Gap 187 closed.
Tests run: the breed classes (founder, plate, parser, breeds, stat curve), all green;
the designer driven in headless Chrome. The full :common:test was not run -
a full run would still need to confirm nothing outside the breed package rolled a founder
expecting stray magic. Gaps 68, 187 and 189 closed.
2026-09-11 — breeds you can drop in, a designer that asks one thing at a time
The owner's brief: the breed designer was overwhelming; break it into steps, keep the options on the left and the horse on the right; let a player make a breed and drop it into a folder with nothing but the jar. And the principle underneath it, now on the philosophy page: a concept is a breed, not a gene - a zombie horse is a zombie breed of ordinary genes (rotted markings, a temper, a low speed score), not one indivisible zombie gene. Everything is waiting in game.
The breed designer, rebuilt
- Sixteen steps, one decision each, in the owner's order (base colour, size, dilutions, white, eyes, magical markings, health, health issues, diet, temper, abilities, speed, jump, where it lives) with the two things the list left out placed after 13, as asked: price and stray magic. Left pane: Current step or Chosen so far, which lists every step with what it says and jumps back to any. Right pane: a founder, and a Reroll example founder button in its corner.
- The example founder is
BreedFounder.plate- a real founder with no stray magic - because “reroll within the traits I picked” is what was asked. A herd strip under it is real rolls, stray magic included. - Every gene is a card: tick the exact pairs the breed may have, with weights and the share each works out to, and under “fine-tune the numbers” a range or a lock for every epigenetic value - seeds included, which the format could not say before. Not on the four stat steps, as asked.
- Base colour is three sliders, not presets. Extension and agouti are drawn independently, so P(chestnut) is the e/e share and the black/bay split among the rest is agouti's - which reproduces the mix exactly. Measured: 38 bay, 15 black, 7 chestnut of 60 founders, against 60/25/15.
- The markings step lists every magical outcome - 424 of them - and
filters on what each actually does: sex, colourful / black only / white only,
coverage at most and at least, what it paints on, which parts it reaches (and
“nowhere else”), magic effects. Those are not in any gene file; they are
measured, by the new
:common:bakeMarkingFactscomposing every outcome on real base coats (MarkingFactsTool, a minute). A gene that shows nothing on a bay is measured on the first base it does show on, the way its icon is chosen, or flaxen would read as covering nothing. - Export at any step; an unnamed breed is named for the moment it was exported. Import, or open a built-in breed. The page also keeps its place in browser storage across a refresh - a convenience, not a save.
- Driven end to end in headless Chrome over the DevTools protocol -
steps, filters, random picks, hover card, export, import, a phone-width window. That
caught the one real bug the unit tests could not: every designer-made breed made
FounderTablelog an error per roll, because the format's weights are relative and the table wants percentages.Breed.founderTablescales them now.
The game side
- The drop-in folder is
.minecraft/phc/breeds/, made on first launch with a README, and the H menu's Breeds tab has Open breeds folder.config/horsegenetics/breeds/is no longer read. - A drop-in breed now actually spawns. Herds came from a fixed list of
37 vanilla biomes in a data file, so a breed living anywhere else loaded, registered
and was never seen wild. The herd modifier is a type of its own now
(
BreedHerdsBiomeModifier): its list, plus every biome any wild breed names, once each. spawn_time(day / night / any) anddescription(the Breeds tab's new “About”) joined the format.- Size is a multiple of an ordinary horse, and the genes follow from it: a founder between 0.7x and 1.3x carries one size copy, outside it two. Every shipped breed was converted from hands through the curve the game already used, so none changed size - one line per file.
- The gene hover card has its picture - the baked icon at the left, text round it - in the spawn egg screen and the horse designer (one screen in two places, same constants) and in the breed designer. The icons reach the jar at build time; that doubled it (gap 187, closed the next session).
Tests run: every breed class, both goldens, the gene files, showcase and white-rate -
all green. The full :common:test was not run. New gaps 187–189
(188 is still open); gap 83 closed.
2026-09-10 (late) — twenty-four genes out of intake, and what only the renders caught
The whole intake folder: three authoring batches of 7, 7 and 4 genes with a note each,
and six more with none. Commits a6d76bd (the batch exactly as it arrived,
so the unsimplified traces survive in history) and b3f9e9b (the install).
Every gene is waiting in game.
Checked before anything was opened
- Hashed — no two files alike.
- Scored for content against all 171 shipped genes (layer signatures plus the nouns in each blurb). Nothing was a re-arrival. Rime and maelstrom are the closest pair at 0.31 — a vortex on each flank, but feathered frost against tapering arms.
- One name collision. The new Contour shares only the word with the
shipped one (hypsometric bands against a patch of ringed cells) and became
Contour Cells. Its allele tokens were the
shipped Contour's own
Ct, so they becameCl.
What had to change on the way in
- Priorities. Thirteen collided with another gene file, and the batch-two and unannotated files sat on 120–159, where most slots are held by hand-written genes that the registry tie-breaks alphabetically. Since priority is also the sidebar family, they were placed by the coverage rule, next to what each resembles: fields 290–298, spots and rings 325–329, speckle 332–333, lines 385–391; holo flake kept 430. That audit also turned up seven coat genes from the afternoon intake filed as body-stat genes (gap 185). Left for the owner.
- Keys and case. Six arrived as
ixora.*. The whole batch wrote its rarity in lower case, andfounders.pylooked tiers up in upper case, so every gene would have been budgeted as UNCOMMON. The script is case-blind now. Running it also re-derived twelve shipped genes that had never been through it (the eleven from the afternoon intake, plus nymphaline), all still on hand-written tables at about ten times their tier. - The SVG point ceiling. Seven traced drawings flattened to between
1,316 and 7,785 points.
intake/tools/fit-svg.mjsis the hand simplification done for blackwork and dorsal wing, made into a tool that measures its own damage. Six drawings changed under 0.33% of their viewBox; candelabra was given 1,000 points and changed 0.41%.
What the renders showed and the tests did not
All tests were green once the files parsed, apart from twelve dead layers. Then a
side-view renderer drew every form of every gene on a bay, and half the batch was
wrong in ways no test measures. The renderer was a scratch diagnostic and is not
checked in: a hundred lines in the coat.pattern package, making the same
calls DeadLayerTest makes (a synthetic bay, SpecPainter.tint for
one expression), and plotting every Face.LEFT texel at its body-space
(x, y), with seeds and the expression taken from system properties. The icon bake shows
one horse per gene from three-quarters; this showed every form, on several horses, from
the side, and that difference is the whole of this section. It is worth making a
Gradle task.
- Eight genes nested their layers, and every pale form came out a blank white
shape. Corolla, agate eye, taper flame, barred wing, uraniid, trillium,
foxglove, ooze drip. Layers sum rather than stack
(gap 169), and near-whites nested three deep
clamp to one white. The black forms hid it, because dark plus dark still reads. Each
layer now multiplies in the inverted shape of every later layer. The prompt's rule 8
never actually said layers sum — only that one cannot read another — and
now it does. The
"over"flag on the roadmap is the real fix, and this batch is the cost of not having it. - Both drips ran sideways. On
WAVES,axisis where the wave travels andacrossis the band, the reverse ofAXIS. Nothing said so;SpecSchema, the creator and both making-a-gene tables do now (gap 184). - Contour Cells drew nothing, and then almost nothing. A
CRACKLEcentroid mask covers everything farther thangap/2body units from the centre. The schema called the field “0 middle, 1 rim”, so the gaps were written as fractions. Fixing the units and the polarity produced rings that seldom appeared, because the lattice is 3D and most centres lie off the skin (gap 182). It was rebuilt on bands of wall distance, which are the polygon shrunk and exist in every cell. Ringwork, shipped this afternoon, has the identical fault and is left alone (gap 183). - Crackle gaps swallowed their plates. Gilded crackle came out as a gold field with pale islands, and holo flake as single texels. The skin sits near a wall far more often than a flat tiling suggests. Both gaps are now at about an eighth of the scale, where the shipped crackles sit.
- Small centre marks were enlarged (agate core, corolla eye, taper
spark) for the same 3D reason. Agate eye's
chancewent from 0.5 to 0.85, because three horses in eight showed no eye at all.
Every change is written into the gene's own notes, with the reason.
DeadLayerTest finished with no new entries. The bake golden gained 84 rows
and moved none, and the pipeline golden changed genotype codes only, with no hash
moving. Parity and the NeoForge build were green.
Deliberately not done
- The two tool gaps (
RAMPangle,FANorigin: "cell") are real and are on the roadmap. Tidewave and opal fire ship with the authors' approximations. - Ringwork, the fourteen
Y/XWAVESgenes and the seven mis-filed coat genes are all shipped work and were not re-done. Each is a known gap with its fix written out. - Two tests are red on
mainand were red before this session —GameteBiasTest's splice check andCreatorMetadataRoundTripTest's verb count. Both were confirmed on a clean worktree at1cf48a8(gap 186). The full:common:testsuite was not run; the classes a new gene can move were.
2026-09-10 (evening) — three play sessions, fixed as they came
The owner played the build three times in one evening and reported as they went; the
session was almost entirely answering what they found. Commits 363b3a8 to
759f184.
Owner-verified in game
- Fireproof protects horse and rider, floats in lava, and immunity ends on dismount. Bird boned protects horse and rider.
- The spawner fires on feeding, uncapped (tested with a carrot on a plain horse).
- Potion milk, single and double copy, including the two-effect merge.
- The spawn egg's gene search box. Height in hands on the info screen and in My horses.
- Hand-feed taming. Every eye locus - third eye, glows, sectors.
- The research shelf stores papers; stall tickets land a horse properly (as a basis); the Breeds tab in creative; molten hooves' prints render.
What was built, and the reason behind each
- Immunities are damage cancels, not per-tick resets. Fireproof only
called
clearFire(), which stops burning but not lava's direct damage; vanilla's fire immunity is an entity-type flag no gene can set. A rider's fall damage turned out to be the horse's, handed down bypropagateFallToPassengers, so bird boned never covered riders either. - A ridden horse is simulated on the rider's client. This explained
two bugs at once: server-side velocity nudges never reach it (so lava buoyancy is a
synced
GRAVITYattribute modifier), and its server-side velocity is exactly zero (so everyon_moveeffect was silent under a rider; movement is now measured by position between ticks). tickCountis not saved. The spawner's once-a-day timer and dryad's 9 000-32 000-tick sapling needed that many unbroken ticks of one horse staying loaded. Every timed beat now counts on saved game time, offset per horse by UUID. The spawner then moved onto feeding at the owner's call, uncapped, counting only a meal actually eaten - an uncapped trigger on a free click would be infinite.- The spawner's colour was promised on its page and never read: a
per-copy epigenetic value now picks from the spawned mob's own colour data component
(
server/MobVariants). - Molten hooves became real prints on the ground - the mod's first
particle type (
horsegenetics:hoofprint, flat, emissive, coloured, laid by stride) - and then four alleles at the owner's design: a dominant white and three recessive colours, black unlit. - Stalls land a horse at the centroid of the floor, checked against its box - it had been centred on one tile, and a horse is 1.4 blocks wide - and report the real ceiling height. The holding pen (one per player, owner-confirmed after briefly considering several) sends any owned horse to a general pen.
- The research shelf became a chest of papers, then copied like a furnace on the block entity. Two bugs found on the way: its drops ran after the block entity was gone, and its labels were offset twice.
- Hand taming never fired
AnimalTameEvent, so it reached neither the breed book nor the tame tasks. The Breeds tab's plate is nowBreedFounder.plate, with no unnamed magic. - A search box on both gene editors, one rule in
EditorRules; a generated timeline of genes; the debug world's kit re-aimed at the checklist, a standing request.
Lessons
- Ask where a ridden entity is simulated before touching its movement. It cost three separate symptoms before the one cause was named.
- A PowerShell function called as
F 'a' + $x + 'b'gets five arguments, not one. A cut-between-markers helper called that way deleted about a thousand lines of the checklist; it was restored from git and the edit redone with the Edit tool. Wrap every concatenated argument in parentheses, or don't script multi-marker edits at all. - A compile ran once while the owner's client was open, because the window check and the build were one command. Gate the build on the check.
2026-09-10
The eyes stopped being a channel and became thirteen genes
The owner asked for eyes that are “extremely polygenic”, specified locus by locus: iris colour and sclera colour per eye, a heterochromia sector and a colour for it per eye, a glowing iris and a glowing sclera per eye, and a third eye on the forehead that can copy one of the others, mix them, or invent itself. Thirteen loci. Then the harder half: the natural genes that used to paint an iris should stop, and ask the eye loci for one instead.
What made it a design question rather than a port was where the asking
lands. The old channel applied a claim at bake time, so a splashed white
horse’s blue eyes were paint: real while it had the splash, gone the moment a foal
did not inherit it. The owner chose the other answer — the requested allele is
written onto the horse when it is made, so a splashed foal genuinely
carries MBl/MBl, shows blue eyes, and passes blue eyes on to foals that are
not splashed at all. Blue spreads through a herd the way any other allele does. That is a
bigger claim than it looks and it is the reason Eyes.force runs in
Genome and nowhere else: forcing twice, once with epigenetic midpoints and
once with the real numbers, would destroy the horse’s own eye alleles on the first
pass and leave the second unable to tell that it had.
The cost that had to be paid for it is on gap 174. The old “this horse is broadly white however it got there” rule measured the finished coat, which is the honest signal and is unavailable at breeding time. It is a declared per-locus sum now, and it will be wrong on horses whose white patterns overlap or avoid each other.
Three real bugs came out of the painting. Two were new and one was old:
blendTowardat strength zero still wrote the base colour, and these are absolute last-writer-wins writes — sotintIriswalking over a sclera texel with weight 0 silently undid whatever the sclera painter had just put there. A black sclera came out white.- Iris and sclera were told apart by the luma of the coat. That stopped being true the moment an iris could be invisible: a cleared iris shows the horse’s own coat, which on a pale horse is bright, so the sclera painter claimed it. Both are read off the template now, which never changes.
- Not redrawing the template over an invisible iris is not enough to make it invisible, because the composite multiplies and the template’s iris is black. A skipped texel came out black — the template’s own eye, which is exactly what it was supposed to remove. Invisible halves are recomposited against plain white before the redraw.
And one that had nothing to do with eyes. The weather loci minted allele
tokens as T+ and T-, and - is
Genotype’s own gene separator — so any horse carrying a
“suffers in” allele wrote a genotype code its own parser could not read back.
Nothing had caught it because it needs a founder to actually roll one, which is a few
percent of a few percent. Adding thirteen loci shifted the founder RNG stream and three
unrelated tests went red at once. The suffix is v now.
What was deliberately not done. The eye loci get no
wiki/gene-*.html pages: thirteen near-identical pages would be thirteen places
for one fact to go stale, and Eye colour is already declared
the single source of truth for eyes. The four magical genes that paint eyes directly
(dhampir, shadowcreature, light, magic sectoral heterochromia) were left exactly as they
were and are now audited on that page — two of them are redundant, and
gaps 175 and 176 record the calls the owner has to make.
The leopard complex’s white sclera rim was left alone too: it is not an iris colour,
and there is no allele meaning “more of the white shows”.
A fourth bug, found by reading a test while waiting for it.
CoatTextureComposerTest’s broadest case builds each genotype from a
bitmask over Genes.codeOrder() and writes (1L << n) - 1 for
“every gene on”. Java masks a shift distance to six bits, so past 64 genes that
silently becomes 1L << (n & 63) — at 291 genes, a shift of 35.
Three thousand random masks leave every locus from the 36th onward pinned to its last
allele, and nothing goes red because the test only asserts that a bake returns a full
sheet. It has been wrong since the registry passed 64 genes. Recorded as
gap 177 rather than fixed, because the same test is roughly
six and a half thousand whole coat bakes and is on its own a large share of the ten-minute
suite — widening it without sampling would make that worse.
Every horse in the world changed. Brown is the dominant iris allele, so every horse now has a painted brown iris where it had the template’s flat black. Both goldens moved wholesale, all 203 gene icons were rebaked, and none of it has been seen in game — §0-BU is the checklist, and its first four items are the ones that could be wrong rather than merely unseen.
2026-09-10
Twenty-seven behaviour genes, specified one at a time and then built
The session started as a design conversation about invisible ability genes and finished with the whole family in the mod. It went in that order deliberately, and the order is most of why it worked: every gene was specified, argued over and written up before any of it was built, so building it was transcription rather than invention.
The specification pass
A twenty-nine-gene proposal was walked one gene at a time, in batches of four or five,
with the owner ruling on each genuine fork. Two were cut.
Moon-sick turned out to be already shipped as the Aga/Aga allele
on night temper. Electric cannot be
built as described at all, because redstone power comes from blocks and never from
entities - the achievable version is pressure plates, which already respond to every
horse in vanilla.
Several specifications were wrong in ways only the code could reveal, and finding that out on paper was cheap. A Minecraft sound cannot be started partway through, so singer plays a record’s opening rather than “a section” of it. There is no concept of a “base” anywhere in the mod, so base alarm measures from the horse and stabling becomes the interface. And the mod’s own swimming assist means hydrophobic restores vanilla behaviour rather than adding to it - which flipped it from dominant to recessive, because dominant would have silently changed how every horse already bred behaves.
Each page carries its own hazards
The owner asked for the game-breaking, compatibility and performance risks to be
flagged on each gene’s coding tab, and that turned out to be the
most useful thing written all session. Five genes can damage a world -
spontaneous breeding is uncapped automatic
breeding that runs while the player is asleep; holy
ward hooks every spawn attempt in the world and can break a mob farm invisibly;
spawner can bypass other mods’ protections;
egg layer accumulates entities; and the three genes
granting a traversal flag to the rider write state onto a player.
Writing those down first meant the implementation answered them by shape rather than by a comment asking the next person to be careful. The ward does not scan for horses at all - the horse tick, which already runs per horse and already holds the ability list, writes its position into a small map and the spawn event walks that. Rider-targeted traversal is re-applied per tick and stores nothing, so there is no state to remove on dismount and it cannot become an invulnerability dupe. Followers are nudged rather than given an injected goal, so a dead horse cannot leave a goal behind on every creature it ever met. And ward entries expire rather than being removed on death, so a missed hook cannot leave a ward suppressing spawns for ever at a position with no horse at it.
What the batch actually cost
The previous estimate on the roadmap - “about one verb per behaviour” - was
roughly three times too high. Twenty-six genes needed seven new verbs, two triggers,
four condition flags and five widened verbs, because behaviours cluster:
four genes wanted sound, five wanted one mob-group filter, three wanted one
traversal parameter. Three base classes carry twenty-three of the twenty-six between
them, and they were worth more than any individual verb.
Parity earned its keep twice in one session, both times immediately: it caught the
creator’s missing condition flags, and then caught MOB_GROUPS being
declared below the table that reads it - a JavaScript hoisting bug that produced empty
choice lists and would have been invisible in the editor.
Both goldens moved, and neither moved wrongly
None of the twenty-seven paints, and that is measured rather than asserted: all 1056 rows of the pipeline golden kept their rendered-byte hashes and only their genotype keys changed, and the bake golden gained 206 rows with not one existing row moving. That check had to be done keyed by gene and allele - a positional diff claimed 312 rows had moved, which was entirely the artefact of inserting 206 rows into a sorted file.
Three things recorded rather than fudged
- Gap 170 - lycan
still keeps its own mob list rather than reading
MobRoster, because its allele order is baked into the genotype code and two goldens. - Gap 171 - a guardian never retaliates against a player, because 26.1.2 exposes no server-side PvP flag to gate it on. A decision forced by an API gap rather than chosen.
- Gap 172 - four pages were invisible because their filenames did not match their gene keys, with the build entirely green. The bake does say so, on a line nobody reads.
A review pass, which found three bugs in the same day's code
Re-reading the finished work before closing turned up a dimension leak in the ward - a horse warding in the Nether would have suppressed Overworld spawns at the same x/z - and two per-entity costs that had been hidden behind comments asserting they were cheap. All three are fixed, and gap 173 records the part worth keeping: every one of them was in code whose hazards had been correctly identified and written down in advance on the gene's own page. Knowing the hazard did not prevent it. Only reading the finished code back against the hazard did, and no check in this repo would have caught any of the three.
The same pass found that hydrophobic only does half of what its page claims - it ejects the rider and does not then head for shore - which is now said on the page rather than discovered in a lake.
Nothing in this batch has been seen in game. §0-BT is the checklist, ordered by what can do damage first.
2026-09-10
Eleven more genes out of intake, none of them duplicates, and the dead-layer check paying for itself on its first batch.
Agatebound, bargello, bracketed, coronal, geode, maelstrom, nacre, starburst, sunspiral, wing margin and witchfire. The batch was checked for duplicates by content rather than by name first: every gene's layer signature — which masks, folded how, painted with what — and the nouns in its blurb, scored against all 159 that already ship. The best match in the whole batch scored 0.13, so nothing was a re-arrival and nothing was skipped.
Two collisions, both resolved rather than skipped
Nacre collided by name with a gene that already ships, and the content check is what settled what to do about it: they score 0.04 against each other. The installed one breaks the coat into hard-edged plates at gold seams; the new one is a marbled saddle with a shifting film along its folds. They have nothing in common but the word, so the new one is Nacre Saddle.
All eleven arrived keyed ixora.* where every other gene in
the mod is horsegenetics.*. The format allows a modder their own
namespace and should — but a gene shipping inside this mod under a
second one breaks every tool that builds a key from a slug, so they were
renamed. Recorded in each file's notes rather than done silently.
The check caught nine dead layers before anything shipped
DeadLayerTest was written earlier the same day, and this is the first batch it saw. It failed on nine layers across three genes — every one of which would have shipped, because all eleven files parse, all eleven paint plenty of texels, and ten of the eleven look fine in a render. The nine had three different causes, which is the argument for measuring the symptom rather than pattern-matching the cause:
- Wing margin's heavy veins painted
nothing at all.
FAN'sspacingis an angle, and at 1.4 radians the veins are eighty degrees apart — the barrel does not subtend enough of the fan for a single one to land on it. 0.51 puts a heavy vein at every third fine one. - Starburst's white core was
overruled — a genuine gap-169 instance. Outline, inline and core
are three nested filled
PATHs at strength 100, so the core was adding white to a texel the two outside it had already driven to the clamp. Each star now subtracts the one inside it. Its flecks were a separate problem: spacing 4.6 at radius 0.6 and chance 0.3 is about three spots a texel and a half across, and they landed on four texels in total. - Geode was mis-scaled, not mis-built.
Every layer was bounded by an
AXISreaching 11.5 units and aWAVESband reaching 7, so the shards lived in the bottom quarter of the horse and the whole gene moved about three hundred texels out of eleven thousand. Visible in the file, invisible on the horse — and the one gene in the batch whose icon had already made me suspicious.
Diagnosis needed one measurement the check does not make: render each dead layer alone. A layer that paints nothing alone has an empty mask fold; a layer that paints plenty alone and nothing in place has been overruled by its neighbours. Wing margin's veins scored 0 alone, starburst's core 132. That two-way split is worth building into the check's failure message the next time it fires.
And dorsal wing stops clear of the underline
Opening the NORMAL mask to -0.75 at round 0.85 is what
carries the butterfly down the flank, but a rounded normal that far open also
admits the belly — a downward-facing face reads well above the
threshold once it is blended — so the wing ran to the bottom of the barrel.
An explicit AXIS on Y now cuts it at y 13.5, about two and a half
units above the underline. A height is the right tool for a height: raising the
NORMAL instead would have pulled the marking back off the shoulder
as well.
What was regenerated
Bundle, icons, 168 gene pages, the designer wasm, both goldens. The bake golden gained 40 rows and moved exactly two — dorsal wing's, from the belly cut. Every pipeline-golden code grew by eleven loci and no horse moved.
2026-09-10
A check for gap 169, and the two metrics that were measured and thrown away first.
Three genes in two days had been brought down by the same thing — a gene's layers each measure their delta against the colour the gene started from and the deltas are added, where every author expects them to stack. This session built the check. Most of the work was finding out which measurement actually separates the broken genes from the good ones, and the two that don't are worth writing down, because both look obviously right until you run them.
What was thrown away, and why
Divergence from stacking. Render the expression twice — once the way the engine does, once applying each layer before the next measures — and diff. It is the most direct possible statement of the bug, and for disjoint layers the two are identical by construction, so it looked like it would have no false positives at all. It fires on about half the corpus. Ringwork diverges on 92% of its painted texels, crazework on 96%, panda on 55%, and all three look good. A gene with overlapping layers whose author tuned the colours until the sum looked right is indistinguishable from one whose author did not.
Clip pressure. How far past the byte the summed delta would have gone. This one ranks pre-fix contour first and second of every expression in the mod, at 89% and 88% of painted texels — exactly right. It also ranks dripwork third and crazework fifth, and those are fine, because clipping toward a colour every contributing layer wanted is harmless: dripwork's layers all want black, so a sum that clamps to black is the intended answer.
No metric separates “relies on summing and looks good” from “relies on summing and looks wrong”, because the only difference between those two is whether a human liked the result. Any check built on the summing itself is either a gate that fails half the mod or a report nobody reads. So the check had to be built on something objective, and there is exactly one objective thing in the neighbourhood: whether a layer is there at all.
What was built
DeadLayerTest: take a layer out; if the horse does not change on
any base coat, the layer was not doing anything. No taste in it, and no
pattern-matching either — whether two masks overlap is not decidable by
reading them, since they are arbitrary functions of position folded with inversion
and subtraction, but it is decidable by evaluating them, which is what the
painter already does. 23 seconds, which keeps it inside the working loop; the
speed comes from an early exit, since a layer proven alive on the bay never gets
re-tested on the other coats.
It catches three unrelated mistakes with one measurement, and none of them fails anything else: an empty mask fold, a layer clamped away by an overlapping one, and a feature smaller than a texel. It found 29 dead layers in the shipped genes. Validated against the known bug by reconstructing the pre-fix dorsal wing: it flags exactly the two layers that were invisible, and the fixed gene is clean.
What it found
- Quarter: nine dead layers — three of the four quadrants, in three separate expressions.
- Ringwork: both contour rings, which are annuli 0.1 body units wide against a texel of 0.5. That is rule 15 of the prompt — size it against a texel — and nothing enforced it until now.
- Moth mantle's cheek mark, moth wings' pale pupil, dorsal shield's centre stud, the innermost neck band of concentric eyespots, hood's neck thinning, ocular's pupil, nimbus's curdle.
- And one of this session's own: dripwork's loose dots, thinned to spacing 7.5 at chance 0.18 an hour earlier and landing on nothing at that pitch. Fixed to 5.5 at 0.32 and its line deleted — at which point the ratchet caught that the coloured form's dots are still dead, swamped by the ink around them, which is a live gap-169 instance rather than a pitch problem.
The blind spot is declared rather than papered over: 47 layers carry a
PIGMENT or LUMA mask, read the coat underneath, and cannot
be judged against a synthetic base — moth eyes' six layers read as dead on
every base the check builds and are simply unhoused. They are skipped and counted,
and a second test asserts the count has not quietly grown.
The root cause is still open
The reason authors keep writing this is that the format cannot say “paint
this on top of that”, so they write the thing that looks like it and get
summing. A per-layer "over": true that applies the accumulated delta
before the layer measures would make dorsal wing's original file correct as
written. That is a format change — the five-file contract plus every golden
— so it is written up on gap 169 as a
decision rather than taken unasked.
2026-09-10
Four genes retuned off looking at them, and the coverage rule of thumb written down.
Four notes from the owner on yesterday's intake, and the interesting thing is that two of the four turned out to be the same defect wearing a different complaint.
“Contour doesn't have enough colour” was not a palette problem
Contour draws a topographic map: six bands of a
landscape, each a threshold on one FRACTAL field. Band 0.38 therefore
sits entirely inside band 0.30 — the bands are nested. And a
magical gene's layers each measure their delta against the colour the gene started
from and then sum, so six nested TOWARDs do not paint six
bands. They paint one colour, which is whatever six deltas add up to, and six of
them add up to white.
Which is what shipped: an almost-white horse with a brown patch. The pastel palette
it was given — #e2d5b8, #e8c8e5,
#c8eeec — was the author compensating for a wash whose cause they
could not see, and adding saturation to those colours would have made the wash
worse rather than better. Every band now subtracts the threshold above it and paints
only the ring between the two. Once they are disjoint the colours can be what a
contour map actually looks like, so they are: a hypsometric ramp from deep green
through gold and orange to red-brown.
The coloured form gets a second hue knob rather than more saturation on one.
inkHue runs the low ground and highHue the high ground,
and they are independent draws — a knob cannot be offset by a
constant in this format (gap 164), so a horse
can roll two hues that sit next to each other and come out nearly monochrome. That
is the honest cost of the missing arithmetic, and it is a good deal less bad than
one flat hue.
Dorsal wing painted a solid black horse, contour painted a white one, and both were layers summing where the author expected them to stack. It is gap 169, and the thing that makes it hard to see is that the symptom does not look like the cause: dorsal wing looked like a mask that was not firing, and contour looked like a palette that was too pale. Neither looks like an arithmetic problem, so neither sends you to the rule that explains it.
Dorsal wing wraps down the sides now
The butterfly is drawn half again as wide as the horse and centred on the spine
(sizeV 1.5, originV -0.25), so the outer part of each wing
falls off the edge of the back and lands on the flank, and the NORMAL
mask was opened from 0.15 down to -0.75 at round 0.85 to let it get
there. The two numbers have to move together: widening the drawing without opening
the NORMAL just clips more of the wing away, and opening the
NORMAL without widening the drawing puts bare flank into vertical
bands.
Worth being precise about what is on the sides, because it is not a drawing. A
top-plane shape is read at (x, z) and a flank face has one constant
z, so the whole side shows a single vertical slice of the
butterfly stretched downward. On the back you are looking at the drawing; on the
flank you are looking at its cross-section running down. It reads as wings folding
over the barrel, which is what it is for.
Dripwork: no ground, fewer marks, and runs down the legs
The white ground went first — an ALL layer at 86% opacity, which
turned every base coat into the same pale grey before the gene drew anything. That
is a lot of horse to spend on a backdrop for a scatter of small marks, and the same
argument took the white out of rising sun. The
marks are on the BODY and the LEGS now rather than
everywhere, and there are fewer of them. The runs are new: STROKES on
Y, weighted to the lower half by an AXIS mask, which is what makes them
read as ink running toward the hooves rather than as a stripe pattern.
The AXIS went in inverted the first time, which selected
everything above the barrel — on a gene restricted to the body and
the legs, that is nothing at all, and the layer rendered as though it were not there.
Caught by looking at it, which is the only thing that catches it.
Rising sun goes to the top of the order
Priority 123 to 620, which is the top third and above every other marking except invert and tron. It covers the whole horse, so by the rule of thumb below it should be near the bottom. It is at the top on purpose: a rising sun is a poster printed on the horse rather than a marking the horse has, and a poster that other markings draw over is not a poster.
The rule of thumb it breaks
Coverage and priority is new and is deliberately not a rule: the more of a horse a gene covers, the lower its priority should be. Later paints over earlier, so a whole-coat gene painting late erases everything under it and a small mark painting early gets erased by anything bigger. Sorted by coverage, a horse carrying five markings shows five markings.
It ships with its own counter-examples attached, which is the point of writing it
as a rule of thumb: rising sun breaks it for the reason above, and
fielded breaks it because a gene that reads
the coat with LUMA or PIGMENT has to run after whatever it
is reading however little it covers. The five priority bands the mod already uses
are written out beside it, because slotting a new gene next to whatever it most
resembles beats reasoning it out from scratch. The prompt carries a four-line
version.
What was regenerated
Icons, the bundle, 157 gene pages, the designer wasm, both goldens. The bake golden moved exactly eight rows — the two alleles of each of the four genes — and nothing was added or lost. Every genotype code in the pipeline golden changed and no horse did, which is rising sun's priority moving it up the code order and nothing else.
The intake folder was empty this session; nothing new to import.
2026-09-10
Fourteen genes out of intake, two drawings that were traced photographs, one that painted itself black, and fourteen founder tables that were four times over budget.
The whole intake folder went in: blackwork, coastline, contour, crazework, dorsal wing, dripwork, dustfall, overcast, ringwork, rising sun, serpentine, shallows, squiggle and teardrop. Priorities 108–124, no clashes, and the batch was hashed before anything was opened (two files once arrived holding a third's content). Twelve of the fourteen needed nothing but installing. The other two, and one thing wrong with all fourteen, are the session.
Two drawings were over the SVG point ceiling
Blackwork's ornament flattens to 4368 points and dorsal
wing's butterfly to 1637, against a ceiling of
SvgPath.MAX_POINTS = 1024. The ceiling is a cost ceiling
— every mapped texel of every skin walks every segment — and it is
the guard that exists to stop a traced photograph going into a gene file.
These are traced photographs.
What makes them expensive is not the drawing, it is the flattening rule: the engine gives every cubic a fixed sixteen sub-segments, which is right for moonwing's seven big curves and wildly wrong for an outline of two hundred and seventy tiny ones. The drawings do not need them. The viewBox is 1000 wide and a horse is about 150 pixels, so a point every 1.5 units is already finer than a texel by an order of magnitude. Both were flattened the way the engine flattens, run through Ramer–Douglas–Peucker at whatever tolerance hit a 700-point budget, and re-emitted as polylines — one point per vertex where a cubic costs sixteen, and the silhouette identical to well under a texel.
The engine-side alternative is adaptive flattening — subdivide until the chord error is under a tolerance instead of always sixteen — which would let a traced outline in at its natural cost and would also move every existing SVG gene's golden, the spec fixtures and the creator's JS twin. It is gap 168 rather than something to do on the way past.
Dorsal wing painted itself solid black, and the reason is worth knowing
The gene draws a butterfly along the spine: a near-black ground over the whole silhouette, a teal margin band, rust squiggles, hot pink cells inside those. On a horse it was a black horse. 484 texels moved on a bay and not one of them was teal, rust or pink.
Every layer of a magical gene measures its delta against the colour the
gene started from — not against what the layer above it left
— and the deltas are added together. So a TINT of
-200 on every channel under a TOWARD teal does not
give teal. It gives teal minus 200, which is black. Confirmed by deleting
the ground layer from the built resources and re-measuring: teal, rust and
pink all appear at once.
The prompt has said this since the import — rule 8, “a layer cannot see what the layer above it painted” — and it is still the easiest thing in this format to get wrong, because every gene that does work does so by having disjoint masks and so never shows the author what the rule costs.
The fix is the decomposition the gene's own notes already describe, written as subtraction instead of as stacking: the ground paints the inner shape minus the rust patches, and the rust paints its patches minus the pink cells. Nothing about the drawing, the colours or the nesting moved.
All fourteen founder tables were between three and six times over
The prompt tells an author to declare a rarity tier and let the budget derive the table, precisely because the rates compound across a hundred and sixty genes. All fourteen declared a tier and wrote a table anyway, and every table was 2.9× to 6.3× the tier it declared. Together they claimed 2.87% of founders against a budget of 0.62% — which is not a rounding error, it is another two percentage points of wild horses carrying something magical, on top of what the existing hundred and forty-odd already spend. All fourteen were re-derived from their declared tiers.
The three-form rule, applied for the first time as a rule
These arrived before the rule was
written down, and every one of them is a white-or-pale form plus a coloured
one. Exactly one qualified: overcast,
whose pale form is a four-step grey ramp from #dee2e6 down to
#575e69 against the same four steps in one hue — the dark
half of the scale simply unused. It has a Ck black sky now, the
same four steps between #0d0d0d and #707070.
The other thirteen were judged out, and the judgement is the rule's own
“when not to” rather than taste. Dripwork's
ink is already black — its #fbfbfb is the ground the
graphic sits on, not the marking. Ringwork is
achromatic already and runs from #d1d5d6 to #17181b,
so there is no unused end to move into, which is the whole premise. The rest
— coastline, contour, crazework, dorsal wing, dustfall, serpentine,
shallows, teardrop and the others — name three to seven fixed colours,
and the palette is the marking.
And four expressions had picked up a pair they should not have
Yesterday's black-allele sweep handed out unclaimed combinations by the rule
“a pair belongs to whatever its more dominant allele's homozygote
shows”, which is right for the pairs the new allele created and
over-reaches on pairs that were deliberately left to the wild-type
catch-all. It caught four: Fdf/n on fracture,
Tmr/n on marble tobiano, Ylc/n on yalia and
Cc/n on overcast, each of which had been wild-type by the author's
choice and had quietly started expressing. All four are back, and their founder
tables with them — fracture and marble tobiano are recessive genes again,
so the whole budget sits on the homozygote where it belongs.
Versioning
Recorded on releases rather than left in a commit message: the next release is 0.5.000, and after that only the last number moves. Three digits, on purpose. The middle number does not move without the owner saying so explicitly — not for a big feature, not for a batch of genes. 0.2.0 to 0.4.1 in two days is what the rule is a reaction to.
What was regenerated
Gene bundle, icons, 157 gene pages, the designer wasm, both goldens (43 rows
added to the bake golden, none moved, none removed; the pipeline golden's codes
all grew by fourteen loci with no horse moving), the search index. Links, gene
tabs, parity and recipes green; :neoforge-26.1.2:build green.
2026-09-10
A black allele for forty-two markings, the rule that produced them written down on the gene page, and Cleave split into two loci.
Every magical marking here that had a white form and a coloured form now has a black one as well — 42 forms across 41 genes. That is not 42 judgement calls; it is one rule applied 42 times, and the rule is the part worth keeping.
White and coloured are two points on one axis with the far end unused
The convention these genes were written to is a white form on
allele[0] and a recessive coloured one that draws the same layers
with "hue": "$hue" where the white one names
#ffffff. Every one of them therefore already knew how to draw
itself in a single flat tone, and the whole dark end of the scale was
unoccupied. The black form costs one allele and no new vocabulary, and it
lands on a set of horses the white form has never read well on — the
palominos, cremellos, greys and dominant whites, where a white marking on a
white coat is a marking nobody can see.
Re-tone, do not repaint — and measure achromatic as chroma
The black expression is the white one with every achromatic target moved into the dark end of the scale, keeping the order and the spacing of the tones it had, and every saturated target left alone: that colour is the gene rather than its lightness. Moonwing is the clearest result — a graphite wing carrying the same iridescent dust and the same gold binding, because only the wing itself was ever white.
Achromatic is max(r,g,b) - min(r,g,b) under about
0.12, not HLS saturation. Saturation reads
backwards at exactly the values that matter, because its denominator
collapses near white: #f4f0e8, a pearl six counts off the top
of the scale, scores 0.35 and gets protected as though it
were a colour. The first pass of this change used saturation and shipped a
black moonwing byte-for-byte identical to the pearl one, along with six
other genes whose black form was a copy of their white form. It was caught
by the transformer printing how many grey steps each gene came out with and
seven of them saying zero — which is the argument for making a bulk
edit report what it did rather than how many files it touched.
The half that is easy to skip: both achromatic forms get the same depth
A coloured form that separates its layers into a dark ring, a mid field and a bright core, against a white form that paints all three off-white, is not two versions of one marking — it is the good one and the flat one. Where the coloured form spanned a noticeably wider range of lightness, the white form was regraded onto the coloured form's own per-layer ordering, in greys. Three genes qualified. Pavonem is the one that shows why: its coloured form makes the peacock eye's pupil the darkest thing on the horse, and its white form had been painting that pupil the same white as the spots around it, so the eye had no pupil at all.
Dominance: nothing that existed moved
Black sits directly under white and above coloured, in declaration order. Written that way, adding an allele to an existing gene changes nothing that already existed — every pair that had an outcome keeps it, and only the pairs carrying the new token are new. The new pairs were handed out by one rule, an unclaimed pair belongs to whatever its more dominant allele's own homozygote shows, which reproduces the series each file already had and is the only rule that got Cleave's two independent pairs right without anybody typing out 28 combinations.
The proof is the bake golden: 42 rows added, 3 rows moved, none removed — and the three that moved are the three regraded white forms. The pipeline golden did not move at all.
The rule is now on the page, not just in the files
Three forms, where the palette is narrow is the whole of it — the dominance table, the token convention, the re-toning rule with the chroma warning, the depth rule, and when not to. It is a recommendation and it says so: where the colour is the gene, coral bloom's sea-blue and gold or anything whose whole idea is that it is ember, a grey version is a different marking wearing its name. The prompt's CONVENTION block says the same thing to a model, with the slot order and the tone targets spelled out.
And Cleave became two genes
Cleave carried four alleles that crossed two questions together:
Clv and Clc for a front-half split, Wav
and Wac for a back-half one. So "the same marking in the other
colour" and "the same marking on the other half" were the same kind of thing,
and adding the black form to it had to add two alleles. They are
separate questions and they are two loci now:
horsegenetics.cleave says whether there is a
cleave and what colour, and cleave_side
says which half, wild type being the front.
Clvw/Clvw n/n is a white front half;
Clvw/Clvw Wve/Wve is the same marking on the back.
It is modelled on accretion +
accretion field, which is the same
shape already in the mod: the modifier locus has no layers, every horse carries
it, and the painting gene asks for it with needs. Two things about
it are load-bearing and are written into the file's notes. The conditional
expressions must be listed before the unconditional ones, because
SpecGene walks the conditionals in file order and only falls
through to the plain combination table when none is satisfied. And
Wve is dominant — needs asks for one copy —
so a foal out of one back-half parent takes the back half.
The old back-half drawing survived the split byte-for-byte: of the six golden
rows that moved, all six are the new heterozygous Wve/n
horse, and the Wve/Wve one hashes exactly as the old
Wav/Wav did.
What was regenerated
Gene bundle, icons, all 143 gene pages, the designer wasm, both goldens, the
search index. Links, gene tabs, parity and recipes green;
:neoforge-26.1.2:build green.
2026-09-10
Four genes retuned off looking at them, one new gene out of intake, and the gene files’ notes moved to the tab they were always about.
A short session, and every change in it came from the same place: somebody opened the icon sheet and said what was wrong. That is worth saying because three of the four fixes are things no test could have raised — the goldens were green, the parity was green, and every gene was painting exactly what its file said.
Coral bloom’s rim was not a rim
Coral bloom was written as three concentric zones of
one tessellation: a bright orange rim in the seam, a deep blue field inside it, a pale
crown at the middle. On the horse it was an orange horse with blue blobs.
The arithmetic is the whole explanation and it is worth writing down, because the same
trap is waiting in every other CRACKLE gene: a wall of width w
through cells of size s takes roughly 2w/s of the surface, so at
gap: 1.2 in cells of 5.5 to 9.5 the seam is not a hairline round a cell, it
is most of the horse, and the cells are the things floating in it. A rim colour is
load-bearing in a way a fill colour is not — a fill can be wrong and the picture
survives it; a rim being wrong inverts figure and ground.
The fix is to paint nothing in the seam. The base coat shows there instead, which is a colour the horse already owns and which therefore cannot take the picture over however much of the surface it covers. Blue band and pale crown are untouched.
Moonwing, four times the size, and over the back
Moonwing’s viewport went from 0.30×0.20 of the
body to 0.62×0.44 and, more to the point, is now deliberately taller than
the barrel. That last part is the whole of how it wraps: a side-plane
SVG ignores z entirely, so the drawing is a function of x
and y and nothing else. Raise its top edge past the barrel’s and the same
outline that covers both flanks covers the back between them — no seam, no second
placement, and the top of the wing simply clipped off by the horse. The extrusion that was
already doing symmetry for free turns out to do wrapping for free as well.
And a gene that reads the coat underneath it
The other half of the moonwing ask was that the pearl allele give other white
markings the same treatment — the gold edge and the coloured dust. That is
LUMA’s white channel, which is why it exists: not
“the pale parts” but the achromatic floor, so a splash reads 1 and a palomino
reads near 0.
The rim is the move worth remembering. There is no erode operator and there does not need
to be one: the white selection, intersected with the same selection inverted and grown
by 1.3 units, is a band exactly that far inside every white border. Growing the
NOT-white eats into the white, and the intersection is what was eaten. Both layers run
first, before the wing — LUMA reads the coat as it stands when
the layer runs, so a wing laid down first would be read back as one of the markings it is
supposed to be matching, and edged twice.
Measuring that turned up gap 166, which is not this session’s doing: on a horse that is white everywhere, the pearl form’s gold band comes out grey. Four warm texels against 589 on a bay, and 692 for the coloured allele on the same white horse. It reproduces with the new layers stripped back out, so it predates them; the cause is unestablished and is written down rather than chased.
Three greys instead of one ivory
Iridescent rosette’s pale allele was a
single flat ivory annulus and it read as a smudge: a ring with no interior structure
has nothing to tell the eye where its edges are. It is now the same three concentric
bands the coloured form uses — same radii, same knobs, same registration — with
three greys where that one has three hues. The side effect is that the gene’s wiki
icon flipped to the coloured allele, because CoatVisibility photographs
whichever allele moves the most texels and the darkest grey sits close enough to a bay to
lose the count. Noted on verification so it is
not reported later as a bug.
Nymphaline
New, out of the intake folder, and the first arrival since the SVG engine landed —
which makes its most interesting property that it does not use the SVG mask.
Its source was eight posterised colour layers traced off a photograph of a butterfly wing,
about 300 KB of path data: past MAX_PATH_POINTS by three orders of
magnitude, and the wrong shape of thing regardless, because a traced photograph is
a patch of membrane and not an outline, so there is nothing in it to place. It is rebuilt as
rules. FAN is the radiating venation, on a pivot that sits on the flank
rather than above the back — from above, the barrel subtends about 74 degrees and any
spacing wide enough to give a vein a texel of width leaves three veins on the whole side;
from the point of the shoulder it subtends nearer 100 and the same spacing gives six.
CRACKLE with vertexWeight up is the fine net where the venation
breaks down, because a second fan fine enough to read as secondary venation comes out under
half a texel wide at every radius on the barrel. Priority 118, with the other whole-coat
genes.
The notes were on the wrong tab
Every generated gene page put the gene file’s notes under “About
this gene” on the gameplay tab. They are implementation reasoning to the last
line — why FAN and not a second SVG, which mask the
registration hangs off, what the import asked for and was refused — and the gameplay
tab is defined as the one written for a player who does not want the model. They are on the
coding tab now, under “Why it is built this way”, which is the same call
outcomeSummary already made about the outcome prose, for the same reason. 56
pages moved.
What was regenerated
Both goldens: eleven rows in coat-bake-golden.txt, and every line of
coat-golden.txt gained a nymphaline segment. Worth being explicit that
no rendered horse in the pipeline golden moved — all 1056 hashes are
unchanged and only the genotype codes grew, which is what adding a locus does, and is also
the check that the four edited genes are on none of those 39 horses. Then the gene bundle,
the icons, the pages, the designer wasm, the search index and links.
The intake folder’s twenty-two markdown sources are deleted — each was checked
against the gene it became before anything was removed, and all twenty-two ship.
intake/overcast.json arrived during the session and is untouched.
2026-09-09
The engine learns to read SVG, and twenty-two genes land with the ten masks they asked for.
The intake folder held twenty-two gene files and every one of them was written against
a vocabulary that does not exist — PAINT ops, CRACKLE
with a stretch, DAPPLES with a radius, SPOTS with a count and
a centre. That is not a complaint about the files. What each one did carry,
and what made the batch worth a day, was a TOOL GAP section saying which
measurement its reference needed and why no arrangement of the masks that exist
reached it. Ten of those were real, and the session ran in that order: build the tools
first, then write the genes that use them.
The SVG mask, which is the one that was not in any tool gap
PATH was the admission that some shapes are not made of a rule —
that a marking somebody drew has to arrive as points. This is the admission
that follows from it: markings that get drawn get drawn in a drawing program, and what
comes out is a d string of cubics, arcs and subpaths inside a
viewBox. Re-typing one as sixty-four normalised control points is the step
where the drawing stops being the drawing — the holes close up, the arcs become
chords, and an evening goes on arithmetic.
So SVG is deliberately the whole of
the grammar rather than the convenient part of it. Every path command, including the
reflected control point an S and a T carry and the
endpoint-parameterised elliptical arc that every exported circle is made of. Subpaths,
so a letter O has a hole — and a fill rule, because whether that hole is
a hole is written in the artist’s file and cannot be recovered from the geometry.
The transform list. viewBox and preserveAspectRatio. A stroke
with real caps, joins, a miter limit and a dash pattern measured along the path.
SvgPath flattens it once, at load, so per texel it costs what
PATH costs however long the string is.
node intake/tools/svg-to-mask.mjs reads an actual .svg and
writes the mask blocks: nested <g transform>s flattened into each
element, rect/circle/ellipse/line/polyline/polygon
converted to path data the way the specification defines them, the presentation
attributes carried across, and a warning printed for everything it cannot see — a
CSS class, a <use>, an element both filled and stroked. Moonwing is
the first gene in the mod that carries a drawing.
Nine more measurements, each because a reference needed one
FAN— an angular phase around a pivot, so bars widen as they radiate.WAVEStakes its phase from a straight line, so its bars are parallel, evenly spaced and all pointed the same way everywhere in the region, whatever the wavelength. Morpho Wing and Gill Bloom both asked for it independently.NORMAL— which way the surface faces, dotted with a body axis, with aroundknob that blends toward the normal an ellipsoid of the same box would have. Carapace Sheen and Kite Bloom both asked for view-angle iridescence; see below for why that half is refused.CRACKLE.vertexWeight— blends in the distance to the three-way corners, so a thresholded network pools where cracks meet and thins between them. An even channel is what a distance to a plane is; a vein is not that. Emberveins.CRACKLE.measure: "centroid"— the same tessellation read from each cell’s middle out. Three separate files asked for a version of this, and it turned out to be one parameter: it is the only per-cell radial coordinate a crackle outline is guaranteed concentric with. Tidefoam, Opaline and Iris Bloom.SPOTS.arc+angle— clips every mark in a tiled field to the same wedge, which is the difference between a shingled roof and a closed net. Fish Scales.SPOTS/RINGS.offsetX/Y/Z— move where one mask reads the cell centre without moving the cell, so an off-centre glint sits inside a concentric set instead of replacing it. Moth Eyes.RAMP.axisgainsnoise,cellandcellId— three colour sources that are not straight lines: a field that wanders, a per-cell radial fade, and one key per cell so neighbours land on unrelated stops. Oil Sheen, Opaline and Opal Crackle.- Moth Eyes also asked for a LUMA channel measuring how far a colour sits from grey. It
was already there, called
saturation; inverted, it is exactly “black or grey and nothing else”.
Two requests refused rather than deferred
Nothing here will ever vary colour by the angle to the camera. The coat
is a texture baked once per horse and no mask or op will ever see a viewer, so true
iridescence is out of reach and will stay out of reach while the coat is a PNG.
NORMAL is the structural half of it — the sheen sits on the planes
that turn into the light and stays there — and that is the honest approximation.
And a knob cannot be offset by a constant. Agate’s import wanted
each cell’s heart to be its rim’s complement, 180 degrees round the wheel;
a hue knob is a number the horse drew and there is no way in the format to say
“that number plus 180”. Both are now stated in the prompt so the next batch
does not spend a section proposing them.
Four things the imports got wrong that would have shipped silently
This is the part worth reading, because none of the four would have gone red.
- Opaline gave each cell one of three tints with a
CHOICEmask.CHOICEis a coin flip made once per horse and returns the same answer on every texel, so all three layers would have painted the entire marking one colour — and which one depended on the seed. That is now thecellIdramp, and rule 11 in the prompt says out loud whatCHOICEcannot do. - Concentric Eyespots had leg bands 0.28 and 0.26 body units wide. A texel is 0.5. Every ring on every leg was under a texel and averaged away to nothing. The import’s own note said a fourth band would do that; it was already happening at three. There is a new rule 15 about sizing against a texel before shipping.
- Iris Bloom Diamonds bet that
CRACKLEandSPOTSagree about where a cell’s centre is when given the same seed and spacing, and wrote a note saying it was a bet. They do not agree. That is whatmeasure: "centroid"is for. FAN’sspacingis an angle, and the first draft of the schema defaulted it to about three bars per full turn. Three genes shipped with numbers that read like body-unit spacings and drew a single bar across the whole marking. Caught by looking at the contact sheet, which is the only way it could have been caught.
Two smaller things
wing_cloak.json is missing from genes/index.json on
purpose, and this session regenerated the index from the folder and put it back.
The absence is the killswitch —
gap 129 parked the gene until it is retooled, and
the whole switch is that one line not being there. Rebuilding the list by walking the
directory is the obvious move, is what the
contract’s warning about a
hand-kept list invites, and re-enabled it silently: nothing went red, because the gene
loads perfectly. It was caught while writing the gaps page and reverted, which moved
the bake golden by exactly its three lines and no other hash. The real gap is that the
file carries two kinds of information — “these exist”, which a script
can derive, and “this one is off”, which it cannot — with nothing
telling them apart. Gap 162. Until that is fixed:
add to the index by hand, never regenerate it.
Flags can default true, which the SVG mask needs twice: a
<path> with no stroke is filled, and a pasted drawing has to be
flipped. That is a real change to the creator rather than a cast, because its exporter
drops any setting equal to its default — a true default has to be written when it
is unticked. The parity check compares flag fallbacks now; it never did when
they were all false.
founders.py was re-run over the whole set, as it is designed to be: it
budgets the tiers together, so twenty-two new genes re-derived everybody’s
numbers. Forty-three existing tables moved, not the twenty-two the commit
message guessed at — because the re-derivation also flattened every table that
had drifted above its declared tier, and several had, by a factor of seven.
Across the mod the share of founders expressing some magical gene went from 16.32% to
13.35% while gaining twenty-two genes. The table and the question of whether
those are the right numbers are on
verification §0-BM; the fix, if they
are wrong, is a gene’s rarity tier and another run, never a
hand-edited founders block.
Sources: common/coat/pattern/SvgPath.java,
SpecPainter, SpecSchema,
wiki/gene-creator/js/svg-path.js,
intake/tools/svg-to-mask.mjs, and the twenty-two files in
horsegenetics/genes/.
2026-09-09
Four pages about making a gene become one, and the prompt is allowed to say a shape cannot be drawn.
A documentation session, run alongside the play session below and touching none of the same files. It started as “bring the gene-writing page up to date” and turned into a merge, because the reason the page was out of date was structural.
The page had missed five masks, and the reason it missed them is the interesting part
FRACTAL, PATH and CHOICE landed earlier the same
day; LUMA, EDGE and spreadFrom the day before.
None of them had reached creating-a-gene.html, so its machine-facing tab
was handing a model a vocabulary with no lace, no drawn shape, no branch and no
way to read the resolved colour — and cheerfully inviting it to
approximate. Both schema tables were re-transcribed whole from
expected.json rather than patched row by row, which is how they went stale
the first time, and the page now says so and gives the one-liner that dumps the
fixture.
Also caught up in the same pass: space: "local" in all four places it
appears, the parts-by-position rule, that a layer cannot see what the layer above it
painted, PATH’s body space where the spine is 0.62 and not 1.0, and
the six things the loader now refuses outright — several of which the
page still described as traps to be careful about, which they have not been for some
time.
“No commentary” was buying a worse failure than it prevented
The pasteable prompt used to end output ONLY the JSON, no commentary. That is right about the ninety per cent of the job that is transcription. It is wrong about the rest, and wrong in a way that hides: a model told to emit JSON and nothing else will always find something to emit, so the answer to “draw a shape this format cannot express” came back as a confident approximation with no sign attached that it was one. Every mask after the first eight exists because a person hit that wall and said which measurement was missing.
It is now conditional and narrow: ship the closest honest approximation, record it in
notes, then a fixed-shape TOOL GAP section — the element, how
the approximation falls short, why no composition reaches it, and a concrete mask or op
design with every parameter and default. Two at most, nothing else. The bar is the one
the page already sets for a human author: a
different measurement, not different tuning, with PATH and
“you have not found the numbers yet” both named as explicitly not gaps.
Then the merge, on the owner’s call that it was spread too thin
The split across four pages was not by subject, it was by accident of when each was
written: creating-a-gene.html had the traps, gene-format.html
had the parameter that caused them, modding.html had the Java escape hatch,
gene-effects.html had the effects vocabulary — and
which one you needed depended on already knowing the answer.
Making a gene is all of it in eight topical tabs:
Start here, Shapes, The file, Masks & ops, Effects, In Java, The prompt, Checks.
The material was moved by line range, not retyped. Four thousand lines of reference retyped by hand is four thousand chances to introduce something nobody would ever find; what is newly written is the connective tissue, and an assembler script refused to emit a page that still named a file it had just deleted.
The tabs are page-local — tabs.js has given a panel whose
data-tab is not one of the three wiki views its own button since the LUT
lab, but nothing had used it at this scale. Two things had to give.
sync-page-views.mjs derived views from the panels and would have
written [] for a page whose panels are all topical, hiding it from
every view at once; a page that names no wiki view is now left alone exactly as an
untabbed page is, and this one’s views: ["coding"] is hand-set. And
.tab-bar had no flex-wrap: three buttons never needed it,
eight do.
The unbuilt architecture was being written twice
Half of gene-effects.html was auras, pools, memory, goals and spawn variants
— none of which runs. The plan was to move it to
the trait architecture page, and on comparison
that page already carried a faithful summary of every one of those
sections. So the merge there is not a bulk move: horse-traits gained the
anchors the new page links into, the build-order table annotated with what actually
shipped against each step, and the divergences catalogue in place of a
slice-vs-architecture table that still described five live verbs.
Two things that were simply wrong
night_temperandnight_watchwere undocumented. Both have been registered onAbilityTypesince the night loci landed this morning, and the effects reference never got them — so it documented every verb but two, with no sign the other two existed. Both now have a section written from the declarations. Gap 160 is the shape of that failure rather than the instance.- The format page still said the creator cannot edit an
effectsblock. That stopped being true when the creator grew a form for it.
Nothing here touched the play session’s files. The eight Java edits in
this work are one-line doc comments and test failure messages that named a page which no
longer exists — including GeneWikiTool, which is the
generator for every gene page, so leaving it would have had the next bake
rewrite all of them with a dead link.
0.3.8 – 0.4.0 — the stall that killed a horse, and a long evening of the owner playing while I worked.
An unusual session in shape: the owner was in the game the whole time, so almost everything here came from a report rather than from a plan, and three things were confirmed working within minutes of shipping. The releases run together because they were handed over one at a time as the reports arrived.
The one that mattered
A horse was teleported into a wall and suffocated, and the cause was two
bugs that only killed anything together. The stall detector flood-filled air,
three blocks above the sign - and a fence is one block tall, so it climbed over the
front rail of every real stall, spilled into the aisle, exhausted its cell budget and
reported “not a room”. That dropped it into a fallback box straddling the
wall. Then landingSpot scanned that box, found nothing standable, and fell
through to return stall.signPos() — the block the sign itself
occupies, flush against the wall, with wall material at head height. It was the only
position in the whole routine never checked for anything, and it was reached exactly
when the stall was least understood.
The search now walks floor columns rather than air: it never rises, so it cannot climb a fence, and it steps at most one block between neighbours, so a slab or a carpet is still one room. A door, trapdoor or gate is the edge of the room in every state — judging a doorway by whether a horse could squeeze through it measures a different stall depending on which way the gate happens to be swinging, which is not a property a stall should have. Landing is measured live from the sign instead of read back from the record, so a rebuilt stall is the stall the horse arrives in; the chunk is pulled in first; and a stall with nowhere to stand refuses the ticket without consuming it. A refusal the player can read beats a horse in a wall.
The graph search moved to common/ as StallFill, behind a
one-method interface the game implements, with StallFillTest building
stalls out of ASCII. That is the cooldown sentinel's lesson applied rather than
restated: a piece of pure logic parked on the Minecraft side is a piece of logic nobody
can write a five-line test for. The test that would have caught this is six lines and
draws an L-shaped room.
Confirmed by the owner, in play
Shearing and milking both work — which closes the cooldown-sentinel gap, since those two had never worked on any horse, ever. Binding a stall sign works. Spending a ticket brings a horse home. Research papers off a book work. The Alleles tab collects. The gene previews turn in 3D and the owner likes them. The cowboy and the horseman read correctly as they/them.
Reports that turned out to be something else
“All of my horses have the exact same speed, health, jump, and size.”
True, and not a defect. The custom spawn egg starts every locus at
Gene.defaultAllele(), and an all-baseline genotype resolves to exactly
HorseTraits.baseline() every time — the rolled epigenome cannot vary
it, because epigenetic numbers ride on allele copies and a baseline locus contributes
nothing for them to modulate. BodyStatSpreadTest pins both halves so the
question is answerable in seconds rather than by reading six gene classes. Whether that
is the right default for the egg is now
gap 157 and nobody has decided.
“Milking does not work.” It did; the bucket had gone to the inventory rather than the hotbar. Worth recording only because it is a water bucket: there is no milk gene in the mod, the only bucket yield is waterborn, and the checklist task is called “Milk a mare” without saying the mare needs the gene.
The research shelf was written up as fixed and was not. Two faults the
first pass had not looked for: four fixed captions were wider than the 176px window and
drawn with raw g.text rather than the fitter three methods away, and the
filing slot had been inactive on the server for the life of the block because
its isActive() read a field only the client ever sets. Every paper filed
had been snapping back to the cursor. The layout constants had been moved, which was the
described fix; nobody had opened the window.
Built to order
Taming by hand. Crouch, hold food the horse eats, look at it, stay still: it walks over and eats out of your hand, and each mouthful is vanilla's own taming roll. Deliberately the same odds as being thrown off — a second door in, not an easier one — and diet-aware, so a carnivore wants meat and a dhampir does not come.
Bareback riding, twice. First it was made to bond, because
vanilla only calls a rider “controlling” once the horse is saddled and the
obvious test therefore paid a bareback rider nothing — and bareback is how you
talk a wild horse round in the first place. Then it was made to steer, at the
top bond tier. The first attempt drove the horse from outside on the tick and felt like
it; the owner's suggestion — fake a saddle and take it off on dismount — was
better, so it now equips a real saddle carrying a PHANTOM_SADDLE component.
The renderer feeds the saddle layer an empty stack, the saddle comes off the tick the
rider leaves, and the rider's inventory is swept, because you can open a horse's
inventory while mounted and this would otherwise mint saddles.
The breed book (wiki/breed-book.html) and a
Breeds tab. The page carries the mod's lore, set out by the owner: none
of these horses are native, every breed is an artefact of a collapsed civilisation,
gathered up and moved here by a race nobody has a name for, who then went away or
stopped coming or is still here and not introducing itself. The horses are thriving
without their masters and remain fond of people — which is the in-world reason a
wild Shetland will eat out of your hand. The hay portal and the breeding carrots are
that race's leftovers: they work, nobody knows why, and the mod never explains the
mechanism. The cowboy and the horseman may be the last of them or may have inherited the
instructions; the page leaves it open and argues that leaving it open is worth more.
The in-game tab follows the browser's standing rule — it fills in as you meet breeds, because every reference tab here is a record of your game rather than a manual. Only Getting Started and Recipes are complete from the first minute, since those are how you find out what to do at all.
Smaller
- The allele collection now sweeps every horse you own whenever the roster is gathered, retroactively — it had only ever hooked taming and breeding, so a bought or gifted horse counted for nothing and the tab read empty for a player with a full stable.
- The wild type left that tab: every horse carries it at nearly every locus, so it was
congratulating players for not having found the gene. Extension, agouti and
shade keep theirs, because at those three the baseline is a real coat rather than an
absence. Both ask
EditorRules.alwaysCarried, which already existed for the spawn screen, so neither keeps a list of its own. - Foals born to a tamed dam start at a quarter of her bond. Starting at zero made a horse born in your own stable exactly as wary of you as one caught wild that morning.
- Shearing was worth more bond than a treat. It is 2 now, level with a hand-feed.
- “Take a gene off a horse” is “Research a gene with a book”. Taking implies the horse loses something; it does not.
- The tutorial's contents lost their inconsistent numbering and its “Next:” footer, which was a second control doing the contents list's job.
- The cowboy and the horseman are they/them across twenty-three Java files and two
wiki pages. Singular they takes a plural verb, so every third-person
-sbeside those pronouns came back a step with it; pronouns about stallions were left alone.
What the next session should pick up
Gap 154 (closed 2026-09-10 - see settled decisions) — the Breeds tab ignores creative and
did not fill in on a tame. The creative half is certain and small; the taming half has a
named prime suspect (a BreedLineage token that is not a bare
Breed.id()) and should be checked there first.
0.3.7 — two things you could craft but not use, and two you could do that did nothing.
A repair release, and the repairs came out of somebody playing it and saying what happened. The shape of the day was the same as the last one: less building, more being told.
The bug that had never worked once
“Sheers aren't working. They say that the horse's coat hasn't grown back
yet” — on a horse that had never been sheared. Then the same for
milking. One cause:
HorseCooldownsAttachment.last(key) answers Long.MIN_VALUE
for a key that has never fired, and ready was
now - last(key) >= cooldown, which overflows to a large
negative elapsed time. So the gate said “not ready” for every horse that
had never done the thing — and since the stamp is only written on success, that
is forever.
Shearing and every gene yield had therefore never worked on any
horse in any build, and the dhampir's bite gate was dead the same way without
anybody noticing. The fix is one absent check. The part worth keeping is
why it survived: the class is Minecraft-side, so
there is no test source set it could have been caught in. The same record in
common/ would have been a five-line test.
Stalls: the gate was the broken part
“Binding a sign to a horse works, creating a stall / the interior detector
definitely doesn't.” StallDetector demanded the sign on the outside
face, a room sealed with nothing in it but air, and everything within one block up or
down of wherever the sign happened to be. A doorway failed it, a roof beam failed it,
a wall torch failed it, hanging the sign from the inside failed it. And stalls are
read by exactly one thing — the ticket — so the gate was standing in front
of the only feature it existed to serve.
On the owner's call it measures instead of judging: both sides of the wall,
!blocksMotion() instead of isAir(), five blocks of headroom,
and a plain box in front of the sign when nothing encloses. It cannot refuse.
Stall signs has the detail.
Tickets, built on top of it
Inert since they were added. The owner's spec: one-time use, teleport a horse back to its stall, and — against the original design — not bound to a horse. You click the horse you want sent. The tier is reach: basic within the overworld, bound within any one world, interdimensional across them.
The middle rung was read off the recipe ladder rather than specified —
the owner said outright they did not know what the third one should be, and an ender
pearl to leave the overworld / an eye and blaze powder to leave the world you are in is
what the recipes already said. bound_ticket is consequently named after a
design that no longer exists (gap 152).
Tickets.
Getting Started is a book, not a scroll
Asked for a table of contents; what it wanted was to stop being one long article. It is a section at a time now, the contents list picks one, and the checklist is split into its seven groups rather than thirty-odd boxes in a column. Every section ends with a link to the next, so it still reads straight through — a tutorial you cannot page through would have been a regression from the scroll.
New Bonding chapter, because none of it was written down where a player could
reach it: the rates, the fifteen-a-day ceiling and why it is there, and what each tier
changes about the animal. Four new checklist tasks — one per bond tier, credited
from syncCare so that every path that moves bond funnels through one
place, and one for giving a horse a barn name.
Two ticks that fired on the wrong thing
TRANSFER_PAPER was completed at the top of
onEntityInteract, before the target was known to be a horse or the held
item to be a paper — so right-clicking anything with anything ticked it. Found
while reading a stack trace for something else. BUILD_STALL ticked when
the sign was bound to the horse, which is a sign in a pocket rather than a stall. Both
moved to the act they name.
And the jar was lying about its own version
mod_version in gradle.properties names the jar;
neoforge.mods.toml carried a second, hand-edited copy that tells the
game. This release built as horsegenetics-0.3.7.jar announcing itself as
0.3.6, and was caught only because the file size came out identical to the previous
one. The toml is templated now and processResources declares the property
as an input, so there is one place to write it.
What the day cost that was nobody's fault
“Trying to use a nametag crashed my game” — a
ClassNotFoundException for HorseProgress, one of our own
classes, whose .class file has an mtime of the exact second of the crash.
A compileJava run underneath a live runClient. Not a mod bug,
and the second time this pattern has cost a debugging session — check the jar or
class mtime against the crash timestamp first.
Where the day ended.
Six releases, 0.3.1 through 0.3.6, and the shape of the day was outside testers: almost everything here started as somebody else playing the mod and saying what went wrong. That is new, and it changed what the work looked like — less building, more reading a stranger's stack trace and asking what they actually saw.
Two of the day's bugs were invisible from this machine and obvious from theirs. The encoder crash that made 0.3.0 and 0.3.1 unplayable fired the moment a horse came into render range, which a dev world reaches less reliably than a survival one; the empty stables needed a rotated jigsaw, and three quarters of them were. Neither would have been found by running the game here for another week.
And two of the reports were not bugs at all — a shared horse
inventory that was the player's own hotbar showing under every horse, and a crash that
was this session rebuilding common.jar underneath a running client. Both
cost real time to rule out, and both are now written down
(coding notes) because the second
one looks exactly like a packaging bug and would be chased again.
What is not done is the playing. The Equine Research Shelf has never been placed, the Getting Started tab has never been drawn, and thirty-one checklist hooks are proven reachable rather than proven correct. Those are gaps 143, 146 and 148, and they are the first thing the next session should ask about rather than build past.
A checklist that teaches the mod, and a collection that makes you want to find one more horse.
The mod now says what there is to do and keeps score. Thirty tasks under the Getting Started prose, in the order somebody learning would meet them, and every unticked one carries a line saying what to actually do — a player staring at an empty box wants an instruction, not a title.
It gates nothing, deliberately. Nothing in the mod asks whether a task is done; somebody who reads the wiki and builds a shelf on their first day ticks two boxes at once and is not stopped. The moment a checklist gates content it stops being advice and starts being homework.
The rule that made it real work
A checklist with an item nobody can tick is worse than a shorter checklist: it reads
as a bug and the player cannot tell which one is broken. So every task exists only
once something completes it — which meant hooks in thirteen files, each one
line, next to whatever event already existed. The enum is in common/ and
the hooks are not, so nothing but a sweep can prove they all connect;
tools/check-progress-tasks.mjs is that sweep and it fails by name.
It also caught its own bug immediately. Written through a shell heredoc, the check's
\b word boundary arrived in the file as a literal backspace, so
the regex matched nothing and it cheerfully reported all thirty tasks unwired. A check
that silently matches nothing is worse than no check; it is a plain
indexOf now, with a comment saying why.
The collection was half-built already
The Alleles tab is every allele in the mod with the unmet ones under question marks,
and most of what it needed already existed: the gene database has recorded
seen tokens per gene for a long time. What it could not do is record a
baseline allele, because an entry in that book is what knows()
tests, and creating one off an ordinary horse would have unlocked every gene in the
mod.
So the collection is a second set beside the book — a flat
geneKey|token per player — and the distinction is the interesting
part. Discovering a gene is a gameplay gate; collecting an allele is a
record. A plain wild-type allele belongs absolutely in the second and must
never be in the first. Sharing one map would have made one of those two behaviours
quietly become the other.
And the shelf stopped looking like a different mod
Reported: the craft page's icons overlapped its text and the whole thing looked rough. Both halves were the same mistake — placing things by eye. The list ran to y=96 and the slots sat at y=40, straight through it.
The layout constants now live on the menu, because that is where
addSlot needs them, and the screen reads every one; a slot and the well
drawn under it can no longer disagree. And the window is drawn in vanilla's idiom
rather than the browser's: mid-grey face, white highlight up and left, dark shadow
down and right, slots with the bevel inverted, dark text. Drawn, not blitted
— the thing that reads as Minecraft is the bevel, not the texture, and four
fills mean the window is whatever size the layout needs with no art to keep in step.
Two smaller things while in there. The shelf's item was the classic 26.1.2 trap: its
model was in models/item/, which this version ignores in favour of
items/, so it drew the missing texture. And an unattached signed transfer
paper now falls back to the blank paper's model through a
minecraft:condition on the deed component, instead of rendering nothing.
The spawn screen ran out of column, and the fix was to stop using it.
Gap 139 was written the day the right-hand column was widened, saying in as many words that the top and bottom groups would still meet on a short window. They did, and it was reported: “Clear genes” and “Make egg” overlapping.
The owner’s fix is better than the two I had written down. Both of mine (scroll the column, split the column) treated the column as the place buttons go; moving Spawn under the preview asks why the one button that does the thing was queued up with Copy, Paste and Cancel in the first place. The strip between the preview panel and the bottom of the window was empty and always had been. So the bottom group is three rows now instead of four, which is 24 pixels the column no longer has to find.
And this time the threshold is measured rather than asserted. The
top group ends at 208 and the bottom starts at height − 74;
they clear at 290, and once rightStep() squeezes to its 20-pixel floor
they clear at 270 — a 1080p screen at GUI scale 4, which is where the report
came from. Below about 258 they still meet, because seven 20-high buttons need 140
pixels even touching. That is written down rather than claimed fixed.
A page that opens first, a copy that takes a minute, and a browser that remembers.
The mod had no way in. Everything it does is documented somewhere on this wiki, which is no help at all to somebody who has just installed it and is standing in a field looking at a horse. So the browser has a Getting Started tab, it is the first thing that opens on a fresh install, and it is the first tab in the strip.
Thirteen sections, in the order a player actually needs them: tame one, notice that you need a mare and a stallion, feed them both a golden carrot, take a gene off a horse with a book, file it on a shelf, buy one from the horseman if you would rather, splice it into a foal, and repeat until the paddock looks the way you pictured. Then the horse dimension, because a door made of hay is worth knowing about.
The pictures are entities and items, not art. Item stacks go through the item renderer and the horseman and the cowboy are built as real entities and drawn the way the roster's horses are - never added to a level, made once, kept. Nothing has to be exported, nothing goes stale when a texture changes, and a resource pack the player has on is reflected. That matters most for the case the page exists for: recognising a villager you have never seen.
The flavour is not decoration
“Some horses are magic” and “breeds belong to biomes” are the two facts that make somebody go and look at a horse rather than walk past it, and both read better as an aside than as a heading. The horseman and the cowboy are written as a pair who built their posts side by side and are, the villages say, married - which is flavour the code half-suggested already, since the two workstations share a texture set on purpose.
One number I did not write down
The brief said a gene carrot gives “a 25% chance of getting that gene”. That is not what the code does. A Known Gene Splice sets the fed parent's gamete outright, so that parent passes the gene on with certainty; what is uncertain is whether the foal shows it, which depends on the other parent and on whether the gene needs two copies. For a recessive against an unrelated mate the visible rate lands near a quarter, which is very likely where the number came from. The tab was written to the code rather than to the number, and gap 145 puts the choice back where it belongs: a flat 25% roll is easier to explain and makes the carrot a gamble, at the cost of the one real virtue the current design has, which is that you can plan a breeding around it.
Copying costs time now
A shelf copy is one iron ingot's smelt - 200 ticks - per rarity tier, so ten seconds
for a common gene and a minute for a mythic one. The progress rides on a
ContainerData, vanilla's furnace pattern, because the menu already syncs
those every tick and a progress bar that lags is worse than no bar. The work is ticked
per open menu rather than per block, which is what lets two players at one
shelf each copy their own gene onto their own book.
And the browser stays where you left it
It was a fresh Screen every time the key was pressed, so it forgot
everything the moment you closed it - you came back to the top of the first tab
however deep you had been reading. The remembered position is now static state, which
is to say per session: the tab, both scroll positions, both search boxes, the sort,
the selections. Only the parts that name a horse are dropped, and only on
disconnect, so a UUID from one save can never select something in another.
Stables in mountains
Reported against 0.3.2. The three stables were beard_thin, which lays a
skirt of material under a piece without clearing what is above it - so a rigid
86-block building projected onto a slope comes out with one end inside the hill. They
carve their whole bounding box now. What is not fixed is the other half of
the cause: meadow and savanna_plateau are in the biome tag
and are mountains in all but name, and no terrain adaptation makes a flat-bottomed
stable sit well on a 40-block slope (gap 147).
A bookshelf that remembers genes, and a portal that finally notices its frame.
The Equine Research Shelf is the other half of the paper loop. A book on a horse gives you a paper for a gene it carries, at random; the shelf is where a paper stops being scarce. File one and that shelf will copy it onto blank books for as long as the original stays in it. Take the original back and the copying stops.
What makes it worth having is that it can be emptied. A one-time unlock would have been simpler and would have made knowledge a flag on a player; a shelf is a thing in the world that somebody could walk into and read, and that you can carry to a friend. Breaking it gives every paper back, deliberately - the alternative is a player never daring to move one.
A set of keys, not a chest
The block entity stores a Set<String> of gene keys. A research
paper is completely described by the gene it names, and the shelf holds one of each,
so the rule is structural rather than enforced: there is no way to express
"two papers for flaxen" in the data, so no code has to remember not to, and a shelf
can hold every gene in the mod without an item stack per gene. Keys become items
again at exactly two moments - a withdrawal, and the block being removed.
The tabs cost one small discovery: Slot.x and Slot.y are
final, so the obvious trick of parking a slot off-screen does not
compile. Slot.isActive() is the right mechanism anyway, and better -
vanilla consults it for drawing and for hit-testing, which is exactly the
pair that must never disagree. A slot you cannot see but can still click is the worst
of both.
The portal was never told its frame had gone
Reported plainly: knock a hay bale out of a lit portal and the interior stays. The
portal block had no updateShape, so it was only ever removed
deliberately. Vanilla's mechanism is the good one and it is barely any code - a
portal block that finds itself no longer enclosed returns air, and turning to
air is itself a block change, so one broken bale collapses the whole sheet in a
cascade and nothing has to know how big the portal was.
Adding it would have broken lighting a portal, and reading vanilla is what
caught that. Level.setBlock runs neighbour shape
updates unless bit 16 (UPDATE_KNOWN_SHAPE) is set - flag 2 does not set
it. Both places that build a portal used flag 2, so each block placed would have
asked its neighbours to re-check themselves while the rest of the sheet was still
air, and the new rule would have eaten the portal as it was being lit. Vanilla's
PortalShape.createPortalBlocks passes 18 for exactly this reason, and now
so do both of ours. The bug never existed outside a compiler, which is the useful
kind to find.
And the Recipes tab, twice
One row per discovered gene meant the splice carrots outnumbered every other recipe several times over and buried the 27 that are not gene carrots, so the tab gained a two-entry category dropdown defaulting to the small half. Each recipe also gained a line saying what the thing it makes is for, out of the lang file so it is translatable and so a recipe that converts between two items can say which direction it goes - packing a bundle and unpacking one share an output but not a purpose. The gene carrots' line is formulaic, as asked: there are as many of them as there are genes and the only thing that changes is the gene.
The H menu stops crafting, and a book becomes the way a horse tells you something.
"Putting crafting in the H menu breaks the flow of normal Minecraft so much it's confusing people." That is the whole argument and it is a good one: a window bound to a key, that is not a block, that nonetheless crafts, is a fifth thing to learn for no gain. Every recipe on that tab already worked at an ordinary crafting table — both custom ones are registered serialisers with real recipe JSON — so removing it cost nothing but the one thing the grid could uniquely do.
The browser is now a plain Screen. Losing the container was the
point rather than a side effect: with no slots there is nothing to drag,
nothing to lose on close, and no server menu to keep in step. Seven types went
with it — the menu, its recipe class, the menu registry, three payloads and
the paper writer — and the key now opens the screen directly instead of
asking the server to open a menu. It came to about twenty container-specific
references in a 1,400-line file, which was a good deal less than it looked.
One behaviour change was caught by reading vanilla rather than by running
it: Screen.isPauseScreen() returns true and
AbstractContainerScreen's returns false, so the conversion would have
quietly started pausing singleplayer — and a horse you were watching would
stop moving whenever you opened its gene list. Overridden back. It is the kind of
difference that does not show up in a compiler and does show up as "the mod feels
wrong", so gap 141 says plainly that there
may be others of that shape.
Recipes, generated rather than queried
The tab that is left is a reference to every recipe the mod adds. Getting the
ingredients to draw turned out to be the interesting part: the client has no
dependable way to ask for a recipe — data/ is datapack
territory and 26.1.2's client-side recipe API hands out display objects rather
than recipes, with nothing in api-notes about
either. So tools/bake-recipe-reference.mjs reads the real recipe
files and writes a flat summary into assets/, where the client reads
it straight off the classpath. 26 shapeless, one shaped, and two custom recipes
whose inputs live in Java and are described in words instead of drawn wrongly.
The trade is written down as gap 142: it
describes the recipes this jar ships, not the ones a datapack might have
replaced.
A book on a horse
Removing the grid removed the only targeted source of research papers, which would have left the splice chain fed by chest loot alone. The replacement is deliberately not targeted: right-click a horse with a book and you get a paper for one gene that horse carries, chosen at random. Choosing the gene yourself — which is what the old button did — meant a paper was never a discovery, only paperwork once you already knew. Now the horse decides. An entirely ordinary horse has nothing to teach and costs you no book, and discovery is deliberately not required, since reading the paper is the thing that teaches you.
Three smaller ones
The search box was closing the window. Typing "speed" into the
gene search hit e and opened the player's inventory. It is a vanilla
trap rather than a mistake here: EditBox.keyPressed handles the
control keys and returns false for an ordinary letter, because
letters arrive separately through charTyped — so the letter
falls out of super.keyPressed and the next thing
AbstractContainerScreen does is test it against the inventory key.
Any screen with a text field and a container behind it has this bug until it says
otherwise. A focused box now swallows every key but Escape; the letter still
types, because GLFW's character callback is a separate one.
Two mares now say why nothing happened. Vanilla does not know
horses have a sex, so a same-sex pair fed breeding carrots goes through the whole
courtship and reaches BabyEntitySpawnEvent like any other pair, where
the foal was cancelled in silence. The attempt is left exactly as it was —
they still try — and it now says so, in one of three lines per sex, to the
player who fed them. Light rather than an error: nothing has gone wrong, and a red
warning would read as a bug in the mod rather than a fact about horses.
♀ and ♂ after a horse's name, coloured, so sex is
readable across a paddock. Appended at render time through
RenderNameTagEvent.CanRender and never to the stored name —
that name is what the record keeps, what a transfer paper prints and what the
rename box shows, and a glyph baked into it would reach all three and then be
inherited. A display aid belongs at display time, which is also why the switch is
client-side.
A gene says what it does when you point at it — and seventeen of them could not.
Asked for plainly: hovering a gene in the custom spawn egg's list should pop up a
blurb describing what it does. The text already existed and already had the right
job description — Gene.description() is documented as
“one to three plain sentences for the in-game gene browser and
tooltips”, with built-ins reading GeneDescriptions and
data-driven genes carrying their own blurb.
Except for seventeen of them. The contract says an absent summary
resolves to "" and callers should treat that as “no
summary available”, which is honest and, on a hover panel, means the gene
silently has nothing to say while the one above it does. Coverage was 185 of 202:
missing were tiger eye, leopard, PATN1, PATN2, shadowcreature, the ten magic
utility loci, and two data-driven genes. All seventeen are written —
compressed from each gene's own wiki page rather than invented, which is the only
way to write a summary that will not contradict the page it summarises. It is 202
of 202 now, and GeneDescriptionCoverageTest fails by name
rather than by count when the next gene arrives without one.
The panel is drawn the way the dropdowns are — fills and text — rather
than as a vanilla Tooltip. Two reasons, and the second is the one that
decided it: a list row is not a widget on this screen (an unadded row takes its
click in mouseClicked), and the browser twin has no vanilla tooltip to
mirror. A panel built out of fills exists identically on both sides; a
Tooltip would have made the designer's version a reimplementation
rather than a copy.
Mirroring it turned up a real difference the two files had been carrying quietly:
rowAt() returns a view index in the screen and a
gene index in the browser, because the screen's rows carry their
own gene and the browser's are bare indices into genes[]. Nothing had
ever needed to tell them apart — both are only compared against the same kind
of thing — but a panel has to be positioned beside the drawn row, and with a
family filter on, the two indices differ. The browser now has a separate
rowSlotAt(), named and commented for exactly that, and
rowAt() is written in terms of it.
A play session, four reports, and one root cause behind three of them.
0.3.2 launches and makes a world, which was the thing worth knowing. The session that confirmed it then produced four more reports, and reading the log for one of them turned up a fifth nobody had asked about.
The stables generated empty, and the guard that did it was commented as doing something else
“The structures generate, but there are no horses anywhere in the structure.”
StablePopulator queues a stable when the chunk holding its
StructureStart loads, and guarded that with a comparison between the
bounding box's minimum corner and the chunk being loaded — under a comment
saying it queued “only the chunk holding the structure's own start”. Those
are different tests. A jigsaw start piece is placed at its chunk's minimum block
corner and then rotated, and SinglePoolElement.getBoundingBox rotates
about BlockPos.ZERO — so three of the four rotations carry the box
into the previous chunk in x, z, or both. Only unrotated stables were ever
queued. The other three quarters generated the building and stopped.
The guard was not merely wrong, it was unnecessary: getAllStarts()
already returns only the starts belonging to that chunk, and
StablePopulationData.claim() is what makes population happen once per
world. It now dedupes against the queue on the same (structure, corner) key
claim() uses, so the two cannot disagree. A stable also logs when it is
queued now, not only when it is filled — this bug's entire signature
was a missing filled line with nothing to say why, and one line without
the other is what tells you which half broke.
Two shipped assumptions, and they are the same assumption
The [Cowboy] and [Horseman] lines were going to
chat in a normal install, because DebugAnnounce.ENABLED was a
hard-coded true. It was hard-coded for a good reason, written down at
the time: it had been !FMLEnvironment.isProduction(), and a whole
session went by unable to tell “the gate is shut” from “the code
never ran”. The constant was not the mistake. The mistake was that the
sentence beside it — this mod is a dev build with one tester and is not
shipped — stopped being true and nothing went back to look. It is now a
debug.announce server config, on in dev, off in a normal install,
switchable in both, which is what you want from a player who has something to
report. The startup line that says which way the gate went is kept.
That is the second defect this week from the same root, after the encoder crash that was invisible on the only machine that had ever run the mod. It has a gap of its own now (gap 140), because the sweep has not been done: nothing has gone looking for the other places that assume nobody else is running this.
One hundred and twenty-one errors that meant nothing, every launch
Found by reading their log for the stables line. Genes loads the
shipped gene files from its own class initialiser, on purpose; then
ModGeneSpecs called loadAndRegister, which re-read the
classpath and handed every one of them back to a registry that throws on a taken
key. Each throw was logged at ERROR. Nothing was actually broken — the first
registration is the one that counts — and that is what makes it worth fixing
rather than shrugging at: it trains the log's reader to scroll past a wall of red,
and a real collision would be sitting in the middle of it. The classpath half is
gone; a drop-in that collides still throws and is still reported. The launch now
says how many genes shipped and how many segments the genotype code has, every
time, which is the first thing worth knowing from a pasted log.
The spawn screen's right column
“WAY too cramped”, and measurably so: the column was 96px wide with a
2px gutter, and "Spawn (creative only)" alone measures 111px.
A vanilla button neither shrinks nor wraps its label, so several were drawn straight
over the edges. It is 128 wide with a 4px gutter now, and the breed label fits
itself to the button rather than being cut at twelve characters — a number
that was a guess at what fitted a 96px button and was wrong in both directions.
The interesting part is what it exposed. Seven buttons grow down from the top of
that column and four are pinned to the bottom, and widening the gutter makes them
meet sooner. rightStep() now spends the gutter to buy room — but
seven 20-high buttons need 140px even touching, and a 1080p screen at GUI scale 4
leaves about 120, so below roughly 290px they still overlap exactly as they always
did. That is gap 139 rather than a fix,
because the honest answers (scroll the column, or split it) are larger than the
change that surfaced the problem. The designer took
the same numbers, per the standing rule that those two screens are one screen in
two places.
Two releases nobody could play, and a test that was watching the wrong cap.
This one arrived from outside, which is new. The owner handed the jar to friends: 0.2.0
worked, and 0.3.0 and 0.3.1 both disconnected on trying to make a world
— Internal Exception: io.netty.handler.codec.EncoderException: Failed to
encode packet 'clientbound/minecraft:custom_payload'. The stack, once it arrived,
named the line exactly: CoatSyncPayload.java:26, and under it
String too big (was 35568 characters, max 32767).
CoatSyncPayload is how a horse’s coat reaches the client — the
genotype and the epigenome as text, sent when a player starts tracking the entity. It
was writing both with a bare buf.writeUtf(v). That overload caps at
32 767 characters, throws rather than truncating, and a throw inside the encoder
takes the connection with it. A full epigenome is 35 304 characters
today. So the first wild horse to come into render range ended the session, which on a
freshly generated world is roughly immediately. Nothing about it is intermittent; it was
simply never seen here, because the trigger is a horse coming into view rather than the
world load, and a dev world gets entered and quit a lot.
The uncomfortable part is that a test exists for precisely this.
EpigenomeSizeTest was written the last time this threshold was crossed, and
it fired then, and it is still green. It was watching
SpawnCustomHorsePayload’s stringUtf8(131072) — the
one cap in the mod with a number written beside it. An implicit cap has no number to
watch. The test could only ever have guarded the site that had already been thought
about, which is the site least likely to be wrong.
So the fix is not the raised cap, it is GenomeCodeCodecs: two constants and
two StreamCodecs that are now the only way a genome code is allowed onto
the wire. Four call sites moved onto them — the coat packet,
StoredGenome (which was on ByteBufCodecs.STRING_UTF8, i.e. the
same 32 767, on a component that rides every inventory sync, so a preset
spawn egg or a filled genome jar would have kicked whoever picked one up), and the
genotype fields on the roster and spawn payloads. The test now guards each cap at half
budget rather than one of them.
The genotype guard failed on its first run, which is the entire argument for having written it: 5612 characters against the 8192 those payloads had been declaring — two thirds spent, on a number that grows with every gene registered, and nothing anywhere would have said so until it did the same thing the epigenome had just done. That cap is 32 768 now.
Shipped as 0.3.2, and both 0.3.0 and 0.3.1 should be treated as withdrawn — there is no way to play them. A world made under either is fine and opens normally, since the fault was in what the server sent and never in what it saved. The size problem underneath is untouched and is still gap 66: 35 KB of text per horse per tracking client is a bandwidth cost nobody has budgeted, and two paths — the horse record and the transfer deed, both of which cross the wire as NBT — still have a 65 535-byte ceiling that no code here declares and no test watches. A little over half spent.
The PATH mask — a shape you drew, stored as points rather than pixels.
Stopped at a clean seam rather than abandoned: both masks are complete on both sides, tested, parity-gated, documented, committed and pushed, the build is green and the tree is clean. Nothing is half-written and nothing is waiting to be reverted.
Resume from the roadmap's paused note, which says what the next job is and what question is waiting on the owner first.
The other half of §10, and the piece the drawing tools have to be written against. Every other mask in the vocabulary says what a shape is made of — a lattice of spots, a band of this width, noise at that scale — and lets the geometry and the horse's own numbers decide where it lands. That is the right tool for a marking with a rule behind it, which is nearly all of them. A lightning bolt, a crescent, a brand, a curl on one particular shoulder has no rule behind it: it is a shape, and the only honest way to describe it is to give its outline.
Control points in a plane, stroked as a line or filled as an outline, extruded through the
horse. plane picks the two axes — side is the flank and
appears on both of them, which is what a marking drawn on a side view should do.
space: "body" normalises the points over the horse, so a shape drawn for the
adult lands in the same anatomical place on the foal. Full documentation, which is the source
of truth, is on the gene file format.
Points rather than a baked stencil was the owner's call from the previous
session, and building it confirmed the reasoning: the shape stays procedural, stays legible in
the JSON, follows a resized horse, and is the same arithmetic in Java and in the browser. It
is the first parameter kind in the format that is neither a number nor a word, so
SpecSchema.Kind gained POINTS and the parser gained its errors.
Catmull-Rom, not Bezier, and the reason is the editor rather than the curve. A Bezier's handles do not sit on the curve; Catmull-Rom passes through every control point. Since the creator is going to draw those points as draggable handles, a curve that merely got near them would make the editor lie about its own data. The ends hold their tangent by duplicating the first and last point rather than extrapolating a phantom one, which is what stops a smoothed path from overshooting off the horse and back.
Two things found by building it that were not the job
- A mask
FLAGhad no checkbox. The creator's mask form sentPARTSandCHOICEto their own editors and everything else to the number/knob/per-dose control — soSPOTS'mirroroffered to be driven by a knob, which is not a thing, and could not simply be ticked.PATHcarries three flags, which turned an obscure bug into a blocking one. It is a checkbox now, andtidyMaskdrops an unticked one instead of exportingfalse. - The parity gate could not see
PATH_CURVE_SAMPLES. Sabotaging the creator's copy from 8 to 7 left the check green. The fixtures probe four texels per part, and one sub-segment more or less on a curve moves the line by a fraction of a texel — so a real divergence in a shared constant was invisible. It is not a schema parameter, so nothing compared it. It is emitted into the fixture and compared directly now, the way the condition flags are, and breaking it fails properly. The fill's crossing test was caught on the first try; both are verified by breaking them and watching parity go red rather than by assuming.
And one mistake worth keeping
The first PATH examples were written at v around 0.7 —
"high on the flank". Rendered, they were floating above the horse's back. body
space normalises over the whole-horse box and that box runs to the ear tips,
so on the adult mesh 0.33 is the underline and 0.62 is the spine. It is the
same trap WhitePattern.toplineHeight exists because of, met again from the other
direction: measuring against the whole box is correct for a mask whose author is
drawing on the whole horse, it is simply not anatomical. Moved to the barrel, and written into
the format page as its own warning rather than left for the next person.
Also worth recording: PathMaskTest failed twice on first run and both were
test bugs, not code. One probe point sat exactly on the Catmull-Rom midpoint of a
symmetric span, so "the curve should leave the chord" was false at the one place it was
checked; the other measured a stroke's softness fade and called it an overshoot. Both are
rewritten to sample the spline directly, where neither confound exists.
:common:test not run in full (owner's standing rule). Run:
PathMaskTest (13), FractalMaskTest, both coat goldens (neither moved
— no registered gene uses either new mask), GeneSpecParserTest,
GeneFilesTest, SpecGeneTest,
CreatorMetadataRoundTripTest, :neoforge-26.1.2:build, and every
wiki and parity check. Re-baked: spec fixtures, creator assets, designer wasm, search
index.
A gene can be a fractal — and a stale roadmap sent the session somewhere else first.
The session opened by picking the top item off
the roadmap and found two thirds of it already
shipped. §1.1 listed three white-pattern calibration defects; the entry for
EDNRB’s frame band still cited BAND_LO / BAND_HI, which
have not existed since the barrel-and-neck rewrite, and the entry for the
cover knobs described a threshold-on-a-bell that both
EdnrbGene.paintFrame and TobianoGene stopped being two sessions
ago when they became score-sort-and-cut-at-a-quantile. Both had been fixed and written up
elsewhere, and neither had been deleted from here, which is the failure mode this
page and the roadmap’s own header warn about. Both rows
are gone now.
The third row went the other way and was closed by decision. Medicine
hat was measured before it was argued about: over eight seeds a kit=SB1/SB1
horse comes out ears 0% white, the rear 30% of the head 0.4% — the
cap is genuinely there — against a 99.6% white mane and a 100% white
neck, so rendered it reads as two dark ears and a smudge behind them. The owner
declined to model it: “it will roll randomly, I do not care if it does or does
not”, and “the hat should not be a unique gene”. Nothing
retains for it and nothing rolls for it.
Gap 29 is now a record of that call rather than a
defect.
The FRACTAL mask
With §1 closed the owner picked §10, the visual
marking designer, and within it the fractal masks before the drawing tools — the
roadmap names them as what peacock and laced tobiano both want, they live
in common/, and the drawing tools sit on top of the mask vocabulary rather
than beside it. FRACTAL joins the mask vocabulary (GeneSpec.MaskType.values()
counts it): value noise summed over
octaves with lacunarity, gain and a domain
warp, and a shape that reads the summed field as
fbm, ridged or billow. It is documented on
the gene file format, which is the source of truth
for it.
Two things in it are deliberately not the textbook recipe, and the first is the reason the mask is worth a write-up at all.
- The octaves are normalised by the root of the summed squares, not by the
summed amplitude. Dividing by the sum — what every fbm snippet
does, and what
PatchNoise.fielddoes — makes the result a weighted mean of independent samples, and a mean is narrower than one sample, more so with every octave. On such a field an author who adds a fourth octave for detail also drags the coverage toward whateverthreshold: 0.5selects, silently, with no parameter to blame. That is the same defect thecoverknobs were built on — a knob written against a field nobody had measured, delivering a 3× swing where a 1.4× one was intended — and it cost a rewrite of two painters. Dividing by the root preserves the spread instead, so one octave is exactly aPATCHESmask and every octave after buys detail and nothing else. It was cheaper to not have the problem than to document it. shapeis applied to the summed field, not per octave. Ridging each octave and then summing gives a thicket; ridging once gives continuous lines that fork, taper and pinch out — the lace. It is the same moveBodyNoise.ridgealready makes on a single octave, and the oneSTROKESis built on.
FractalMaskTest pins both, and measures the naive normalisation
alongside so the claim in the comment is not taken on trust: six octaves of the
textbook form come out visibly narrower, and the shipped form does not.
Two things looking at it changed
- One octave is not bit-identical to
BodyNoise.value, only identical to a rounding step: the sum is written0.5 + (v - 0.5), which is notvin binary floating point. The test asserts a last-bit tolerance and the comment says so, rather than claiming an identity that is a hair off. - Lace is two or three octaves, not six. Rendered rather than
reasoned about: a texel is about 0.5 body units, so by the fourth octave of a
scaleof 5 or 6 the finest detail is below the sheet’s resolution and aridgedfield breaks into stipple — a perfectly good speckle, and not lace. The guidance to take the extra irregularity fromwarpinstead is on the format page.
All four contract files moved (SpecSchema, SpecPainter, the
creator’s schema.js and spec-engine.js), plus
gene-format.html. No new primitive was needed in
js/noise.js: the fractal sum is built out of value, which is
already ported.
The parity gate was proved to reach it rather than assumed to. Two
FRACTAL layers went into example-genes/prismatic.json — a
ridged one and a warped fbm one, so both branches are exercised
— and then the JS divisor was deliberately broken and check-parity.mjs
watched to go red before being restored. A fixture that never runs the new code is green
by definition, which is exactly how the UV swap hid for a day.
Settled for the drawing tools, which are not built: a drawn shape will
become vector paths in body space — a handful of curve control
points in body units — rather than a baked bitmap stencil. It stays procedural, stays
small in the gene file, survives a resized horse, and the same maths runs in Java and in
the browser. That makes the tool a curve editor rather than a brush, and it wants a
PATH-family mask before any UI is written.
Also settled, by reading the code rather than by asking: the roadmap
entry that said to decide whether the pipeline needed a scalar-expression channel
“before building the UI” was already answered. GeneSpec.Knob plus
Value.FromKnob lets any mask parameter be driven by a per-copy stored scalar
over a declared range, and Value.PerDose gives a per-dose triple beside it.
“Expression scales parts” is expressible today; what is missing is that the
creator never shows the channel.
:common:test was not run in full (owner’s standing
rule). Run: FractalMaskTest, both coat goldens (neither moved — no
registered gene uses the new mask), GeneSpecParserTest,
GeneFilesTest, :neoforge-26.1.2:build,
check-parity.mjs, check-links.mjs,
check-gene-tabs.mjs. Re-baked: spec fixtures, creator assets, the designer
wasm, the search index. Unplayed: nothing new in game — no shipped
gene uses FRACTAL yet, so there is nothing to look at until a gene does.
Twenty-five loci get the science they were built from — and three of them argue back.
The health layer and most of the natural colour genes have been carrying their
real-world basis in a Java doc comment and nowhere a reader could reach it.
Gap 89 had that written down as 25 pages missing from
the science view, with a check
(wiki/tools/check-gene-tabs.mjs) holding the list so it could not grow. The
owner supplied source material for all 25. Twenty-three are now written; the
backlog is 2.
Each tab is the same shape, because the source files were: the locus and the variant with its coordinates, the mechanism, the real inheritance table, breed-by-breed frequency where anyone has measured it, and an explicit three-way split between what is established, what is strong inference, and what is not supported. That last section is the one worth having — a page that only says what is known reads as more certain than the field is, and several of these loci have a famous unsupported claim attached that a player is more likely to arrive holding than the real one.
The point was never the citations
It was the fourth thing each page now does: state where the mod and the literature disagree, and which way. Until both halves were on one page there was nowhere that comparison could happen, and the risk was never that a divergence was wrong — it was that it was undocumented and would be “corrected” one day by somebody who had read only one side. Three turned out to be substantive enough to earn a gap of their own.
SCN4A kills a horse that lives. The mod
makes the HYPP homozygote lethal at birth. A real H/H horse survives, is more
severely affected than a heterozygote, and is ineligible for AQHA registration
— a studbook rule, which has no analogue in a game. This is the mod’s largest
flat contradiction of fact on the health layer, and it is a design call rather than a
misreading: HYPP is the one locus where the player makes the mistake with their eyes open,
because an H/N horse is visibly unwell and can still be excellent on every
other axis. Making the homozygote a cliff is what turns that temptation into a decision. If
a severity axis ever lands, H/H becoming much worse but survivable is
the fix — and it would cost the gene its cliff.
HMGA2 pays the pony allele in the wrong
direction. HEALTH_PER_P is +2.0, on the reasoning that a
pony is a hardy horse rather than a broken one and that a locus whose only effect is
“worse” is one a player fixes once and never thinks about again. Sound design.
The only validated health association at that locus is higher basal insulin and,
through it, laminitis risk — so the sign is inverted against the evidence. A metabolic
or laminitis system would give the real effect somewhere honest to live;
the diet gene is the nearest existing hook.
CKM is a speed locus the literature does not
support. Its only equine performance claim came from a 2010 Thoroughbred
candidate-gene study, failed that same paper’s own independent replication
sample — the combined analysis retained COX4I2, not CKM — and was
explicitly non-significant in a later Quarter Horse study. The biochemistry is real and the
genotype-to-phenotype link is not. The trade is deliberate: the mod wants three legible
speed genes of different sizes so a breeding programme has a gradient with a long tail, and
CKM is the smallest of the three precisely because it is the least defensible. It also
names its alleles T/C where the marker in the paper is G>A.
A fourth is drift rather than a decision. MATP
makes pearl commoner than cream — 1/22 against
1/30 — where the real ordering is emphatic in the other direction: cream
is in 25 of 28 surveyed breeds and reaches 0.42 in Lusitanos, pearl is in 7 and tops out
near 0.12 in Iberians. The cause is visible in the source comment: those are “the same
numbers the two old genes carried”, from when cream and pearl were separate loci tuned
independently. Merging them made the two figures directly comparable for the first time, and
nobody had looked.
Everything else that diverges, diverges for one reason
The missing age model, showing through on gene after gene. Cerebellar abiotrophy arrives at six weeks and worsens for months; SCID gives a foal a fortnight of perfect health on its dam’s borrowed antibodies; HERDA is a horse that is fine until you put a saddle on it at two; naked foal syndrome runs from weeks to three years; champagne foals are born blue-eyed and turn amber. All of them are charged flat, from birth. Written out across five pages it reads less like five separate approximations and more like one missing system, which is what it is.
Two pages needed a different kind of honesty. Diet has no
real locus behind it at all, so its tab is a real-horse feeding guide judged against the
mod’s own item list — and the finding worth having is that Dveg and
Dwht are the only narrow diets a real horse could survive on, while hay, the
one thing the mod treats as ordinary, is the only item on the whole list a horse actually
lives on. SHOX models one allele where the real locus has two
overlapping deletions whose compound heterozygote is also affected, which the
mod cannot currently represent.
The last two, and what they cost to be sure of
Tiger eye and rabicano came last, because their first source files both arrived holding a byte-identical copy of the mushroom write-up — all three hashed the same. Re-supplied, they turned out to be the two most interesting pages in the batch, for opposite reasons.
Tiger eye is the mod’s cleanest match to its source and asserts one
thing the source will not. TE1/TE1 and TE1/TE2 give amber, carriers show nothing, the coat is
untouched, TE2 is rarer than TE1 — all of that is exactly right, including the mod
setting TE2’s frequency at 0.6× TE1’s against an observed 20-to-8 split. But the mod
gives TE2/TE2 its own paler yellow iris, and the literature has one
documented TE2 homozygote — a palomino whose eye phenotype could not be separated from
the cream allele it also carried. The extrapolation is reasonable and it is an
extrapolation.
Rabicano has no gene at all. No causal variant, no locus symbol, no
validated inheritance model, no test — the strongest lead is a 1.7 Mb haplotype near
KITLG on ECA28, about 160 kb from the transcription start site. The
Rb symbol on the coding tab is this mod’s invention, which is now written down
where somebody looking for the paper will find it instead. What is striking is that the
mod already had the right structure: a dominant allele whose expression is a
per-horse roll that can come out at nothing. That is not a compromise between two
readings of the evidence, it is the only shape that reproduces both halves of what the
literature reports — apparent dominance in pedigrees, and expression ranging down to
invisible.
What the closure created
Gap 89 is closed and its roadmap section has been replaced rather than deleted, because finishing the 25 made a second gap visible underneath. Measured against the new tabs, which run 1,200–2,300 words, 17 of the 23 that already existed read as stubs: MC1R is 41 words for the foundational red/black locus, EDNRB is 62 for the frame-overo lethal that four new pages cite as their cautionary example, and MITF is 90 for SW1 — the common splash allele, whose PAX3 twin now has 2,024. And 20 of the 48 natural tabs name no breed at all; some correctly, but leopard complex without Appaloosa and ACAN without the Miniature Horse are the same hole one level down. It is written up, tiered by which pages the new tabs cite and cannot get an answer from, on the roadmap.
The other half of the audit was the tabs that already existed. Measured against the new ones, which run 1,200–2,300 words, the older science tabs are thin: MC1R is 41 words for the foundational red/black locus, EDNRB is 62 for the frame-overo lethal that four new pages cite as the cautionary example, and MITF is 90 for SW1 — the common splash allele, whose PAX3 twin now has 2,024. And 20 of the 48 natural science tabs name no breed at all. Some of those are universal loci where that is correct (shade, sex), but leopard complex without Appaloosa, ACAN without the Miniature Horse and megaesophagus without the Friesian are gaps of the same kind gap 89 described, one level down.
Some players would rather their saddles fit.
An owner's call, and a small one to build: the size loci now answer to a server
setting. body.size, true by default; false and every horse renders and
collides at 1.0 whatever its alleles say.
The reason it is the one body trait with a switch is the sentence that normally sells
it. Speed, health and jump are numbers; Attributes.SCALE is not, because
vanilla scales the hitbox from it as well as the model. So a Falabella and a
Percheron meet a saddle, a lead, an arrow and a two-block doorway in genuinely different
places, and that is a feature in most worlds and a nuisance in some. Server-side for the
same reason: a client that disagreed with the server about a horse's size would be aiming
at a horse that is not there.
The gate is one expression, in
HorseRecords.applyTraitsToEntity — the single place the resolved scale
reaches the game. Everything upstream is untouched: the size genes are registered, drawn
from the same founder tables, inherited and reported identically, and
Traits.scale() still resolves to what the alleles say, so the info panel, the
family tree, the browser and the inspect paper all keep showing the real number. Two
details were deliberate rather than incidental. It writes 1.0 rather than skipping
the write, so flipping the setting off and reloading actually shrinks the big horses back
instead of leaving them frozen large. And the other route to
SCALE — an effects attribute verb naming
scale, which no shipped gene uses but the table has always allowed —
answers to the same setting in both applyAttribute and
clearAttributes, so a modifier already standing comes off on the next reconcile
rather than being stranded on the horse by a setting change.
Two things came out of writing it down that were not in the ask.
A breed's height target rides on the same write —
stats.height in hands becomes a StatAxis.SCALE band, which is the
entire reason a Falabella looks like one — so with the setting off every breed in the
mod stands the same height. That is the honest consequence and not a bug, but it costs more
than "tack fits" and nothing in game says so
(gap 132, with the compress-rather-than-flatten
alternative written out). And both gene testers scale their preview by the
genotype and cannot know whether the server will honour it — the designer has
no server to ask at all (gap 133).
The thing this cost, now on api-notes: a
ModConfig.Type.SERVER file is written to the instance's
config/<modid>-server.toml, and a world's serverconfig/
folder is an optional override of that path rather than where the file is made.
The verification entry said the wrong one until the generated file was actually looked at.
Checks: §0-AX, and the first of them
is that the default is unchanged — a mistake here flattens every world
silently.
Ask the neck what shape it is — and six gene layers that had never painted a texel.
Two defects, both picked for being fixable and provable without opening the game.
The coat cache was unbounded, and no reload touched it
Gap 130, opened by the previous session’s
resolution analysis and closed here. GeneticCoatTextureFactory filled two
ConcurrentHashMaps with computeIfAbsent and never removed an
entry, so a client accumulated one permanently registered DynamicTexture per
distinct genome it had ever drawn — adult and foal separately, plus a mask for every
glowing one — and released them only on logging out. And there was no client
reload listener anywhere in the mod, so editing the gradient and pressing F3+T
appeared to do nothing at all.
The fix that matters is the shape of the cache, not its size.
TexelBudgetCache lives in common/ because the interesting part is
a policy rather than a texture: an LRU whose capacity is texels rather
than entries. One entry is a whole sheet, so what has to stay bounded as
SHEET_SIZE changes is the memory, not the horse count — an entry-count
LRU tuned today would quietly be sixteen times the memory at four times the sheet, which
is precisely the trap the resolution work is walking
toward. A 64-entry floor sits under the budget so the cache can never come out smaller
than a frame, which would not merely thrash but could release a texture an
already-submitted draw call still wanted. Every removal, eviction and clear()
alike, goes through one hook that releases the texture and drops the reverse-map entry.
CoatAssetReload is the listener; the event is
AddClientReloadListenersEvent, not the
RegisterClientReloadListenersEvent the gap guessed at, which is now on
api-notes along with the reshaped
PreparableReloadListener.reload signature.
Worth recording: the unit test caught an off-by-one in the eviction loop on its first run — it double-counted removals and stopped one entry early.
The neck’s geometry was written out by hand in eleven files, and six of them were dead
Gap 103. A Y band across the adult neck is a
collar: it wraps the crest and the throat alike, which is the bug that put
goth’s hood on the underside of the neck and had to be
reported twice. The workaround was a WAVES mask with a sawtooth longer than
the horse (wavelength 90) at an amplitude of 77.94, so the
displacement never wrapped and the band came out as a plane tilted to
2A/λ = tan(60°) — sixty because the neck’s own long axis
is sixty off horizontal.
The gap said five gene files. It was eleven — and the audit found
why it had spread so quietly. amplitude is in whatever space says
while wavelength never is, so six of the eleven had copied the pair
into space: "part", where 77.94 displaces the band twenty to forty normalised
spans off the horse. Measured over all 1280 neck texels, beetle pearl, diamond
scutes, duskfall speckle, ink scroll, nebula points and wyrmwood sigils selected
exactly zero, on every horse and at every seed. They loaded, they registered, they
had icons, and the layer was inert. Nothing could have said so: a mask cannot fail, it can
only select nothing.
HorseSkinGeometry.local is the replacement — the inverse of
posed’s frame change, giving a body point as fractions along the edges of
the part’s actual cuboid, read by AXIS, WAVES and
RAMP as space: "local". On the neck, axis: "X" is
across its depth (low is the crest, which is the face the mane box sits on) and
axis: "Y" is along its length, so a band there is a collar square to the
neck rather than to the world — which is what ink scroll and wyrmwood sigils
described all along. Details on body space and
the gene format.
The old trick turns out to be exactly that coordinate. The fit is
affine with a maximum residual of 0.0002 over the whole neck, which is what made the
conversion safe rather than hopeful: the five genes whose bands did land converted with a
texel-for-texel identical selection, and four of them — cosmic, shark, stars, suit
— came back byte-identical in the bake golden. Only goth moved, because its
$depth knob was in body units and is now a fraction; the range of hoods is
unchanged and an individual horse redraws. Thirteen golden rows moved in total, which is
exactly the six revived genes plus goth and nothing else.
What used to be two constants is now assertions. PartLocalFrameTest pins
that local equals part space on an unpitched part, differs sharply on the pitched neck, and
that the old sixty-degree coordinate is still affine in the new one with the slope the
neck’s 7-unit depth implies — and it establishes “the crest” by
asking where the mane box is rather than by repeating a number, so it survives the
neck being reproportioned. Beside it, GeneFilesTest now refuses any
WAVES amplitude far above 1 in a normalised space, which is the general form
of the mistake all six made; it was checked by breaking a gene on purpose and watching it
fire.
The thing worth carrying forward
Both defects had the same shape and it is worth naming: a failure that produces no output at all is invisible to every test that hashes output. The six dead layers were in the bake golden the whole time, hashing consistently, because a mask that selects nothing is stable. What found them was measuring where a gene paints, per part, rather than whether its bytes moved — and that measurement took twenty lines. The same reasoning is why the cache defect needed a texel budget rather than an entry count: both are cases where the number that has to stay true is not the number the code was counting.
The sheet size turned out to be a variable in disguise, eight mods got a verdict, and the whistles got a design.
A documentation session — no Java changed, no gene moved, no golden touched. Three questions asked, and the first one had a better answer than expected.
Scaling the coat sheet is mostly already done
The question was “what breaks at half, eight, sixteen, thirty-two and sixty-four
times the current sheet?”. The premise of the question was that resolution is baked
in everywhere. It is not. HorseSkinGeometry.faceMapsOf
multiplies vanilla’s own 64-space texOffs by
TEXELS_PER_UNIT, and HdHorseModel divides by the reciprocal
through a per-cube texScale, so the UV layout has always been a function of k
and nobody has ever typed a texel coordinate into the geometry tables.
CoatBuildContext.size() already exists as the seam a gene should read.
So the answer is a short list rather than a rewrite. The absolute references, all of them:
- The eye rectangles in
CoatRegionsare literals — but every value divides exactly byTEXELS_PER_UNIT, so they are 64-space rectangles that were pre-multiplied once and can be un-multiplied with no behaviour change at the current size. That makes the refactor provable by the goldens. EyePatch’s four-bit iris. “A sector is four bits because an iris is 2×2” is a rendering fact that stops being true in both directions: it survives larger sheets as a coarse quarter-split and dies at half size, where the iris is one texel and both heterochromias have nowhere to go.SpecPainter.spreadis the only genuinely bad one. It is a per-texel square search whose radius is derived from k, so it is correct at every resolution and quartic in cost — k² more texels each searching a k²-times-bigger box. It has to be redesigned rather than re-parameterised, and a signed distance field computed once per bake would speed up the current resolution too.- The sample grid is a
Sampleplus aBodyPointobject per mapped texel — an object graph, so roughly an order of magnitude more memory per texel than the finished texture, static and never freed. It is the largest allocation in the mod and nobody had counted it.
No gene indexes on texel coordinates at all. Every gene takes
(px, py) from forEachTexel as an address to write to and does its
arithmetic on the BodyPoint beside it; there is no per-texel hash, no
neighbour step, no threshold on px anywhere in
genetics/genes/. That is the body-space decision paying off years later, and
it is why raising the resolution is supersampling rather than reinterpretation. The
genes that are coupled to k are coupled through the three shared sites above, plus
the soft one nothing can detect: constants tuned by eye at the current size, which the
goldens cannot isolate because they all move at once.
Two things also worth having found. Storage is free — no coat is ever written to disk, so resolution costs nothing in save size, jar size or bandwidth. And OptiFine is irrelevant: it, ETF and EMF solve the resource-pack selection problem, which a mod that generates its texture does not have.
The point of the exercise: paint on the player’s own pack
The resolution work is the prerequisite for the feature the owner actually wants
— read whatever white horse the player’s resource pack ships and paint the
genome on that, at the pack’s own resolution. Checked, not assumed: vanilla
26.1.2 ships horse_white.png and horse_white_baby.png at 64²
under exactly the two paths this mod shadows in its own namespace, and the
ResourceManager the factory already uses resolves through the active pack
stack. So it costs a namespace change, plus an un-mirroring pass to give the adult
sheet’s four legs and two ears their own patches — the foal needs
none, because vanilla’s baby model already gives each leg a distinct patch
— plus making the sheet size a variable. Two tiers, no fallback chain: a pack that
ships our namespaced template gets it used verbatim, otherwise we derive from
minecraft:.
The undecided part is left undecided on the page: a pack whose “white” horse is cream would tint every coat in the game, because the composite is a multiply that assumes the template is shading and nothing else. Normalise it, accept it as the pack’s intent, or gate the feature — owner’s call, flagged rather than guessed.
All of it is on the new texture resolution page, which is now the single source of truth for the subject.
Eight mods, and the finding that the blocker is usually a version
Compatibility had the accidental half — what happens when two mods meet and neither knows about the other. It now has the deliberate half: what a patch would actually have to do. It decomposes into four seams (entity identity, the mesh and its UVs, the renderer, gameplay overlap), and the recurring finding across SWEM, Icy’s Better Horses, Horseman, Horse Powered, Horse Combat Controls, AstikorCarts, armour-only mods and the Fresh Animations pack is that the overlap is nearly always one subsystem and the blocker is nearly always a version rather than a technical conflict. Of the eight, three are co-installable with 26.1.2 today.
Two ideas came out of it worth more than the verdicts. User-written model
remaps: HorseSkinGeometry is already a table of pivots, boxes and
64-space texOffs, and an OptiFine/EMF .jem is very nearly the
same table — so describing someone else’s horse mesh is a file, not a fork, and
every gene and both editors would work on it unchanged because no gene has ever known what
mesh it was painting. What it cannot express is non-box geometry, which is exactly what
blocks the SWEM patch. Gaits: the mod resolves speed, jump and health and
then says nothing about movement, which throws away a real heritable breed-defining trait
it already has the inputs for. A GaitContribution resolving in
common/ to a named gait plus a tempo, with a thin per-mod animation mapping on
the NeoForge side, would make the gene work shared across every detailed-leg mod instead of
written once per mod.
Callable horses: bind the whistle, not the player
The Callable Horses mod binds one personal horse per player to a keybind. Taking
the feature, not the shape — a player here has a stable, and picking one favourite is
the choice this mod exists to make hard. The binding goes on the item:
ModDataComponents.BOUND_HORSE already exists on the stall sign and its javadoc
already anticipated being reused, so a bound whistle is that component on a whistle, bound
by right-clicking the horse the way every other item in this mod claims one. Many horses,
many whistles, and the component’s name field puts the horse’s name on the
tooltip for free.
Keybinds: the item is the interface. The mod has exactly one production
keybind (H, the browser) and everything else is an item and a click. Callable
Horses needs two keys because it has nowhere to put the binding; we do not. At most one
optional convenience key, registered the way HorseBrowserKeyBindings does it.
Writing it up settled three questions the roadmap was already carrying, which is the
part worth recording. Bond gates the bind — a whistle that binds to a
barely-tamed horse makes bond meaningless. Path if it can, teleport if it
cannot, using the range BondFollowGoal already walks. And the three
whistle tiers become loaded chunk / any distance / any dimension, which finally
gives the echo whistle an identity that is not just a bigger number, and puts the
configuration in the crafting ladder rather than in a config file. Against the tickets, the
split is clean: a whistle brings the horse to you, a ticket sends it to its
stall — the interdimensional ticket and the echo whistle being the same idea
pointed opposite ways.
Declined deliberately: respawning a dead horse. A horse here is a genome and a pedigree
and the breeding layer rests on death being real. The one genuine unknown is finding an
entity in an unloaded chunk — HorseRoster already documents that
whereabouts are entity facts — and the answer is an index in the per-world horse
database, which the browser, the stall records and the tickets all want as well. So it is
shared infrastructure to be scoped on its own, not whistle work.
Found on the way
Gap 130, in two halves and both present-day
defects at the current resolution. The generated coat textures are never released until
logout — clear() does exactly the right thing and its only caller is the
logout handler — so a session accumulates one registered DynamicTexture
per genome it has ever drawn. Harmless now, and the first thing that stops the sheet ever
getting bigger. And there is no client reload listener anywhere in the mod,
so F3+T leaves every baked coat on screen and leaves the templates and the
LutSet holding the old pack’s pixels — which means editing the
gradient and reloading appears to do nothing, and which is a hard prerequisite for the
resource-pack feature above.
Nothing was built, so nothing new is waiting in verification. The compatibility verdicts are read off the other mods’ public listings rather than their source, and the two that matter most — whether Horse Powered’s mob capture preserves data attachments, and whether Icy’s replaces the renderer for its breed coats — are exactly what a listing does not answer. They are stated on the page as checks, not conclusions.
The random number generator was never mixing its seed, and two goldens had to move to fix it.
Eight gaps, taken as four pieces of work. The largest one changed every horse in the mod.
Three gaps, one root cause
SeededRng wrapped java.util.Random, whose seed handling is a
single XOR against a constant. That is not a mix, and three separate
entries on the gaps page turned out to be the same fact seen from different angles:
- Gap 44: the first
nextFloat()off sequential seeds spanned 0.58 to 0.95, mean 0.77 - never touching either end of [0,1). It had already cost an hour on a 62/22/16 split that measured as 100/0/0. - Gap 117: because each gene reads its knobs at its own offset in a shared stream, and the offset is its priority, moving two unrelated genes pushed magic jump's mean 1.7% out - fifteen standard errors on a distribution that had not changed.
- Gap 47: a related but separate defect -
Epigenome.fromSeeddrew every gene from one stream in registry order, so adding a locus renumbered every gene after it and the horse at seed 13 became a different horse.
The seed now goes through splitmix64's finaliser before
Random sees it, and each gene draws from its own generator namespaced by its
key. The first measurement becomes 0.0002 to 0.9999, mean 0.5043;
neighbour covariance drops from about 0.013 to about 0.001.
The convincing evidence was a deletion.
MagicBodyStatGenesTest carried its own splitmix64 finaliser as a local
workaround, with a comment saying the real correlation was the gaps page's problem rather
than the test's. That workaround was removed rather than left to double-mix, and the file
passes without it - which is the fix demonstrating itself, and leaves the test as a
tripwire if anyone takes the scramble back out.
What it cost, and the check that nearly went wrong
It changed every horse ever generated, exactly as the gap said it would: 883 of
1056 lines of the pipeline golden and 218 of 623 of the bake golden, plus 154 gene
icons, the designer wasm and the spec fixtures. The check that mattered was not the count
but the membership: the genes that moved are exactly the genes that store
epigenetics, with acan and b4galt7 correctly untouched.
Regenerating the pipeline golden turned out to be impossible as documented
(gap 128). Its javadoc said "run the test, copy the
file it writes"; it only wrote that file when the golden was missing, never on a
mismatch - so build/coat-golden.txt held an older run's output, and copying it
back reinstated a stale golden while the test went green. The tell was a file that had
"moved by zero lines" while the test was still failing, which cannot both be true.
Two tests then failed for no real reason, and both were the fragility gap 47 describes - a claim about a distribution pinned to specific draws. Neither was widened without measuring first: the splash dose response was comparing single horses at seed 3, which after the reseeding became the single unluckiest draw in sixty (+0.08 where the mean is +0.43), and the grey spread was a range over 8 seeds - the statistic most sensitive to a small sample, since it depends only on the two extremes. Over 24 seeds it is about 45 against a threshold of 20. Both now state their claim over a distribution.
The last ungated mirror
Gap 100 is closed and the creator's geometry port
is inside the parity gate. posed() existed twice - in
HorseSkinGeometry and again in model3d.js's
emitPart - in two languages, in two files that were not obviously a pair. It
now exists once in geometry.js, with emitPart calling it;
528 cuboid corners were verified bit-identical before and after, so the
preview mesh did not move. The fixture bakes every part × face on both skins: 924 new
checks, 5485 to 6409.
The probes are deliberately not only corners. A corner is a fraction of 0 or 1 on both axes, and several ways of getting the axis pairing wrong agree at every corner and disagree everywhere else - which is the exact shape of the UV swap that once hid behind a stale fixture for a day. Proved by regressing the port three ways: swapping the body-Y/Z pairing (460 failures), dropping the pitch rotation (402), flipping one sign in the normal (44).
A per-horse choice, and a mirror bug caught before it shipped
Gap 102: the mask fold has no branch in it, so
quarter faked one by sampling noise at a scale of 4000 body units - a
hundred times the length of a horse - and amplifying it until the clamp turned it into a
yes or no. The new CHOICE mask is the real thing: an integer drawn once per
horse, constant across the body, exactly 0 or 1. Both of the trick's residual defects go
with it - the slight drift across the body, and the 1-in-401 strip that clamped to neither
end and produced a half-strength quadrant.
Writing it turned up a 64-vs-32-bit modulus hazard, and that is why the
example gene uses options: 3. The draw is mixed and the modulus taken over the
low 32 bits only, because the creator mirrors this in JavaScript where a 64-bit remainder
needs long division the u64 helpers do not have. Java over all 64 bits and JS over the low
word agree for every power-of-two number of options and silently disagree for 3, 5
and 6 - so a two-way example would have been green either way. With a three-way choice in
prismatic.json, reintroducing the mismatch fails 72 parity checks.
Wing cloak switched off
Parked rather than patched, at the owner’s call: it paints under two dozen texels out of roughly five thousand, which is invisible to a person and was marginal long before the reseed moved it across the icon baker’s floor. It wants retooling, not a lowered threshold.
The killswitch is one line — "wing_cloak.json" out of
index.json — because that file is the only thing that registers a gene.
Everything else is derived and fell out on the next bake: the designer’s
genes.json, the generated spans of pages.js and
index.html, the landing card. Every file still exists, and the wiki page carries
a banner explaining the state and how to undo it.
It changed no horse, and proving that was worth the two minutes. All 1056
composed-coat hashes in the pipeline golden came back byte-identical — only the
wing_cloak=n/n segment left the genotype code prefix, which is why all 1056
lines looked like they had moved. The bake golden lost exactly its three lines and
nothing else. That second fact is also a clean demonstration of
gap 47’s fix: before this session, removing a locus
would have redrawn every gene registered after it.
Two measurements, and one gap that is a writing job
Gap 53 is measured and the answer is reassuring: about two founders in three wear white from the two ordinary-marking loci, mean coverage about 10%, only about 8% loudly marked. The estimate on the page was three in four, so it was close and slightly high. The measurement has a trap in it: counting white on a founder directly says 91%, because on a flat greyscale chart a cremello, a grey and a palomino are pale coats with no marking on them. The giveaway was 91% wearing white while only 78% carried either locus. The test composes each founder twice and differences them, so the base coat cancels.
Gap 89 was recounted rather than fixed. The recorded
24 is 25; a briefly-feared 157 is wrong, because 131 of those are magical genes where having
no science tab is correct. None of the 25 has an orphaned real-world section that could
simply be moved, so it is genuinely a writing job and wants the owner - filling those tabs
means writing veterinary material, not generating it.
wiki/tools/check-gene-tabs.mjs does the code-side half: it holds the 25 as a
written-out allowlist and fails if a new natural gene page arrives without a science tab, or
if one on the list gains a tab and is not removed from it.
Two parser refusals, three live bugs, and the discovery that the composer was never inside the parity gate.
A code-only audit pass: things findable and fixable without opening the game.
The parser now refuses what has already shipped broken
Two gaps had been half-fixed on 2026-09-08 - the broken files were corrected and the checks were never written, which is the half that stops it happening again.
- Gap 105: a
perDosetriple on an expression that can only ever be reached at one dose is a constant written in the form that looks most like a variable. A coloured expression is reached by two copies of the second allele, so its dose is 0 and it readsperDose[0]- which authors write as0.0, because a dose of nothing should draw nothing. A chance of 0 means the mask never fires, and five genes shipped painting nothing at all. - Gap 106:
smoothstepwith its edges reversed is a hard step in the original direction - it does not reverse the ramp, it removes it. The test is possibility, not certainty, because the worse of the two historical cases was a knob whose range straddled its ownfrom: half the horses drew reversed and nothing about the file looked wrong.RAMPis deliberately exempt, its from/to being a linear interpolation where reversal is a thing an author might mean.
Both were scanned across the corpus before being written, and both trip zero shipped genes - 513 banded masks, 130 of them pointing at knobs. So these are prevention with no migration behind them, which is the cheapest moment to add a rule. Six tests cover the two rejections and the three shapes that must keep working.
Three live bugs
- One item could not be crafted.
Cowboy Hitch and
Horseman’s Table had byte-identical shapeless
recipes, so the recipe manager resolved one and the other was unobtainable in survival -
and those blocks are how both merchant NPCs come to exist.
Villagers stated the shared recipe as though it were fine,
which is how it survived being written down. They now differ along the line that page
itself draws between them: the hitch is rope-heavy, the table takes the leather.
neoforge-26.1.2/tools/check-recipes.mjsrefuses the next collision. - The gene creator had been drawing every dark coat too dark, for months. See below - this one is the interesting one.
- Eleven repeated HTML ids, so eleven sections could not be linked at all
while every link to them still looked correct. The link checker now refuses a duplicate id;
it found the duplicate
gap-46on the gaps page itself, two different gaps under one number.
The composer was never inside the parity gate
The creator’s mirror of the composer had rgb === 0 ? PURE_BLACK_ALPHA :
0xFF - an exact equality against pure black, which is what the game used to have
too. The game was changed to a ramp when a LUT edit moved the chart’s black corner off
#000000; the mirror was not. So the branch had stopped matching anything at
all: black bakes to about #161515, the equality never fired, and the
creator drew every dark coat at full opacity.
The finding is not the line, it is why nothing caught it.
check-parity.mjs runs restrict() and tint() and never runs
compose() - the magical cases are seeded with a synthetic flat colour field rather
than resolved through a chart - so the entire composite path had no net whatsoever. The gate
looked comprehensive and had a hole the exact size of the bug.
The composite itself still cannot be compared in the harness: it needs the gradient PNG and
the white template, and there is no image decoder in Node there. The arithmetic can be,
and the arithmetic is what drifted - so the fixture now carries answer tables for
nearBlackAlpha and the shadow lift, generated from Java, and parity checks them.
Verified by restoring the old equality and watching three checks fail, one of them on
#161515 exactly.
What this says about the other mirrors
There are four JavaScript ports of common/ in the creator -
spec-engine, schema, geometry and the composer in
fields.js - and until today the gate covered two of them.
Gap 100 is the same shape and still open:
geometry.js never received posed(), and the arithmetic lives one
directory over in model3d.js. It is now a small job rather than an open question,
because the technique exists: export the function, bake a table of Java’s answers into the
fixture, compare. That is what closed the composer.
Two genes that make a horse frightening, a per-gene coat test, and a category audit that found the taxonomy was sorting on the wrong axis.
The night loci
Two new mechanical genes, and between them they are the mod’s first deliberate attempt at unsettling rather than pretty. Night temper has eight variants in two families — four that hunt and four that flee, each naming who it feels that about. Night watch has five, and they are a ladder rather than a list: stares from anywhere through walls, closes to ten blocks first, watches only while it can see you, moves to where you cannot see it, stands directly behind you. Any of the five silences the horse’s footfalls at night, which is what makes the locus work at all — the behaviours are unsettling in proportion to how little warning you get, and hoofbeats are warning.
The temper locus supersedes the watch locus whenever it has something to
act on, because a horse that both stalks you and flees from you is not two behaviours, it
is a bug. That rule could not live in common/: whether there is a cow within
sixteen blocks is a question only the running game can answer. So the handler resolves the
temper first and withholds the watch ability if it found a target, and the goal reports
canUse() == false and stands down instead of fighting for the navigation.
Both founder tables are entirely heterozygous (owner’s call), which
nothing else in the registry does. Every feral horse carries exactly one variant and none of
them expresses it, so the plainest outcome each locus has is the one that cannot be
caught. It broke a test, and the test was right to break: GenotypeTest
asserted that a maximal founder roll lands every gene on its default homozygote, and these
two have no such row at all.
The coat pass
Nine genes, all from the owner’s own list. Three were painting a ground over the whole horse before marking it — coccinella black, shieldback green, tribal ward pale — which made every carrier the same animal underneath, the opposite of what a coat gene here is for. All three grounds are gone.
Removing them broke two genes, and the break is the interesting part. Shieldback and tribal ward each had a “coloured” allele whose only job was to vary the ground. With the ground gone the two alleles came out byte-identical — a locus with two names for one outcome. Both now point their unused hue knob at the marking instead, which is what “a colour the line carries” should always have meant on a gene whose ground was never the interesting part.
- Koi was painting the bright patches and their complement, so the two tiled the horse and no base coat showed. The complement layer is gone from the orange and dark varieties; the white one keeps it, because white-against-colour is the variety whose whole point is the hard boundary.
- Snow cloud’s white was a flat
PARTSmask over every part, so the base coat could only ever be not-seen. It runs through a large-scalePATCHESmask now, so there are bare drifts — which is what the gene’s own name describes. - Cleave’s “detached spears” were fat ovals scattered over a band up to 42% of the horse long, which read as white splotches loose on the far half. Long and thin now, and tied close behind the line.
- Opossum’s edge is dithered rather than faded, and the distinction is worth stating because the format makes it easy to get wrong. Coverage scales an op’s strength, so an axis ramp times a speckle gives faded dots — every dot present, each one weaker — which reads as a gradient with texture in it. Dots that get rarer is a density change, and density is a speckle parameter no mask can drive. So the falloff is three bands with hard edges and falling densities, which is this format’s way of writing a density gradient.
- Nyxborn stars only what is genuinely black. On the
shipped gradient a black horse reads 0.807 on
LUMA’sdarkchannel and a bay reads 0.782 — no threshold separates those without losing half the black. What does separate them is saturation: 0.02 against 0.556, because the difference between them is colour and not brightness. - Stained glass gained a grisaille palette — grey and white only, the colourless glazing a real window uses where it wants light rather than a picture, and the one palette that reads as glass on any base coat.
The category audit found the taxonomy sorting on the wrong axis
The complaint was concrete: hood and
magic item drop were in the same category. The cause
was general. Magical families are derived from priority bands, and
MAGIC_CORE had become a 24-gene dumping ground holding coat markings, hair
colour, milk buckets, spawn eggs and a horse that stares at you through walls.
Priority is a real fact about a gene that paints and an accident about one that does not. For a painter it says where in the stack it lands, and the bands were laid out as families precisely because the paint order and the taxonomy want the same grouping. A gene that paints nothing has a priority only because every gene needs a slot in the genotype code, so banding on it grouped genes by when they happened to be written.
So the non-painting magicals are named explicitly now, by what they do — yield and death, behaviour, trails and emissions, and the body stats that already had their own band — and the bands go back to meaning what they were built to mean. It is the same split the naturals already make, where anything invisible is a health locus; this is that idea applied to the other half of the registry, with the groups spelled out because “invisible” covers more ground here.
The coat baking test
CoatBakeGoldenTest closes gap 124: every
registered gene, every variant allele, composed through the real gradient and hashed.
203 genes in 623 lines, and it runs in seconds rather than needing the
ten-minute suite — which is the property that matters, because the pipeline golden is
expensive enough that its cost gets weighed against its value and this one is not. It was
checked by nudging one gene’s strength by a single point and confirming it fails.
It hashes; it does not look. Four of the twelve genes that broke their hashes yesterday were fixed by the change that broke them, so a diff here is a prompt to go and look at the icons, never a verdict — and the test says so in its own failure message.
What the audit of the generator turned up
- Phase 1 chains its layers and phase 3 does not, and only phase 1 says
so. A
LUMAmask reads the coat as it was before this gene painted, so “replace the white, then star the black” is not expressible in that order. Nyxborn works around it with a union mask. Gap 125. - Two genes make opposite total claims about white. Extreme white dominant holds a lock that beats every gene at every slot; Nyxborn promises to paint every white out. The lock wins, which is correct and falls out of the design — but neither description mentioned the other, so the only way to find out was to breed the pair. Gap 126.
Also: shadowcreature’s page had no preview widget at all — it was written from the template for a gene that paints nothing, and shadowcreature paints plenty. That is why its previewer “was not loading”.
2026-09-08
An overnight upkeep pass: a link checker, the invert bug measured and fixed, and two rules that now enforce themselves.
Left to do “whatever upkeep seems necessary” with the long jobs unattended. Four things, and the second is the one that mattered.
A link checker, and two links it caught
wiki/tools/check-links.mjs is new: every internal href and
src across the wiki must resolve to a file that exists, and every
#fragment must find an id on the page it points at. Nothing had
ever checked this, and the wiki is hand-written HTML with several thousand cross-links
added in two bulk sittings.
It found two real breaks, both mine from the previous two commits:
magic item drop linked to a page called
item-preset-horse-spawn-egg.html that does not exist (the preset egg is
documented on spawn eggs), and the session log pointed
at known-gaps.html#gap-107, which had been closed and deleted. The fragment
half is the part most likely to rot, because an id is invisible in the rendered page.
It also has a --orphans mode for the other half of
gap 115. It reports none: the two gene pages not in
pages.js (shade and
accretion field) are deliberately absent,
because GeneWikiTool.foldedInto omits a locus that paints nothing and is
documented inside the page of the gene that depends on it. Both are linked from their
parent. Not a bug, and worth having established rather than assumed.
The invert bug: measured, then fixed
Gap 120 was opened the same day it was found, on stained glass, and left for the owner on the grounds that fixing it moves ten shipped coats. Measuring it settled the question. A throwaway probe rendered every gene that leads with an inverted, parts-restricted mask and counted texels inside and outside its declared parts:
gene inside OUTSIDE parts it paints but never declared
iridescent_jewel 5600 6176 [HEAD, MUZZLE, MANE, TAIL, EARS, all four LEGS]
opal_wing_veins 5600 6176 [HEAD, MUZZLE, MANE, TAIL, EARS, all four LEGS]
elytra_veins 3874 6176 [HEAD, MUZZLE, MANE, TAIL, EARS, all four LEGS]
webbed 1481 6176 [HEAD, MUZZLE, MANE, TAIL, EARS, all four LEGS]
moth_mantle 7208 4568 [MUZZLE, MANE, EARS, all four LEGS]
lace 8322 2008 [MUZZLE, MANE, TAIL, EARS]
fracture 4751 520 [MUZZLE]
6 176 is every texel the gene excluded, and webbed was putting four times more paint outside its declared region than inside it. That is not a curiosity to leave for a decision; the before-and-after render is a horse that had been dipped in white with a net on its barrel, versus a bay with a net on its barrel.
The first fix was one condition — a texel the parts test ruled out stays 0 through the invert — and it was half right. It fixed the four, and it broke the opposite idiom: galaxy’s black field and tribal ward’s pale one are both “a ground, with a cut aimed at one part”, written as a broad first mask and a narrow inverted second one. Zeroing outside the cut deleted the ground everywhere the cut did not reach. Tribal ward went from a painted white horse to a plain bay with swirls on it, which is visibly not what its own blurb describes.
The rule that serves both is that a mask’s parts
mean different things depending on where the mask sits. The first mask defines the
layer’s region, so outside its parts coverage is 0 and an invert does not bring it
back. Every later mask modifies that region, so outside its parts it has nothing
to say and contributes its combine’s identity — it is skipped.
PARTS is exempt, because there the list is the mask rather than a restriction
on it.
Twelve icons moved. Galaxy and tribal ward came back byte-identical to before any of this. Webbed and the other three stayed fixed. And five more moved that nobody had been looking at — masked, ehretia, peafowl, snow cloud and war mask — every one a layer that had been losing coverage on a part it explicitly listed. Masked’s “mask over the head and ears” was not reaching the ears. Ehretia’s “dorsal stripe from withers to tail” was not reaching the tail. Those are the genes that quietly paid for the second half of the bug.
The parity check was green about all of it by default, which is the
part worth remembering. example-genes/prismatic.json had no layer whose second
mask was narrower than its first, so nothing in the fixtures could tell the two semantics
apart. A discriminating layer was added, and then checked by deliberately regressing
the JavaScript twin: 264 parity checks fail under the old rule and pass under the new
one. Re-baking a fixture that cannot reach the change is the same trap as not re-baking it
at all.
It also explains an entry already on this page. Gap 119 records webbed being rebuilt after a contact sheet showed it as “a white ground with brown islands rather than a net”. That was this bug all along, and the rebuild had treated the symptom.
Two hard rules that now enforce themselves
CommonPortabilityTest scans common/’s own sources for a
Minecraft / NeoForge / Mojang import, for Map.of() or
Map.copyOf( outside CommonMaps, and for the two Java 9+ calls
that were removed for the TeaVM and Java 8 targets. Nothing had ever checked hard
rule 1 or hard rule 2, and both fail in ways the compiler cannot see — a
green build, a shipped mod, and the damage in a browser or in a backport that does not
exist yet. That closes gap 116.
Writing it turned up the boundary worth recording: the check was too strict at first and
flagged light’s six-entry Map.of(…).
The trap is the empty immutable map specifically, a populated one gets a sized
table, and that gene paints — so the browser exercises it on every preview and would
have died on it the way the ALL mask did. Allowed, and the test says why.
And the long job
Full :common:test and a regenerated golden coat file, since ten genes
changed what they paint. The bakes were re-run first, including
:web:bakeDesignerAssets, because common/ moved and the designer
runs the real engine rather than a port of it.
0.3.0, and the release procedure earning its keep on the first run.
Tagged 0.3.0 for the mechanical-gene batch — a minor bump rather than a patch, because eight new behavioural loci and six coat genes are features and the genotype code moved again. Both version strings, the releases page, the tag and the GitHub release object.
The procedure’s first step caught something on its first real use.
“Re-run every bake and confirm it moved nothing” turned up a modified
wiki/gene-creator/js/examples.js: :common:bakeCreatorAssets copies
example-genes/prismatic.json into the creator’s example list, and the
previous session had edited prismatic (to exercise the new LUMA and
EDGE masks for the parity check) without re-running that bake. The creator
would have shipped an example gene missing the two masks the release is about. The
regenerate table in CLAUDE.md only said “the coat PNGs or the name
tables” for that task; it now names example-genes/ too, in the same row
rather than a new one.
The golden coat file was regenerated, which is what
gap 118 said to do at the next deliberate suite run and closes
it. It had been two sessions stale — forty-six genes across the two imports, each of
which moves Genes.codeOrder() and so every horse’s epigenome slots. The
suite is genuinely green at this tag rather than green-except-one-known-failure, which is
the state a tag ought to be in and had not been.
Fourteen genes, a mask that asks the gradient chart what colour it made, and eight loci that paint nothing at all.
The brief was “a bunch more mechanical genetic toggles, so a breed can be described by what its horses do”, plus six coat genes. It came to fourteen genes, two new masks, six new effect verbs and one correction to the effect format that had been wrong since it was written.
The mask that started it: LUMA
Nyxborn was specified as “replace all white with black, then put tiny white stars
over all the black”, and the request added: use the LUT to decide what black
is. That is not a detail, it is a different question from the one the format could
ask. PIGMENT reads melanin — a pair of levels that mean
nothing until a gradient chart turns them into a colour. “Black” and
“white” are facts about that colour, and the
LUT locus can hand a horse a chart whose black corner is
violet. Such a horse has identical pigment everywhere and is no longer black anywhere.
So the format gained LUMA, which thresholds
the resolved colour — what phase 2 got out of the chart, plus whatever the
magical genes before this one painted. Magical genes only: the natural phase is what
decides the pigment the chart is handed, and the overlay pass has already spent the
accumulator, so the parser refuses it in both rather than let it read zero.
Its channels were picked off measurement rather than guessed. A throwaway probe baked
nine representative coats through the real gradient and printed the readings, and the
useful finding was that white is not light: a
bald white texel reads 1.000 on the achromatic floor, a perlino 0.871, an old grey 0.698, a
palomino 0.475 — while all four are “pale”. On a palomino “the white
markings” and “the pale parts” are different sets of texels, and only one
of the two channels tells them apart. The same probe gave the black end: black 0.807, bay
0.782, chestnut 0.599, which is why Nyxborn’s stars land on a bay and not on a
chestnut.
The four genes that were hunting for the horse’s own white through pigment — fielded, yalia, voided and opalized — were moved onto it, which is the second half of what was asked for. The three that hunt for black through pigment (integration and opalized’s dark side) were left alone: nobody asked, and moving them would shift shipped coats for no stated reason. Worth doing deliberately one day.
Two more pieces of vocabulary
EDGE— the rim of each body part’s box, on the face the texel is on. It is the one mask that asks about the model rather than about body space, because a field sampled at a point has no idea where the horse stops, and on a Minecraft horse the line where two of a box’s faces meet is the only thing that reads as an edge. Tron is built entirely from it.spreadFromonPIGMENTandLUMA— which side aspreadmay take its growth from. It is the only way to ask for the bottom edge of a marking rather than its whole rim, and bloodstained is the gene that needed it: mask the white, then multiply by the non-white grown upward, and what is left is the band just inside the marking’s lower boundary and nowhere else on it.
The six coat genes
Nyxborn, stained
glass, Tron, ocular and
bloodstained are gene files.
Shadowcreature could not be: its three halves land in
three different phases, and the gold eyes with no sclera can only be reached from the
overlay pass, because CoatRegions.redrawEyes restores the eyes from the
template as the last act of the bake.
Every one was baked as an icon and looked at, and four were wrong on the sheet in ways no test would have caught:
- Ocular drew nothing at all at first, then blobs, then eyes whose irises
sat beside their whites. The last one is the instructive bug: an eye is four
concentric
SPOTSdraws off one lattice, andstretchdivides the sample before the lattice walk — so a different stretch per layer is a different lattice, and the parts of one eye scatter. Every layer of a compound element must share its stretch. Also: at two texels to the body unit, aradiusunder 0.5 is sub-texel and most cells paint nothing. - Stained glass blackened the mane, tail and ears, which are not in its
parts list. An
invertis applied after the parts test, so an inverted mask with apartslist paints everything it excluded. Fixed here with a leadingPARTSmask — and ten shipped genes have the same shape, recorded on known gaps rather than changed silently. - Nyxborn’s stars went from 161 texels to blotches to right, on the same sub-texel arithmetic.
Eight loci that paint nothing
Swim speed, water breathing, fighter, on death, item drop, mob aura, meat and milk volume. Three shapes between them:
- Codominant, per-copy percentage — swim speed, water breathing,
fighter.
AbstractMagicFactorGeneisAbstractMagicStatGenewith the last step replaced: the same genetics, aGeneAbilityinstead of a number onTraitBuilder. They are two classes on purpose.Traitsis four numbers and a condition list, re-derived from the genotype and stored nowhere, and it is the record meant to survive a version port unchanged — “swims faster” means nothing without a running game, which is the lineGeneAbilityexists on. - Recessive switches — on death, item drop, mob aura, meat. Each has a compound heterozygote that says something true rather than arbitrary: two different variants give the wild type, because “what happens where this horse died” and “does a mob want to be near it” cannot have two answers.
- A production trait — milk volume, codominant and summed, and meat, whose two copies are averaged. The difference is a claim: a percentage is a dose and two copies are twice the push, but the amount of meat on an animal is a property of the animal, and a homozygote is one horse rather than two half-horses.
The founder rule, and its one exception
The standing rule for a magical locus with an invisible carrier is that the wild population sits on the combinations that show — item drop, mob aura, meat and the coat genes all follow it. Magic on death does not, at the owner’s explicit instruction: carriers only, never affected, so a feral horse never craters the ground it dies on. The argument for the standing rule assumes the outcome is something you would want to find, and an exploding horse is not; a player who has never heard of the gene should not lose a stable to it.
Six effect verbs, and one thing that was simply wrong
charges, breath, on_death, item_drop,
mob_aura and combat, all translated and all flagged unverified in
their own comments. attribute was also finally wired — it had been parsed
and logged-once since it was written.
Its list named swim_speed, and there is no such attribute.
Vanilla has never had one; the thing that means “how fast does this mob move in
water” is water_movement_efficiency, Depth Strider’s attribute. It
had gone unnoticed because the verb was never executed, so the name never had to resolve to
anything. That is worth remembering about any verb sitting in the format unbuilt.
The interesting one is charges. Milk volume has to reach the milk
gene’s three fillings, and the two loci cannot see each other’s
epigenome — a gene is handed its own values and nobody else’s,
deliberately. So the verb names a yield kind rather than a gene, the milk locus
tags all three of its fillings "milk", and a future gene producing some other
fluid is governed for free instead of by somebody remembering. Charges divide the cooldown
rather than banking uses, so “three times a day” is three fillings spread across
the day — which needs no new stored state at all.
Two tripwires fired, both correctly
- A full epigenome is 35 459 characters, past the half-of-cap
headroom
EpigenomeSizeTestguards. The cap doubled to 131 072 on both sides. Every gene with an epigenetic schema costs a segment and the count only goes up, so this will fire again; raising the cap is the right answer rather than trimming a gene to fit a number that was arbitrary to begin with. - The showcase magical share drifted to 0.667, just past a 0.66 ceiling. That edge moves upward every time a magical coat gene is registered — the incidental tail is “did any of N genes happen to express”, and N only grows. Widened to 0.80 rather than pinned, because pinning it means editing the number on every gene import and the assertion is about the forcing working at all.
The golden coat file is knowingly a run further behind. Fourteen genes
change the registry order, which changes every horse’s epigenome slots, which changes
every hash in it — and regenerating it needs a full :common:test.
Known gaps has it.
A gene file can hold its own reasons now, and thirty-two genes arrived to test that.
Two things, and the first is why the second is worth having. The gene format gained a
notes block — on the gene and on each expression, an
array of paragraphs, printed on the generated wiki page as written. JSON has no comments,
so everything an author actually knew (why the wavelength is 90, which two layers must stay
in that order, what the source description asked for and where the build knowingly does
something else) used to go on a hand-written page and drift from the gene the first time
either moved. It lives beside the layers now. See
the format and
creating a gene, which spells out which of the
three prose fields a sentence belongs in.
Then thirty-two genes off the intake folder, in three waves as they
were dropped in. Every one is a translation rather than a transcription: the
sketches were written against a vocabulary this mod does not have — boxes at explicit
coordinates, rings at named centres, arithmetic inside JSON, spread on ops
that have no such parameter, pigment ops on magical genes — and each gene’s own
notes block now records what it asked for and what was done instead. That is
the first real use of the field and the reason it was added.
- Looked at, not just parsed. All thirty-two were baked as icons and
read as a contact sheet, and four of them were wrong on the sheet in a way no test
would have caught: banded socks came out as a solid sock (the band was half the leg, so
every repeat overlapped), beetle pearl as a single blob, opaline zebra as a smudge, and
webbed as a white ground with brown islands rather than a net. The zebra is the
instructive one — it was built from
STROKES, which wander, and a zebra whose stripes wander is a brindle.WAVESis the only mask that repeats evenly, and that is what it is for. index.jsonis hand-kept and nothing says so. A gene file dropped in the folder loads perfectly in a dev run and is absent from a built jar, because a jar has no directory to walk. That is now a contract of its own next to the four-file ones.- Four things the format cannot do, learned one rejection at a time and
all four recorded in the genes that met them: no arithmetic in a value, no addressing a
point on the body,
spreadis aPIGMENTmask parameter rather than an op one, and a magical gene may not use a pigment op at all (the parser says so outright, which saved two of these).
Invert's preview window: an empty map, in the browser only.
Gap 107 is closed, and the cause was not in the gene.
TeaVM’s empty immutable map traps with remainder by zero on the
first get — it hashes the key into a table it never sized — so
Map.of() and Map.copyOf() are correct on a JVM, correct in the
game, and a live grenade in the wasm every wiki page runs. Invert
is the only gene in the registry with an ALL mask; an ALL mask
takes no parameters; its Params map was therefore empty, and the painter’s
very first lookup on it killed the bake. Everything else about the gene was fine, which is
why a session had already checked every value it consumes from Java and found nothing.
- Found by running the wasm under node. The gap said it needed a browser
console, which is the one tool this end does not have — but the TeaVM runtime
takes a
nodejsflag and reads the module off disk, and the four textures the pipeline wants can be dumped out of Java as int arrays. Ten minutes of that gave the exception and a stack. Then synthetic genes registered throughregisterGenesbisected it to the mask in three runs: anALLmask failed, aPARTSmask with no parts failed, aPARTSmask with parts did not. It was never aboutINVERT. - Eight sites now build their maps through
CommonMaps, which is aLinkedHashMapbehind an unmodifiable wrapper — correct empty on both targets, and Java 8 clean, which hard rule 2 wanted anyway. Nothing enforces the rule, so the nextMap.of()incommon/reintroduces it: gap 116.
A gene may now say which picture is its picture.
Which allele combination and which base coat a gene is illustrated on is
measured — bake the candidates, keep the loudest — and that stays the
default, because it means no list goes stale when a gene starts or stops painting. But
loudest and most recognisable are not always the same thing.
Flametouched’s homozygote is a whole-horse ember
gradient and shouted down the flames the gene is named for;
Patina moves more texels on a tobiano than on the plain bay
it reads better on. Both now declare a preview block, which the icon baker,
the gene page’s opening coat and the preview window’s opening outcome all obey.
Two genes out of a hundred and fifteen; the thresholds behind the measurement are
gap 114.
A second palette, and the tools stopped keeping their own list.
The LUT locus has a green-through-pink allele beside the
blue-pink one. The gene was built to grow and did: a row in LutGene.VARIANTS
carries the allele, the expression table, the carrier wording and the founder odds. What
did not follow from that row was the offline tooling — the icon baker, the
wiki baker, the sample tool and the golden test each wrote Map.of("bluepink", ...)
out by hand, four copies of a list none of them knew when to update. They call
LutSet.fromRegistry now, which is the same walk the game has always done.
Smaller, and all owner's calls.
- Panda’s white band is half again as wide: the body keeps its coat outside 0.22–0.70 rather than 0.30–0.62. The freckle band moved with it, because it reads the same two edges.
- Opalized dropped below voided (611 → 609), and synort came down from the fields band to 106, among suit and hood at the bottom of the order. 106 rather than the 104 first tried: 104 put it under dhampir, whose own test says only suit and hood may be painted under a dhampir — being invisible on every dhampir is not what "one of the lowest" was asking for.
- A test that failed for a reason that had nothing to do with its subject.
Moving those two priorities shifted magic jump along the epigenome stream, and 3000
sequential seeds put its mean 1.7% out — fifteen standard errors, on a
distribution that had not changed.
Epigenome.fromSeedis ajava.util.Random, whose seed scramble is one XOR, so neighbouring seeds are correlated streams and the draw at a given position in them is correlated across the whole sample. The test now runs its index through a splitmix64 finaliser, which is what makes n a sample rather than a stream offset. Whether the underlying correlation matters to real horses is gap 117 and the owner’s call, because the fix would change every horse ever generated.
Not done: the coat golden file is a run behind — thirty-two genes
moved every hash in it, and regenerating it means a full suite run, which the owner has
asked not be done casually. The next deliberate run will fail on
CoatPipelineGoldenTest and that failure is expected;
gap 118 has the recipe.
Released 2026-09-08: 0.2.0, and two gene pages that outlived their genes.
Cutting a release rather than building anything, and the interesting part is what the process caught. The tag is 0.2.0, not the 1.1 that was asked for: the only previous tag is 0.1.0, there is no 1.0, and to be verified is still the authority on how little of this has been seen in a game — a 1.x on a mod where most of what is built has never been rendered in a client would be a claim the project cannot support. Owner’s call, taken on that reasoning.
- The re-bake was not a formality. The session being packaged had
already staged its regenerated artefacts, and re-running the five bake tasks over the
same tree still moved four of them —
index.html,gene-opalized.html,genes.jsonandwiki/horse-designer/wasm/web.wasm. Nothing warns about this. A staged artefact looks exactly like a current one, and the designer would have shipped a wasm built before the last two gene edits. This is the whole reason the regenerate table inCLAUDE.mdis phrased as “re-baking is part of the check, not a chore beside it”, and it is the second time that sentence has earned itself. - Two gene pages outlived their genes, and the wiki’s search went on
serving them.
collar.jsonbecamesuit.jsonandwaves.jsonwas folded into Cleave as an allele; both genes correctly vanished frompages.jsand the landing page, because the bake writes those spans from the registry. Theirwiki/gene-*.htmlfiles stayed on disk, unreachable from any nav and still picked up bybuild-search-index.mjs, so searching the wiki for “Collar” returned a whole page about a gene that no longer exists.bakeGeneWikiPagesowns that directory and knows the registry, so it can sweep it; until it does, a gene rename leaves a lie in the search results. Gap 115. - A releases page, because the release notes had nowhere to live.
The repo allows exactly two markdown files, so there is no
CHANGELOG.mdto write and a release body pasted into GitHub is not a source of truth anybody can find later. Releases is the player-facing record — what shipped in each tag, by theme, without the reasoning, which is what this log is for. The README now points at it and says plainly that there is no back-compatibility layer and a new version will not load an old world. - A pushed tag is not a release, and the difference is invisible from
here. The tag went up with
--follow-tagsand everything read as done; the owner then went looking and the Releases page was empty, because GitHub files a bare tag under Tags and a release is a separate object that has to be created.ghwas not installed, so it now is - user scope through winget, no elevation - andgh release createis written into the release procedure along with the check that matters afterwards:gh release view --json isDraft,assets, because a release publishes perfectly well with a jar that never attached. The lesson is smaller than the mechanism: the last step of shipping is looking at the thing you shipped from where the user looks at it, and "the tag pushed" was not that. - The jar was opened rather than assumed. 0.1.0 exists entirely
because
common/was not folded into the jar and a dev run cannot notice, and everything added since that fix is resources - 85 gene files, 49 breed files, the name tables, four structure NBTs - which would fail the same silent way. So the built jar was listed: all of them are in it,suit.jsonpresent withcollar.jsonandwaves.jsongone,ExtremeWhiteDominantGenecompiled in andPinkHairGenenot. That is the current tree and not a stale build, and it turns the scariest-sounding item on the checklist into a much smaller one: what a zip listing cannot answer is whether the runtime finds them, which is still §0-AQ. - An interrupted test run blocks the next one.
:common:testfailed outright onUnable to delete directory ... test-results/test/binary, left locked by the run that was killed mid-flight. No java process held it andgradlew --stopdid not clear it; removing the directory did. Worth recognising rather than re-diagnosing, because it reads like a build break and is not one.
Built 2026-09-08: a gene that changes the rule instead of the coat, and the paint order rebuilt round it.
Owner-driven, and the whole session hangs off one observation: the mod’s magical markings are painted after the natural ones and mostly cover them, so a horse carrying both a tobiano and something loud reads as the loud thing alone. Three separate requests were about that, at three different levels.
- Extreme white dominant is the general answer, and it is the first gene here that paints nothing at all. While it is present, a white texel is final: every magical gene still runs, in the ordinary order, and every one of them is discarded wherever the coat is already white. The obvious implementation is wrong and it is worth saying why. Giving it priority 999 and repainting the white last is a slot, and a marking sitting above the slot paints over the white exactly as before; there is no number high enough, because “highest” is a position and the rule is not positional. So it is a capability -
WhiteLockContribution, read out of band by the composer the wayLutContributionis - and the mask it produces is applied at every step of phase 3 and over the overlay pass. Its own priority is a code-order slot and nothing else. The pipeline page has the mechanism. - The lock is seeded from phase 1 and grows as phase 3 runs. Seeded from the pigment field, because the texels the melanin genes left unpigmented are the natural white markings, exactly and by construction. Grown after each gene paints, because the owner asked for “prevents the painter from ever changing a pixel if it’s been set to white” - so white a magical gene puts down is locked from the moment it lands too. The eyes are cut back out and never re-added: a blue eye on a white face is pigment biology rather than a marking, and locking it would silently delete every eye-colour gene on precisely the horses whose eyes are worth looking at.
- The specific answer was to move nine genes. Anything that reads somebody else’s marking has to be painted after it, so fielded, yalia, voided and opalized went to 600+ and invert to 700. In the other direction, dhampir paints the whole horse absolute white and sat at 135, above every data-driven gene in the mod - so a dhampir was a plain white horse whatever else it carried, which is a dead end rather than a phenotype. It is at 105 now, and suit (102) and hood (103) below it. The paint order is a new page that draws the whole stack from the registry at run time, highest first, so this is the last session that has to describe it in prose.
- Waves was not a gene, it was Cleave backwards. Owner’s call, and once said it is obviously true - both split the horse in half along a boundary, one taking the front and one the back. They are one locus now with four alleles (white and coloured, each way round) and Cleave’s spear boundary drawn on both. Cleave also gained the half-opaque outline it was asked for, built by subtracting the hard-edged tooth field from a wide-edged one:
SpecPainter.bandramps outside the band, so the difference is a strip on the coat side of the boundary, hugging it and following the teeth. The three hard masks are subtracted one after another rather thanMAXed first, which the fold cannot express and does not need to - over-subtraction can only happen where one of them is 1, which is inside the field, where the field has already painted. - Two genes gained an allele that reads the other end of what they already read. Opalized’s
Opbtakes the iridescence to the black regions instead of the white, which needed no new mask type at all: the white form asks for texels with no pigment left, this one asks for texels at the top of the black channel. Yalia’sYlois the white form’s last layer on its own - the outline round the horse’s own white, and nothing else - so it adds no white anywhere and is completely silent on a horse that has none. - Panda was backwards and is now the other way up. White is a band round the belly and barrel carrying on over the neck, head and tail; the shoulder, the butt and all four legs keep the coat. The panda face is kept, because it is the gene’s name.
- Pink hair is gone, owner’s call - redundant beside mane colour, tail colour and the hair genes the magical import brought. Every page that used it as a live example now uses one; every page that used it as a piece of history says what the technique was rather than which gene did it first.
Built 2026-09-08: which horse a gene is photographed on, got right on the third try.
Three owner reports - Fielded shown on a plain bay, mushroom wanting a chestnut, KIT’s icon a featureless white horse - are all the same question, and CoatVisibility answers it for both the icon baker and every gene page’s preview window. Two rules changed, and the second one is worth reading before touching it again.
- “The first coat it shows on at all” is not “the coat it is worth photographing on”. Fielded draws tendrils out of the edges of existing white; on a bay there is no white, but the muzzle and pasterns sit low enough in the pigment band to catch a few dozen texels of it - over the threshold, so the bay “worked” and the tobiano was never tried. The rule is relative now: measure every base coat and take the first one that reaches 60% of the loudest. An ordinary gene paints about the same amount on all of them and still answers bay.
- “Not completely white” is a claim about pigment, not about pixels - and measuring the pixels failed twice, in opposite directions. Counting distinct quantised colours let a dominant white straight through: the composite is a multiply against the template’s hair shading, so a flat white horse is a spread of a dozen near-whites and reads as varied. Counting contrast then caught the dominant white and caught a cremello, a chestnut, a black and a grey with it - genes for which a horse of one colour is the honest picture. The version that ships asks phase 1 instead: a dominant white has had its pigment removed, a cremello has had it diluted and still resolves through the gradient. That is a difference in kind rather than a threshold, and it is a number the pipeline already computes.
- KIT then needed a second candidate per allele, because every KIT homozygote is a white horse. Dominant white, camarillo white, near-white or dead - so a homozygotes-only rule can photograph nothing but a blank, while the patterns the locus is famous for are all heterozygous. An allele whose homozygote comes out blank now offers its heterozygote with the wild type instead. The two pools are ranked together rather than in sequence, and that mattered: KIT does have one non-blank homozygote,
W20/W20, which puts white on four pasterns and reads as a plain bay - preferring any homozygote over any heterozygote settled for it over an eighteen-times louder pinto, and the icon went from an all-white horse to an all-bay one before it went right.
Built 2026-09-08: the punnett square asks who the parents are, and three other wiki changes.
- The inheritance grid on every gene page is a real punnett square now. It was allele × allele - every genotype at the locus, which sounds better than it reads: it answers “what genotypes exist” when the question a player has is “I own these two horses, what can they throw”. It also fell apart above three alleles and dropped to a plain list. Now the parents are chosen, one across the top and one down the side, and the square is the four gametes they can pair - four cells for any locus however many alleles it has. Each distinct phenotype gets its own colour and anything that does not present is grey, so “how often does this actually show” is answerable by looking. Both parents start heterozygous, which is the cross worth defaulting to: two homozygotes have nothing to say, and carrier × carrier is the whole reason a punnett square exists.
- A page may now carry a tab of its own, and the LUT lab is the first - it is a tab on the LUT locus’s page rather than a page beside it. The three wiki views stay three: the index switcher offers them, the sidebar filters on them, and a fourth would mean a “Lab” filter that a hundred and fifty pages have nothing for. So a page-local tab gets a button and never becomes the reader’s remembered view.
- The landing page’s sections collapse, and the health genes start shut. Done at run time by
wiki/landing.jswalking a flat.section-head+ siblings shape, because half ofindex.htmlbelongs to:common:bakeGeneWikiPagesand a wrapper in the markup would have to be understood by both halves. Which sections start shut isGeneFamily.collapsed(), true for exactly one family - forty-odd cards with nothing to show, sitting between the two halves people actually came for. - Accretion Field lost its card and kept its page. It is a real locus that paints nothing on its own and exists to be read by Accretion, and two adjacent cards for one marking read as two markings. It is detected rather than listed: a gene with
affectsCoat()false that some other gene names incoatDependsOn()folds into that gene’s entry. A modifier that grows a painter gets its card back at the next bake.
Built 2026-09-08: a “Creating a gene” page, written twice - once for a person and once for a model.
Owner-driven. The page carries two tab panels rather than two pages, so the sidebar has one entry and a reader picks the register they want.
- The Gameplay tab is about the part that is hard, which is not the format. The gene file format already documents every field; what it does not say is that you are not drawing, you are writing a rule that is true at every point on a horse and then finding out what it looks like. So the human tab is a decision table (what KIND of thing is this marking - region, countable marks, wandering lines, something periodic?), the argument for composing before inventing, and the traps in the order they actually catch people.
- The Coding tab ends in a paste-able prompt. Ten numbered rules that are not optional, the body-space extents, the part names, a full template, and the instruction to output JSON only - so an owner with a reference image and two sentences can hand the whole job to a model. Every rule in it is one this session learned the hard way: region-first-shape-last, width-under-spacing, PALETTE has walls, RAMP varies colour and not coverage, reversed smoothstep edges are a hard step, which PIGMENT channel actually discriminates, and the neck’s two tilts with their calibrated constants.
- The vocabulary tables were transcribed, not remembered. They come from
expected.json’sschemablock, which:common:bakeSpecFixtureswrites out ofSpecSchema- so every parameter name and every fallback on the page was correct when written. They are still a copy, which is exactly the failure mode gap 13 is about, so the page says so in a box at the top and names both the maintained prose table and the machine-readable one as the things to believe instead. - The icon baker found a gene that sometimes paints nothing. Sharing the visibility measurement with the preview picker meant re-baking every icon, and auroraband’s came out empty. It is not the tooling: its band is one stroke at a spacing wider than the barrel, so on about one epigenome in six the stroke lands off the mesh entirely and the horse wears nothing. Gap 108; the icon is deleted rather than kept stale, which is the baker working as intended.
Built 2026-09-08: a third pass over the registry, and two more masks caught painting the wrong thing.
Owner-driven, another twenty-odd notes off the gene pages. Most of it is placement and colour and is written up on each gene's page; what is worth keeping is below.
- A
PALETTEhas walls, and Gamma's were cutting across its own fields. Its blooms had softnesses of two and three body units - several texels of fade - and still read as hard-edged shapes, because the edge was not in the mask at all.PALETTEgives each cell of its own lattice one whole colour and neighbouring cells meet at a wall; that is the documented point of the op and the difference between an opal and a gradient. But its lattice has nothing to do with the spot field's, so the walls ran straight through the blooms. ARAMPhas no cells, so the hue sweeps along the body instead and the gene still carries several colours without any of them ending on a line. The same trap is live wherever a soft mask is painted with a hard op - the mask's softness is not the whole story about whether a marking has an edge. - The neck's pitch bites in both directions. Hood is meant to fade back along the neck and was fading from the crest down instead - the colour left the top of the neck before it left the throat. A band on plain
Xis a plane of constant X, and against a long axis sixty degrees off horizontal that plane cuts the neck diagonally. The fix is the same trick as goth's with the sign flipped: goth wants a plane lying along the neck (slope tan 60), hood wants one square across it (slope -tan 30). Two genes, two opposite tilts, one calibration - and gap 103 now covers six genes rather than two. - A
RAMPvaries the colour, not the coverage. Fade is an ombre that should leave the roots alone, and it was painting the whole mane and tail: its only mask wasPARTS: HAIR, so coverage was 1 everywhere and the roots got the ramp's first stop rather than the horse's own hair. A ramp positions colour along an axis and has no opinion about where the layer applies; if a gradient is supposed to fade out, the coverage has to fade with it. - Two more genes were flooding a part because the region was folded in after the shape. Filigree's new head tracery and Lasertae's shoulder both did it: a
PARTSorAXISmaskMAXed on after the shape mask evaluates tomax(shape, 1)on the named part and the shape is simply gone. That is three instances in two days of the same mistake, and it reads perfectly naturally the wrong way round, which is why it keeps happening. Region first, shape last. - Voided stopped being random, and the format did the rest. It was painting a warm grey - hue 25, saturation 0.25 - through a lightness knob, so "black" was whatever the knob rolled. The gene's whole promise is that it changes no pattern and no boundary, only the pigment, so there was nothing about it that should vary. Deleting the knob makes it deterministic by the format's own rule (a gene with no knobs is), which lets the coat cache share one texture across every voided horse with the same markings. A knob removed is sometimes a cache win.
- Circuit's colour was a thirty-degree slice of the wheel. Its
coreHueran 195 to 225, so every circuit horse ever bred was the same cyan give or take a shade. The value was already stored per allele copy - every knob is - so nothing was missing but the range. Opened to the full wheel and renamedhueto match the other forty-odd colour genes. - Gudial is a gradient between two hues rather than two flat ones. It already carried two hue knobs; a
RAMPis written as a start plus a span, and the format has no arithmetic, so the second hue could not stay a hue and be subtracted from the first. It became the span, which is the better knob anyway - it cannot roll the two colours on top of each other the way two independent hues can. - Two genes got a fade onto the tops of the legs and nothing more. Nebula Light and Spectrum Light both ended at the elbow on a line. Each gained a separate layer for the legs rather than an extra mask on the body one - different parts cannot double-paint, and it avoids the fold problem above entirely. Nebula also took a curated pink-violet-blue palette off the owner's own list, with the teal-green entries left out: those were the ones that made a palette wall jump.
- Not fixed: Invert's preview. The owner reports the window does not load on that page. Everything it consumes checks out from here -
genePreviewJsonandpreviewGenotypeCodeboth return well-formed output, the icon is baked, the founder table is sane, the page is registered and carries a family - so it is a browser-side failure and needs a console. Gap 107.
Built 2026-09-08: Pavonem is an actual ocellus, because the owner drew one.
Owner-driven, and the one item on the review that was flagged rather than guessed - the note said “flag that one if you need me to walk you through it” and it was taken up. What came back was an SVG with the proportions in it, which settled in one message what several passes of guessing would not have.
- The middle ring is not painted, and that is the whole thing. The gene had been a pale oval with a bright spot low in it - a spot with a highlight, not an eye. A peacock ocellus is three zones: a dark pupil, a ring around it that is the horse’s own coat left completely alone, and a broad outer leaf around that. Paint the middle and it stops being an eye. Off the reference drawing the radii are 0.20, 0.55 and 1.00 of the leaf, and all three are about 1.2 times taller than wide.
- Two saturations of one hue, not two colours. The reference is navy and green; the instruction was that the centre is the most saturated, the middle is the coat, and the outer is a washed-out version of the centre. So the coloured form runs the pupil at 0.98 saturation and the leaf at 0.42, and the white form - which has no saturation to vary - runs the pupil to white at 96 and the leaf at 52, which is the same relationship expressed in the only axis it has.
- Concentric by construction. All three zones are
SPOTSmasks sharing one seed, spacing, stretch andvary, so they land on the same lattice cell and draw the same per-element size roll -varymultiplies every radius by the same per-cell factor, so an eye that rolls large rolls large in all three zones and the ratios hold. That is why the radii are literals rather than knobs: three independently drawn knobs would break the proportions the reference is made of. The drawing sets the eye slightly low in the leaf rather than dead centre; at the size these render that offset is under one texel, so it was dropped and said so.
Built 2026-09-08: a gene page's preview opens on a coat the gene can actually be seen on.
Owner-driven, one request - “set the default preview model for fielded to tobiano bay”. Written up in CoatVisibility and in gene-preview.js's header.
- It could not be done by editing the page, because the page is generated. A hand-written
data-baseongene-fielded.htmlwould survive exactly until the nextbakeGeneWikiPages. So the choice had to be made by the tool - and once the tool is making it, making it per gene by name would be a list to keep, which is the thing this repo keeps learning not to build. - So it is measured.
GeneWikiToolnow bakes every gene against every base coat and writesdata-basewhen the first coat the gene can be seen on is not the bay. Fielded was the request; Voided and Opalized came out of it for free, because all three modify white somebody else put there and none of them paints a thing on a solid horse. No list anywhere says so, and a gene that starts or stops painting changes its own answer at the next bake. - The measurement was already written, twice over nearly.
GeneIconToolhas chosen an icon's backdrop this way since the bulk import - bake the plain coat, bake the gene over it, count the texels that moved by more than jitter. Rather than write that a second time it moved toCoatVisibility, which both bakers now call. Two tools answering “can this gene be seen here?” differently is a small bug that would have been very hard to notice. - The window ignores an attribute it does not recognise.
data-baseis generated, but the page carrying it can be older thanBaseCoats.all(); an unknown key falls back to bay rather than being trusted into a blank selector.
Built 2026-09-08: the owner reviewed the whole registry gene by gene, and the review found more than the fixes did.
Owner-driven, roughly thirty notes taken while reading the gene pages, arriving as a running commentary rather than a list. Written up on each gene's page; the two engine changes are on the gene format.
- Five genes were drawing nothing at all, and the owner found three of them by opening the pages. “Colored crescents isn't loading on the bay example”, then Lasertae, then Stellar. It is one bug:
perDoseindexes on copies of the gene's first-declared allele, and a coloured expression is reached by two copies of the second - so its dose is always zero, it readsperDose[0], and every one of these files had written that as0.0on the entirely reasonable ground that a dose of nothing should draw nothing. Achanceof zero means the mask never fires. Crescents, Lasertae, Polymoon, Stellar and Wormholes all shipped with an invisible coloured form. Grepping for the shape found the other two before they were reported. Gap 105 is the real fix: the parser can see this at load. - A Y band on the adult neck is a collar, and the first attempt to fix that got the angle wrong. Goth's hood was on the throat; the tilted-plane trick was built for it, and the owner came back with “it's still coloring the underside of the neck”. They were right. The slope had been set to tan(30) from the neck's pitch, and what matters is the neck's own long axis: its box is 12 by 7 and its bounding box comes out 12.07 by 13.89, which only works if the long axis is sixty degrees off horizontal. tan(60), not tan(30) - the complement, which tilts the plane the other way and runs the band across the neck rather than along it. Probing with four coloured bands settled it in one bake, and is what should have been done the first time rather than the second.
- Integration was reading a channel that saturates. It is supposed to spread outward from wherever the coat is already dark - “like the black socks on a bay” - and it was covering the entire horse.
darknessis0.55*red + 0.95*black, so a bay's barrel already reads about 0.84 against its points' 0.95: at any threshold high enough to mean “the points” it had already selected everything. The channel that tells a sock from a barrel isblack, where the body sits at 0.2 and the points at 0.85. Found by painting four threshold bands on a horse in primary colours, which took a minute and settled what half an hour of reasoning had got wrong twice. spreadchanged meaning twice in one session, and ended up simpler than it started. It began as “grow the pale side of the reading”, which is what Fielded needed. Integration needed the opposite. Rather than a second parameter, it moved to afterinvertand now dilates the mask's output - grow whatever this mask selected - which serves both, and is provably the same operation for Fielded. Then Integration proved the disc was in the wrong space: dilating three units off a bay's socks covered the horse, because the leg patches sit next to the barrel on the texture sheet. It measures in body space now (gap 104, opened and closed the same day), and the fix was cheap becauseHorseSkinGeometry.sampleis a cached grid rather than a computation.- A mask list is a fold, and putting the shape first throws it away. Lasertae's eclipses were meant to be confined to the shoulder, and the region terms were
MAXed on after theRINGSmask - so a neck texel evaluatedmax(0, neckBand), the band on its own with no ring in it, and the shoulder came out as a wash. The rule is: fold the region into one number first, then multiply the shape in. Worth remembering because it reads perfectly naturally the wrong way round. - An eclipse is not an annulus, and
RINGScould never draw one. Lasertae took three passes. A ring has two edges by construction, and the whole point of an eclipse is that its outer one is not there - the corona has to run out into the coat over two or three texels rather than stop. What draws it is two concentricSPOTSon the same seed and spacing: a big disc with a softness of more than a body unit, times a small one inverted and sharp. The big disc's soft edge is the corona fading to alpha zero; the small one's hard edge is the circle of bare coat it closes around. Neither is a shape the mask vocabulary has a name for, and both fall out of composing two it does. widthhas to stay underspacing. Zebra's Coat had stripes up to 10 body units wide at a 3-unit pitch - they overlapped four deep, and the “narrow transverse striping” its own description promised rendered as a solid white neck. Six genes were within a whisker of the same thing; the others are broad by design, and only this one was reported.- Two markings lost a stamped-on spiral. The
SPIRALmask draws one figure per part, centred on the part's bounding box, and it reads as a decal applied to the horse rather than as something grown on it. Circuit and Filigree both dropped theirs at the owner's request; nothing else uses it, and the mask is now unexercised outside the parity gene. - What was flagged rather than guessed. Pavonem does not read as a peacock eye and the owner offered to walk through what one should look like - so it is untouched, which is the right answer to an offer like that. Flametouched had its look reverted: the earlier session had changed the gene when the request was only about which of its two forms the wiki icon shows, and the icon baker always picks the loudest homozygote with no way for a gene to nominate a different combination.
Built 2026-09-08: twenty-four gene fixes, four new painters, one polygenic locus — and a mask bug that had been eating half of fifteen genes.
Owner-driven, a list of twenty-four notes taken from looking at the horses. Written up on each gene's own page; the vocabulary additions are on the gene format and the cross-locus block on the genetics model.
- The bug was found by not believing the file. Panda is supposed to keep the coat on the shoulder and the hindquarter, its own file said so in two masks unioned with
MAX, and the horse had one of them. Rendering the hindquarter mask on its own drew it correctly, which is the fact that made it a code bug rather than a gene bug.SpecPainter.coveragebailed out the moment aMULTIPLYmask took the accumulator to zero — a sound optimisation for a chain of multiplications and wrong the instant anything later can put coverage back. So the second half of every union was unreachable wherever the first half was empty, which is to say everywhere it mattered. Fifteen gene files use that shape. The test is now “can anything from here on raise it”, and the JavaScript twin had the identical line, which is whycheck-parity.mjswas green about it for its whole life: two engines agreeing is not the same as two engines being right. - Four things went into the mask vocabulary, and three of them exist because noise cannot repeat. Every shape in this format that curves, curves because a noise field bent it, and noise never comes back to the same place twice. That is right for a marking and wrong for a lobe, a string of lights and a row of teeth, all three of which are periodic.
WAVESis a band whose edge is displaced by a sine, a triangle or a sawtooth: Waves gets the three-to-six even scallops its description had always promised and never drawn, Angler's lights sit on a curve you can follow with a finger instead of on a forking ridge, and Cleave gets straight-sided spears — the triangle and sawtooth are the only way to ask this vocabulary for an edge made of straight lines, because everything noise-derived rounds off however far the softness is wound down. CRACKLEis the giraffe, and it is a different measurement rather than a different tuning.DAPPLESandSPOTSboth draw round marks however hard they are pushed, because both measure the distance to a cell's centre. A giraffe is polygons: straight sides, corners meeting three at a time, an even channel between them. That is the distance to the wall between two cells, which needs the two nearest lattice points and not just the one —BodyNoise.cellEdge, half the difference of their distances. Qular was fuzzy blobs and is now a giraffe read backwards.- The other two additions are parameters rather than masks.
SPOTStakes"shape": "heart"— the classic polar heart, evaluated in the(x, y)plane so it stands upright on the flank — which is what let Valentine drop its pink half entirely (the owner is making that a LUT) and become an appaloosa whose spots are hearts and whose blanket has hearts cut out of it. AndPIGMENTtakesspread, which grows the pale side of the reading by a radius, so a gene can finally ask for “beside the white this horse already has”. Fielded's whole description turned on that and it had been drawing wisps on solid horses; it now draws nothing whatever on a horse with no white. - Accretion is two loci, and the format grew a
needsblock to say so. The owner asked for it polygenic: one locus for colour, one for which half of the horse. Nothing in the file format could express a gene reading another gene — butGene.expressionInandGene.coatDependsOnhave existed since the leopard complex started reading PATN1 and PATN2, so the addition is a way to reach an existing hook from a file rather than a new mechanism. An expression may carry"needs": {"<gene key>": {"<token>": copies}}; entries carrying one are exempt from the exactly-once coverage check, because they override a combination rather than claiming it, and the plain entry underneath is still what a horse gets when the condition fails. So the table stays total whatever the second locus says. Accretion Field paints nothing, is carried by every horse alive, and is why two underside-marked parents can throw a topline foal. - Hued pangare stopped being a gene file, and that was the point of the request. The owner asked for it to use “the exact same drawing process” as pangare. It had been a Y band up the belly and another down the neck, tuned by eye — and pangare is not a band, it is a region map with nine named weights on it and a mottle over the top. Two hand-tuned copies of that would have drifted the first time either was touched, and drifted invisibly, because both horses would still have looked like mealy horses. So the field came out of
PangareGene's painter asmealyCoverageand the new class calls it: the same code, differing only in that one takes red out in phase 1 and the other walks the colour toward a hue in phase 3. Ordinary pangare paints exactly what it painted before. - Patina was drawing on the wrong half of the horse and the reason was an argument order. Its
PIGMENTmask readfrom: 0.75, to: 0.35— andsmoothstepwith its second edge below its first is not a reversed ramp, it is a hard step in the original direction. So “verdigris where the coat lightens” was landing on everything dark. It follows pangare's regions now (the owner asked for that shape) with a heavy speckle through every edge, and reads on a chestnut the way copper reads: grains rather than a wash, and nearly invisible on a black, which is what its own description had always said. - A Y band on the neck is a collar, not a crest. The adult neck is pitched thirty degrees, so goth's hood — a band in the upper part of the neck's box — wrapped the throat as readily as the top. Probing it with four coloured bands showed that immediately and is the only reason it was cheap to fix. The hood is a tilted plane now, got by giving
WAVESa sawtooth whose wavelength is longer than the horse: the displacement then never wraps, so it is a straight ramp, and a band on a ramped coordinate is a plane at an angle. Cosmic uses the same trick to hang its blanket off the crest the whole length of the neck. It is a trick and it is gap 103: the pitch is written out by hand in two gene files. - Quarter needed a choice, and the format has no way to make one. It is supposed to cut the trunk into quadrants and pale one of them; it was drawing a soft wash indistinguishable from nimbus. A knob can move a boundary but cannot pick between two of them, and the mask fold has no branch. What it does instead is sample value noise at a scale of 500 body units — the whole horse inside one lattice cell, so one number per horse — and amplify it until the clamp turns it into a coin. Two coins give four quadrants, one layer each, exactly one of them live. It works and it is honest about being a workaround (gap 102); what it really wants is a per-horse integer knob.
- Every gene preview now opens on six base coats rather than three. Bay, black and chestnut are the three pigments every marking is read against, and they are no background at all for a gene that modifies somebody else's white: fielded now draws nothing on any of them, and a preview of it on three solid horses is three pictures of a plain bay. So
BaseCoatsgained a tobiano, a splash and a sabino bay — heterozygous, and picked to cover the three shapes white comes in. - What was not built. Nothing on the list was skipped, but one item was interpreted: “change flametouched to have the preview be of the single-color flametouched” describes an outcome that did not exist, so the gradient form was made single-coloured — one hue whose strength ramps fore-and-aft, rather than a hue sweeping through two or three — which makes the form the preview opens on the single-coloured one. The flame form beside it keeps its sweep. If that reading is wrong the fix is small and the item is flagged on the checklist.
Built 2026-09-08: the seven magical family index pages are deleted — the landing page was already the index.
Owner-driven, one request. Written up in GeneWikiTool; the grouping itself is unchanged and still common/genetics/GeneFamily.
- Two indexes over one list is one too many. Each
genes-magic-*.htmllisted one family's genes with an icon, a blurb and a link to the full page — which is what the landing page's section for that family already said, in the same order, from the same table. The pages were generated an hour after the landing-page cards were, by the same tool, from the same source, and nobody noticed they had become a duplicate of each other. The owner did. - What went with them. The “All ground and strong white” item that led each of the seven sidebar sections, and the family card that led each section of landing-page cards.
GeneFamilylost itsslugandmagicalSpecFamilies(); nothing else asked for either, which is the tell that they existed only to name a file. The.gene-cardblock instyles.csshad no other reader either. - A gene page's eyebrow still names its family, as text rather than a link. The family is a heading now — a sidebar section and a landing-page section — and not a destination, so there is nothing for it to point at. Keeping the words is worth it: the eyebrow is where a reader learns that Dust is a speckle gene rather than a spot gene, and that is the one fact the page's own title cannot carry.
- The hand-written family blurbs went with the cards and are in the history rather than on the page: each opened by counting its family (“Sixteen genes that replace the base coat…”), which was a derived number sitting in prose and already drifting. What is left is
GeneFamily.lede(), which every section heading has always used and which counts nothing. harvestIconswent too, and it is the only removal that lost a capability. It existed because a family card's icon was a hand-picked representative — nothing in the registry would have chosenpanda.pngto stand for ground-and-strong-white — so a card that carried an icon the tool would not have picked kept the one it had. A gene's own icon has always been derived from its name, so with the family cards gone there was nothing left for it to preserve.
Built 2026-09-08: the gene icons were being drawn through a sheared camera, and every gene has one now — or honestly none.
Owner-driven, five requests in one, and one of them had to be answered twice. Written up in GeneIconTool and HorseSkinGeometry.posed; the card markup is GeneWikiTool.card.
- Two separate bugs were making the icons wrong, and fixing the first one was not enough. The owner said the baked horses looked wrong; the mesh was fixed; the owner said they still looked wrong. That second report was correct and the useful part of the session. The mesh was the obvious half: the baker placed every texel on the part's rest-pose bounding box, which is all the coat pipeline ever needs, and the adult neck's AABB is nearly twice the neck — so the horse was a pile of oversized blocks with a rake of stripes for a tail.
model3d.jshad solved that for the browser preview years of sessions ago and says so in its own header; that arithmetic is nowHorseSkinGeometry.posed()/posedNormal()on the Java side, and the two files name each other as twins. - The second bug was the camera, and it was a sign.
sideViewbuilt its basis with cross products and wroteup' = v × rightwith two of its three terms negated, so “up” was about 45 degrees off perpendicular to the view direction —dot(up, view)came out at essentially 1 rather than 0. An orthographic projection down a non-orthogonal basis is a shear along the view axis: every part slid up or down the image by its own depth, which put the head in the sky, the tail on the floor, and the legs nowhere near the barrel they hang off. The basis now comes straight out of an azimuth and an elevation, so all three axes are orthonormal by construction rather than by a cross product somebody has to check, and the camera can be read back as a viewpoint instead of three hand-tuned components. - The lesson is that the picture was not enough. The sheared horse read enough like a horse to survive being looked at twice, once by the owner and once by me immediately after fixing the mesh underneath it. Gap 13's cousin: looking at the output is necessary and is not sufficient when “it looks like a horse” is the whole test. Two cheap checks would have caught it in seconds — the three dot products of the basis, and counting enclosed transparent pixels, which went from 568 on a plain bay to zero once the projection stopped shearing the texel grid apart.
- An icon for every gene, and the allele it advertises is measured rather than assumed. The baker walked the data-driven genes only, so 88 of 155 had a picture; it walks the registry now. Which allele to show could not be found by equality: swapping an allele copy swaps the epigenome slot the coat reads with it, so
A/Ais never byte-identical to theA/aunderneath it, and a plain difference test was about to illustrate Agouti with a bay. It counts the texels that moved by a channel step or more and takes the loudest allele, which gives Extension its chestnut and Agouti its black. The fallback backdrop became a ladder — tobiano bay, chestnut, leopard bay — and that is what gets Flaxen, PATN1 and PATN2 a picture of themselves rather than of the horse under them. - A gene that changes no pixel gets no icon at all (owner's call, once the rest was working). Thirty-six of them — the health loci, the four magical stat genes, and the ones like cutie mark and particle that do their work in a client render layer rather than in the sheet — were baking as the plain bay. That is honest and useless: a card for a jump gene showed a horse that has nothing to do with it, and thirty-six identical bays down a page read as a bug. The baker deletes a stale file rather than skipping it, so a gene that loses its paint loses its picture on the next bake; the landing card, the family index and the harvested-icon fallback all drop to words, and those sections keep the tighter three-across grid because only a grid holding an icon widens.
- A card is a picture beside a paragraph now, not a paragraph under a picture. Two columns, the horse on the left and every word on the right. The icon also stopped riding inside the harvested card body: it is now harvested separately by href, so a change to the card markup can reach a card somebody has edited by hand — which is the whole point of harvesting, and was quietly not true.
- The landing page reads in the order a reader wants it. Start here, Gameplay, Items, then the coat engine, For modders and Project, with the hundred-odd gene cards last;
wiki/pages.jsmirrors it, so the sidebar and the landing page tell one story. “For modders” was also appearing on the Gameplay view, which is not what that heading is for: the Horse designer was the single card under it flaggedgameplay, and a section shows when any one of its cards does. That card moved to Gameplay, where a player would look for it anyway.
Built 2026-09-08: the wiki groups genes the way the editors do, every gene page is listed in both places, and neither list is written by hand.
Owner-driven, four requests in one. Written up on the spawn eggs and in GeneWikiTool; the grouping itself is common/genetics/GeneFamily.
- One taxonomy, three readers. The sidebar had five hand-written gene sections, the landing page had a different arrangement again, and the gene editors' new family filter would have made three. They are all
GeneFamilynow - natural coat, dilution, white, health and other, then the magic - in the same order under the same labels. The magical families were already derived from the paint-priority bands andGeneWikiToolwas already using that table to write the seven family pages, so this was less a new idea than finishing an existing one. - A gene page is registered by existing.
:common:bakeGeneWikiPagesnow writes the whole gene region ofwiki/pages.jsand a matching span ofindex.html, walking the registry rather than a list. All 162 gene pages appear in the sidebar and as a card on the landing page; before this, 84 of them were reachable only through a family index and none had a card. PATN1 and PATN2 have no page at all - they are documented inside the leopard complex - and the tool names them on stdout rather than inventing two. - The label and the views come off the page, not out of a table. A gene's sidebar text is its own
<h1>and its views are thedata-tabpanels it actually carries - the same rulesync-page-views.mjsapplies to everything else, applied while the file is already open. So the sidebar cannot offer a science tab that is not there, and cannot call a gene something its own heading does not. The cost is that three curated sidebar labels went back to the page's heading (“KIT (white spotting)” rather than “KIT (dominant white / sabino)”); the fix, if it matters, is to change the heading. - A hand-written landing card always wins. The tool harvests each card's body by href and puts it back verbatim, rebuilding only the anchor - so the sixty-three cards somebody wrote survived, a hundred and one were generated from the genes' own blurbs, and editing one by hand keeps working. Rebuilding the anchor rather than keeping it is what makes a card that moves between families pick up the right colour and icon.
- Bay is the default base coat everywhere now, not black.
BaseCoats.all()is ordered bay, black, chestnut, so everything that takes the first entry gets a bay - the LUT lab included. The old order was darkest-first, on the argument that a white marking goes from most obvious to least that way; true of white markings, and wrong for the rest of the registry, because a black horse hides every dark marking on it and a great many of these genes paint dark. The gene-preview window had already been overriding it by name for exactly that reason, and the gene icons were already baked on a bay - this makes the three agree. - The per-outcome prose moved to the coding tab. The eighty-four imported genes carry a description of each outcome that came out of the source documents part by part - “from the poll down through the orbits and the nasal region to the muzzle, bounded along the cheekbone and the jaw”. That is a specification of how the thing is drawn, and it was sitting on the gameplay tab, which CLAUDE.md says is for a player who does not want the model. It is now on the coding tab immediately above the layer list that draws it, where the prose and the mechanism can be read against each other. Gameplay keeps the picture, the outcome names and what it takes to carry them, the odds and the carrot.
- The designer's toasts are gone entirely (owner's call, later the same day). They were a stack of dismissible cards over the field on a ten-second timer, and the timer did not reliably fire - a pointer resting anywhere over the stack held every card in it - so a run of Randomize clicks buried the horse behind the thing you were trying to look at. Nothing was lost with them: the conditions and the struck-through gene rows were already on the panel permanently, so those notes went to the console, and the two cases where nothing on screen would otherwise change kept a real channel - a failure to boot holds the boot overlay up and says why, and an unreadable horse file raises a dialog. A notification that can outlive its own dismissal is worse than no notification.
- Two things about the build, found while closing the session out.
:common:testis green at 674 tests in 9m42s - the twenty-five and thirty-four minute runs earlier in the day were two suites contending for the same results directory, not the suite getting slower, so gap 97's figure still stands. And TeaVM's wasm output is not byte-reproducible: two consecutive builds of identical source give different bytes at the same length. That matters because the regeneration rule reads both ways at a glance and only holds one way - a stale wasm beside a changedcommon/is a real problem, a wasm diff on its own is just a re-bake and not worth committing. Written up on architecture. - And the landing page's four headline numbers are generated at last - gap 93, closed. No new Gradle task was needed: the tool was already rewriting
index.html, so the stats block went in behind the same markers. It corrected 69 genes to 155 on the way in, and the distinct-coat figure now prints as a floor rather than a number, because it passedLong.MAX_VALUEsomewhere in the eighty-four magical genes and a saturated counter quoting its own ceiling is a lie.
Built 2026-09-08, NOT yet looked at in game: the gene editor grows a randomize menu, per-gene locks and a family filter — on both screens.
Owner-driven, one request. The custom horse spawn egg and the browser designer are one screen in two places (hard rule 5), so all of it landed twice — and the parts that are genuinely the same rule rather than two parallel ones stopped being mirrored and moved into common/. Written up on the spawn eggs; checklist there.
- Randomize is a split button with eleven modes.
common/genetics/RandomizeMode— the plain founder roll, a uniform true random, one for the naturals and one for the magic, one for the dilutions and one for the whites, a random breed, +1/+2/+3 magical, and the epigenome on its own. The face does the thing, the arrow picks which thing, and the choice sticks. Picking from the menu also runs it: having said what Randomize means, you wanted it done. - The narrow modes are the point, and they are what made the shared enum necessary. Most of the time you are not asking for a different horse, you are asking for a different something on the horse you already have — so a scoped roll leaves everything outside its scope untouched, epigenetics included. That is one rule with four moving parts (scope, locks, the visible-gene switch, and putting the untouched loci's epigenetics back into a freshly rolled epigenome), and keeping two hand-written copies of it in step was not going to survive the week.
EditorRulesandRandomizeModeincommon/now hold it;variantPairmoved there too, having been written out twice verbatim since the designer existed. - The sex is rolled first, before a single allele. Not cosmetic: a stallion carries one copy of an X-linked gene and a mare two, so rolling the genes and then the sex means immediately snapping half of what was rolled back to something else. Only the whole-genome modes do it — re-rolling the dilutions is an edit to the horse in front of you, and changing its sex underneath that is not what was asked for.
- A padlock per gene. Locked means every randomize, main and epigenetic, leaves that gene exactly as it stands: alleles, whether it is on the horse, and its epigenetics. The last of those is the half most likely to break, because the epigenome is rebuilt whole and the locked loci have to be put back into it —
Epigenome.withthrows on a gene that stores nothing, hence the newEpigenome.carries. Drawn as eight rectangles rather than typed: the font has no lock glyph in the plane it can reach, and the mark has to be the same on both screens. - “Rnd health”, off by default, and the line it draws is not
affectsCoat. The switch is for genes that change nothing you can see, which is not the same set:PATN1,PATN2and shade all paint nothing themselves and every one of their expressions is a wild type, yet a horse carrying them looks different, because a painter names them incoatDependsOn.Genes.influencesCoatreads that declaration rather than keeping a list of exceptions beside it. Off, because the reason to press Randomize on a gene editor is almost always to look at a coat, and rolling a lethal into the horse you are looking at is a surprise nobody asked for. - The gene list gained a family filter, and the families are derived.
common/genetics/GeneFamily. Magical genes band on their paint priority — which costs nothing, because the magical bands were laid out as families in the first place, andGeneWikiToolhad already been using exactly that table to write the seven family pages. It reads it from here now, so the menu and the wiki cannot drift. The naturals: anything invisible is a health gene, the rest band on priority, and a handful whose slot in the paint order fights their family are named in one short override table with the reason. Shade is the honest example — parked at 95 among the disorders because it paints nothing itself, and nonetheless one of the three genes that decide what colour a horse is. - Extension, agouti and shade are always on the horse and carry no
x. Every horse has alleles at all three; a list that hides them until you click implies a horse can be without them. - One way to copy a horse, not two, and it is the one that carries everything. The screen had Copy code / Paste code and the designer had those plus Export / Import. A genotype code is the alleles and nothing else — paste one back and the horse returns with fresh epigenetics, no name and no breed, which on a mod whose whole premise is that one genotype makes many horses is a lossy copy pretending to be an exact one. Both now speak
common/horse/HorseFile: the designer writes it to a file, the spawn egg to the clipboard, and a horse crosses between them intact. That closes roadmap §9, which had asked for an Import horse file button beside Paste code; it shipped as a replacement instead, because nobody reads two copy buttons and works out which is the lossy one. - What was actually verified. The wasm was loaded in Node and driven through every export the page uses — all eleven modes, the locks, the filter menu, the file round trip — and a separate harness ran the editor model directly: a locked KIT survives all eleven modes with its epigenetics intact, Rnd white moves nothing outside its family, the +N modes always reach their floor, and two hundred true random rolls never produced a pair a gene forbids. None of it has been looked at on screen, in the browser or in game, which is where the layout risk is: two new buttons in the right column and a padlock column that narrows every gene name.
Built 2026-09-08, NOT yet looked at in game: eighty-four magical genes, imported in bulk out of process/.
Owner-driven, one overnight request. Every gene is a JSON file — none of them is a Java class. Each has its own generated page, gathered at the time by seven family indexes — ground and strong white, fields and regions, spots and rings, speckle and dust, lines and strokes, mane and tail, colour modifiers. (Those index pages were later deleted as redundant to the landing page.) Checklist §0-AN. Gaps 94–97.
- Every gene has its own page; the family pages are an index over them. The owner's correction, and the right call: a gene page is where acquisition and gameplay information lives — the preview, the inheritance table, how often a wild horse is born carrying it, and the gene carrot. Eighty-four generated pages plus seven indexes, and the wiki went from 116 pages to 200. The two genes that already had hand-written pages keep them: the tool treats a page without its marker comment as off limits, and drops it from the generated part of
pages.jsso it is not listed twice. That generated span is how eighty-four pages get registered without hand-editing hard rule 7's one page list. - The designer was showing none of them, and the cause was ordering. Reported by the owner.
HorseEditorsnapshotsGenes.codeOrder()in its constructor - it has to, the page addresses gene rows by index - andDesignerApi.main()built one immediately, which the page calls before it hands the gene bundle in. So all eighty-four registered successfully, reported no problems, and none appeared.mainno longer builds anything; the editor is built lazily, first atsetNameWords, which runs after both register calls, and both register calls drop the cached editor as well.java.jsnow compares the bundle's length against what the page is showing and says so in the console if they ever disagree again — the bug's whole problem was that it was silent. - The format grew before any gene was written, and that was the whole trick. Reading all seven source files first turned up perhaps a dozen shapes described over and over: countable spots, hollow rings, fine stipple, tapering strokes, a spiral, a colour running along an axis, a colour drawn per cell. Building those as five masks and two ops —
SPOTS,RINGS,SPECKLE,STROKES,SPIRAL,RAMP,PALETTE— is what let eighty-four genes be data instead of eighty-four classes. See the format. - Every colour op can now take a hue instead of a colour. The single most valuable addition, and the least obvious:
coloris a constant, and a constant cannot be the thing a horse drew for itself.hueis an ordinary number parameter, so it can point at a knob — and one knob is the difference between a gene that paints teal spots and a gene that paints spots of whatever colour this line of horses runs to. It is a negative sentinel rather than "is the key present", because the gene creator carries every parameter whether or not you touched it, and a presence test would preview one colour and export another. - Glow is a layer flag, not an op.
"emissive": trueon a layer marks the texels it covers full-bright, orthogonal to what colour the layer paints — the sameTOWARDpaints a lit teal spot with one flag flipped.SpecGeneimplementsCoatOverlayContributionto do it, because the overlay is the passmarkEmissiveexists in. Two combinations are load errors rather than surprises: a natural gene cannot glow, and an emissive layer cannot use aPIGMENTmask, because the pigment field is gone by then. INVERTis the one op that could not be composed. Every other colour op walks the texel toward something; a negative is a function of what is already there. It reads what the texel looks like rather than what it stores, for the same reasonTOWARDdoes — otherwise the negative would differ per base coat, which is the one thing an inversion must not do.- The shipped gene folder was never loaded.
GeneSpecLoader.fromClasspathhad no caller, so Suntouched and Waterborn had been in the jar and dead.Genes' class initialiser registers them now, which is also where the eighty-four land; the NeoForge module's duplicate copy of the folder is gone, and both genes moved out of theexample.namespace, where they collided with the example files a test registers. - Two tools, and they earned their keep in the first hour.
GeneIconToolbakes one three-quarter snapshot per gene on a standard bay (which the wiki index wanted anyway);process/tools/sheet.pystitches them into a contact sheet. Baking the first five and looking at the sheet showed four of them were effectively invisible: the sources quote centimetres for a real horse, and one body unit is eight of them and two texels, so a "3 cm ring wall" is a third of a pixel.process/tools/coverage.mjsis the other half — a gene that paints nothing and a gene that paints where the camera cannot look are the same picture and opposite bugs, which is how Lantern's dorsal line was declared missing when it had been there all along. STROKEShad to be rewritten once. The first version thresholded|n − 0.5|on the noise field, which is not a width: value noise is flat at its extrema and steep between them, so the same parameters drew a hairline over half the horse and a blot over the other half — which is what every stroke gene looked like on its first bake. It divides by the field's own gradient now, making the reading an approximate distance, andwidthis in body units like every other mask's parameter.- A
MAXorADDon a layer's first mask is now a load error. Coverage starts at 1, so either returns 1 whatever the mask found and the layer paints the whole horse — indistinguishable from a mask that is merely too generous, and how Brindlelace came out solid black. - Founder rates are budgeted across the set, not chosen per gene. Twenty genes at "a few percent each" took the share of wild horses showing something magical from a half to 0.86, because the shares compound.
process/tools/founders.pyderives each gene's table from its rarity tier and budgets the whole set; eighty-four of them now reach about one wild horse in fifteen. A magical breed sets its own pools and is unaffected. - Every white-only gene carries a coloured recessive twin, per the owner's rule: white dominant, colour needing two copies, and the coloured allele never giving blue eyes. Thirty-three genes qualified. A gene that paints white alongside something else does not — Corvid's black muzzle, Cosmic's hue ramp, Winged's dark ground — and where that line was drawn is written out in the checklist for the owner to disagree with.
GenotypeCatalog.size()had been claiming to saturate and never did. Every gene multiplies the catalogue and a two-allele dominant one doubles it, so it takes about sixty to pass 263; this import reached it, the plainlongmultiply wrapped to a negative, andgetthen rejected every index against it. It saturates properly now, and the plain product is capped to leave room for the masked entries so those stay reachable.- Four tests turned out to be asserting luck rather than behaviour, and were only ever green because the model was small.
clearLoaded()dropped the jar's own genes and never put them back, so any test after one that cleared saw a shorter genotype code — it surfaced as an unrelated gene inheriting the wrong particle trail.ParticleGeneTestrequired every foal to inherit its parent's body site, whenEpiDriftdeliberately re-rolls a category outright once in a thousand breedings, precisely so that a change is a real event; the test held only because the RNG stream had not reached one. And the pen-sign label cap is a per-gene budget now rather than 200 characters, since the label length tracks the registry size and not the wrap logic it is there to guard. - What was refused. Nothing changes after the coat is baked, so Patina's oxidation over years, Opossum's progression with age, Goth's greying over successive sheds and Stardust's spread all became a fixed extent drawn once per horse. Laciano's homozygous blindness and deafness were not built: these genes paint and glow and do nothing else. Paint Moon was refused as a duplicate — it is real-world pinto and roan genetics, all seven of which are already first-class natural genes here, and building it would give every horse two independent tobianos that know nothing about each other.
2026-09-07
Built 2026-09-07, NOT yet looked at: the wiki became three views — Gameplay, Coding, Science — and got a real search.
Owner-driven, one request with four things in it. Design: every gene page, every item page, the landing page. Checklist §0-AM. Gaps 88–91.
- The wiki was serving three readers one index, and serving none of them. A player who wants to know what a flaxen looks like, someone adding a gene, and someone checking the model against real horse genetics want almost disjoint subsets of the same page. The split is
wiki/tabs.jsreading<section class="tab-panel" data-tab>; a page with no panels is left completely alone, which is why the seventeen pages that are one subject for one audience needed no edit at all. The chosen view rides in?view=and in sessionStorage and is pushed through every link the sidebar and the index build, so choosing Science and clicking Flaxen opens Flaxen's science tab. - Where a section went was decided by rule, and the rules came from the owner. Design rationale - the "why one locus and not forty genes" sections - goes to Science, not Coding, because those arguments are almost always framed against real genetics. Look-alikes and breed lists go to Gameplay, because telling two horses apart and knowing where to find one are things a player does. Everything unrecognised defaults to Coding, which is the safe direction: a coding tab carrying a section it did not need is a nuisance, and a gameplay tab carrying one is the thing the split exists to prevent.
- Nothing was rewritten out of existence, and that was checked rather than assumed. The split moved whole top-level blocks and was verified word-for-word against
HEADon all 69 registered gene pages: zero words lost. Worth having done, because the first run of the transform re-processed pages it had already split and double-wrapped 67 of them - the pages were reverted and it was re-run once, and the script now refuses a page that already has panels. - The inheritance table is asked of the mod, not written down. A new
@JSExport-DesignerApi.geneInheritanceJson- reports every genotype at a locus and the phenotype each one gives, andwiki/gene-inheritance/draws it. That is hard rule 3 applied to a table rather than to a painter: forty gene pages carry a hand-written combination table, and a live one cannot fall out of step when a gene gains an allele. Above three alleles a square stops being readable, so it falls back to a plain list of outcomes - the owner's call, and the reason the gameplay tab of KIT is a list and the gameplay tab of flaxen is a grid. items.htmlwas one page describing thirteen unrelated things. It is now a roster of links, and each item or group has a page: hair, the breeding carrots, the splice carrots, research papers, seed jars, whistles, stall signs, tickets, transfer papers, the Horseman's Table and the spawn eggs. Their gameplay tabs are written fresh in plain language; the implementation prose moved across intact rather than being reworded, because it was accurate and rewording accurate prose is how errors get introduced.- The sidebar box was never a search. It filtered page names, so anything you could not already name was unfindable - which on a wiki with seventy gene pages is most of it.
wiki/search.jssearches the text of every page, and a hit knows which tab it is on and links to it. The corpus is baked to a script rather than fetched as JSON on purpose: the wiki is read from afile://path as often as from a server, andfetchdoes not work there. It is loaded lazily on the first keystroke, because it is 1.8 MB and almost every visit never searches. - The page list moved out of
nav.jsintowiki/pages.js. The landing page needs the same list to know which cards to hide in which view, and a second copy would have drifted the first time a page was added. Hard rule 7 now namespages.js. Which views a tabbed page appears in is not an editorial call at all - it is exactly the tabs it has - sosync-page-views.mjsreads them out of the HTML and writes them back, and only an untabbed page keeps a hand-set value. - 24 natural gene pages came out with no science tab, and were left that way. Their real-world material is in the lede rather than in a section a split could pick up. Writing a stub would have meant either inventing veterinary claims or shipping 24 pages whose science tab says "not written yet", and an honest absence beats both. They are listed in verification as the next thing to write. The magical genes having no science tab is correct, not a gap - they are invented.
Audited afterwards, same day. Two of the gaps this session opened were checkable in the repo without a browser, so they were.
- Every recipe on the new item pages was checked against the JSON, and the pages were right. All 29 recipe files against all eleven pages: the hair chain and its four reversals, the three whistle tiers, the four tickets, the shaped transfer paper, both jars and all ten carrots including the five themed reagents. What the audit found was absence, not error - the stall sign's own recipe (oak sign + horse hair) had never been written down anywhere, on the old page or the new, so nothing told a player how to get one; and the Cowboy Hitch is a registered item that appeared on no roster at all. Both fixed.
- And it found a real bug.
cowboy_hitch.jsonandhorsemans_table.jsonare byte-identical shapeless recipes with different results, so only one of them can ever match and one of the two blocks is uncraftable in survival - which matters because those blocks are how both merchant NPCs come to exist. Villagers states the shared recipe as though it were fine, so it had been documented rather than noticed. Gap 92, deliberately left unfixed: choosing a reagent to tell a hitch from a table is a gameplay decision and not a documentation pass's to make. - The predicted routing failure was real, and on the pages that could least afford it. Measuring prose per gameplay tab found 26 pages whose gameplay tab was seven words - two headings and the widgets - because they carry neither a look-alike table nor a breed list, which were the only two things the routing rule could move onto Gameplay. Agouti, tobiano, roan, MATP, shade, sooty, leopard and natural zebra were all in that set. Each now opens with a short plain-language "What it does" written from what the page already asserts; thinnest tab is 44 words, median 95. The other direction - whether anything reached Gameplay that should not have, and whether the 307 sections that defaulted to Coding belong there - is still unread. Gap 88.
- And the landing page had been quietly lying about the size of the mod. Checked at the end of the session because CLAUDE.md names derived numbers as a repeat offender: three of the hero's four headline stats were stale. 48 genes against a real 69; 4.3×1031 genotypes against 6.7×1048; and 11,098,128,386 visually distinct coats against 3,121,348,608,000,004 - six orders of magnitude out. Only the breed count was right. Verified against the built jar rather than recounted by hand, corrected, and the accessor names left in a comment beside them. This is gap 13 recurring on the most-read page in the repo, so the actual fix - generating the four numbers from a Gradle task - went on the roadmap rather than being trusted to somebody remembering (and shipped the next day). Gap 93.
Built 2026-09-07, NOT yet looked at: breeds became JSON files, and got spawn eggs, a designer, and a splice mark.
Owner-driven, one request with four things in it. Design: the breed file format, breeds, items, carrots. Checklist §0-AL. Gaps 83–85.
- The 49 breeds were not translated by hand, and that was the whole plan. An 885-line
Breeds.javaretyped as 49 JSON files is 49 chances to mistype an allele weight and no way to notice. So the writer came first:BreedSpecWriterturns aBreedback into the file that would produce it,BreedFileToolran it over the Java definitions once, and only then were the definitions deleted.BreedFilesTestnow asserts every shipped file is exactly what a fresh bake would write, so the two halves cannot drift apart afterwards either. Map.copyOfwas quietly making the output non-deterministic. Found on the first bake:Breed's compact constructor usedMap.copyOfandSet.copyOf, which are deliberately unordered, so the gene pools came out in a different order from the one they were declared in - and would have come out differently again on a JVM with a different hash seed. Harmless while a breed only fed a founder roll; a checked-in generated artefact whose diff depends on the hash seed is exactly the trap CLAUDE.md's regenerate table exists for. Ordered copies now, with the reason on the line.- A breed file's two kinds of wrongness are treated differently, and that is the format's main design decision. A gene file is parsed strictly because the creator writes it. A breed file is mostly references to other people's genes, so a malformed file is thrown and a file that names something this install has not got is warned and skipped, one locus at a time. A Friesian written for a bigger modpack has to degrade to the horses this install can actually make, not vanish.
- "Allele band restrictions for every gene" turned out to be the interesting half. The owner meant the epigenetic numbers, not the allele pools - so
BreedBandsgeneralises what the stat targets already did for the four body axes to every gene and every declared value. A breed can now say "deeply black" rather than only "black", written onto both allele copies of every founder and then let go of, exactly like a stat band. It refuses three things loudly rather than quietly: the four body-stat genes (owned by thestatsblock), seeds and categories (neither has a "slightly more"), and a value the gene does not declare. - The source checklist replaced four booleans nobody had written yet. "Can the cowboy sell it", "can it found a wild herd", "does it get a spawn egg", "can a stable have one" are four independent questions, and answering them as a set means no consumer has a special case - each just asks
Breed.allows.Feral Mixednames none of the four, which is the honest answer for the absence of a breed, and is what stopped the empty-set default being "all four". - Breed spawn eggs are one item with a component, not forty-nine items. The deciding argument is not effort: a breed a player drops into
config/after the jar was built has no registry entry and could never have an item of its own, so per-breed items would have made the eggs a built-ins-only feature and quietly broken the promise the file format makes. The blank egg is listed in no tab, because an egg with no breed on it is a puzzle rather than an item. - "Spliced (Breed)" needed no new rule. The owner's phrasing - "think of it like a cross between Breed and Splice" - is implementable literally:
spliceis a reserved lineage component, a spliced Friesian is the pair{friesian, splice}, andBreedLineage.combinegained no branch at all. Splicing a cross falling out as Mixed is a consequence of that rather than a decision - three lines have always been Mixed. The first attempt did add a branch, let the mark ride on a cross, and produced "Spliced (Arabian × Friesian cross)"; the test written from the owner's table is what caught it. - Whether a splice "reached the foal" is a question about the foal.
SpliceOutcomeasks whether the foal carries an allele neither parent had. Not "the fed one", because a mare fed a splice for a gene the stallion is already homozygous for was going to produce a carrier whichever copy she passed on - nothing outside can tell whether the carrot did anything, and nothing was added, so nothing is claimed. That reading is decidable and the other is not. - The browser needed a second copy of the breed files, and it is generated by the same tool. TeaVM cannot walk a classpath, so
bakeBreedFilesalso writeswiki/horse-designer/assets/breeds.json- the same content as one array - andjava.jshands it toBreeds.registerBundlebefore anything asks for a breed. Two copies of anything is a chance to disagree, soBreedFilesTestchecks they hold the same breeds in the same order. Finding this also fixed a latent bug:HorseEditorsnapshottedBreeds.all()in a field, which in the browser would have been the empty list. - The breed designer validates with the game's own parser, so there is no parity to keep. Every question the page asks - the gene list, the allele tokens, the epigenetic schemas, the base-coat presets, whether the file is good, what a herd of it looks like - goes to
common/compiled to wasm. The base-coat presets in particular are built by callingBreed.Builder's ownextensionBlackBias()/agoutiBayBias()helpers and reading back what they produced, so the weights the tool offers and the weights in a Friesian's file are the same numbers by construction. - What the page deliberately does not have is a spawn button, and neither does the horse designer. The custom spawn egg gained Make egg this session, which is a second output action with no browser twin, so the divergence is written at the top of both
HorseEditor.javaandgui.jsrather than left to be discovered - hard rule 5. A browser has no inventory; its equivalent of both buttons is Copy code. - The conversion found a bug that had been invisible in the Java. Four breeds - Cleveland Bay, Friesian, Przewalski, Quarter Horse - name no shade pool, because they were written with
.fixed(AGO, ...)rather than theagoutiBlack()helper that callsshadeAny(), andBreedFounderforces an unnamed modifier locus wild. It is precisely whatshadeAny()'s own javadoc warns about. A missing key is obvious in a 30-line file and was invisible in an 885-line class - which is a fair argument for the format on its own. Not fixed: adding the pool changes the shade of every horse of those four breeds in every existing world, and that is a decision about how they should look rather than a typo to sweep up in a conversion pass. Gap 87. - Not built: a per-breed carrot. The request listed one; asked about it, the owner called it an oversight. Nothing was built and no field was reserved for it.
Built 2026-09-07, NOT yet looked at: three stables that generate with rare horses already in them.
Owner-driven, one request with three buildings attached. Design and attribution: generated stables. Checklist §0-AK. Gaps 80–82.
- None of the three files was a Minecraft structure, and that was most of the work. Two are Sponge Schematic v2 and one is a legacy MCEdit
.schematiccarrying numeric 1.12 block ids with no block names in it at all.tools/stables/bake-stables.pyconverts both. The trick that made it affordable is the DataVersion: the output claims to be 1.13.2 rather than current, because the game runs every structure it loads through the DataFixer against the file's own version - sograss_pathbecomingdirt_path, and every other rename in twelve years of Minecraft, is done by the game rather than by a hand-written table that would be subtly wrong. The legacy id table therefore only has to reach 1.13, and it stops there on purpose. - The generation is vanilla's and the horses are this mod's, joined by a structure id and nothing else. That split is what makes "drop in an NBT and a JSON" true: the three worldgen files are ordinary datapack registries this mod has no code for, and
StableDefinitionsonly says what stands in a building it never places. A pack with only the worldgen gets an empty stable; one with only the stable file is ignored. Both failures are quiet. - The unit test found a real design bug in the magic ladder. "All horses have at least one homozygous magic trait, 10% chance of two, 5% of three" is a statement about the horse - and the first implementation added the guarantee on top of whatever the breed's own geometric draw had already given it, so every Friesian had four or five and the ladder described nothing observable. A stable that names a magic count now clears the magical loci and puts back exactly the rolled number. The test that caught it asserted "fewer than half the horses have extras" and got 400 out of 400.
- The four body-stat loci are magical genes and are not magic. Same test run, second finding: clearing "every magical locus" for the no-magic stable also cleared size, magic speed, health and jump - which is how
BreedFounderhits a breed's stat bands, so it quietly deleted the breed's character and made every stable horse a baseline-statted one. They are exempt everywhere now, in both directions: never cleared, never forced. - A stable may not hand out a horse that is dying, and may hand out one that is unwell. Every locus a stable forces comes from
SpliceSafety's pool, so nothing it adds can kill. But a breed's own genetics can still produce PSSM1, exactly as it would on a wild horse of that breed, and a stable is not a promise of a healthy animal. The line is that a building full of dying foals is not a surprise, it is a bug. - Finding the stalls is a block scan, and the alternative was worse. A structure NBT cannot say where a stall is. Baking data markers into the NBTs would have worked and would have made "drop in an NBT" untrue for everyone after us - a modder would have had to edit somebody else's schematic in game first. So the populator reads the placed blocks: a floor, two air, a roof, and two of four sides shut in. Two sides rather than three, because a stall's open side is its gate and the one opposite is often open to the aisle - three finds only boxes, and a box is not a stall. It is a heuristic, it degrades to "put them outside", and it is gap 82.
- The populator fires on the wrong event because there is no right one. There is no "structure finished generating" hook, so it queues on the start chunk's load, waits 40 ticks for the neighbouring chunks the piece spans to finish their own feature stage, and re-queues if any is still missing. Every number in that was picked by reading. The part that is not a guess is the once-only record: without a per-level
StablePopulationDataa stable gains another seven horses every time a player walks back to it, which is the famous failure of this whole class of hook and looks like a feature for two days. - The attribution is a doc with a licence question on it, not a credits line. Three buildings by Seuchendoktor, Alpha_One_Seventeen and Tyiarrah, all redistributed inside the jar rather than linked. Planet Minecraft carries no uniform licence and none of the three has been read. That is gap 80 and it is cheap to fix now (delete two files) and expensive once somebody's world has one in it - so it is written down as a blocker on distribution rather than as a nicety.
2026-09-07: deleted the “HYPP is not built” notes, some sessions after HYPP was built.
Prompted by the owner asking whether HYPP had been added. It had — registered, unit-tested, with its own page and a nav entry. What had not happened was deleting the notes saying it had not.
- Six places were still telling a reader it did not exist: two breed
notes()strings, two cells in the breeds table, and the class javadoc on bothBreedandBreedsnaming it as an example of what the mod cannot model. Tiger eye and the leopard complex were in the same javadoc list and are also built; of the seven early-lethal foal disorders that page named, six shipped and only NNF has no locus. - This is the failure the “update the page in the same change” rule exists to prevent, and it is worth writing down because of where it survived. The wiki pages that own each gene were all correct; the roadmap was correct; what was stale was the prose beside the code, in javadoc and in display strings, which no docs pass ever walks. Nothing behavioural was wrong, and a reader checking whether HYPP existed would have been told twice by the source that it does not.
Built 2026-09-07, NOT yet looked at: five themed random splice carrots.
Owner-driven, one request. Design: the five themed splices. Recipes: items. Checklist §0-AJ.
- They are one field, not five carrots.
CarrotEffect.GeneSplicegained aSpliceCategory, and everything downstream — the token, the fold, the combine recipe, the feed handler — carried on working. That was the point of the check: if adding five carrots had needed fiveCarrotEffectcases and fivefoldbranches, the abstraction the roadmap claimed was cheap would not have been. - Every themed pool is a subset of the safe pool, and that is the whole safety story. Each theme starts from
SpliceSafety.pool()and removes from it, so the existing blacklist covers all six random carrots at once and a health gene registered tomorrow is out of every one of them the moment it exists. There is a test asserting the subset property rather than a comment claiming it, because the failure mode is a dead foal in somebody's world. - “Positive health genes” forced the first carrot that narrows the combination. The blacklist stops at hearts deliberately — a slower horse is not a damaged one — so HMGA2's pony allele and LCORL's short copy are both safe and both the opposite of what a carrot labelled positive promises. Filtering the locus was not enough; the performance carrot draws only pairs that beat the baseline on speed, health or jump and lose to it on none. Size is read neither way, because a draught horse is not a better horse than a pony — which is what drops
HMGA2off the carrot entirely and keepsLCORLon it for its speed. - The coat three are a declared table, and that is a deliberate departure.
SpliceSafety's standing argument is that a hand-written list is wrong the day someone adds a gene and wrong silently. It holds there because the cost is a dead foal. It does not hold here: dilution / white / marking is a taxonomy a person applies, and no measurement of a finished coat sorts dun (a dilution that draws a dorsal stripe), roan (removes pigment without being a spotting pattern) or brindle (paints white and is a striping gene) the way a horse person would. A derived answer would have been wrong and harder to argue with. The table is written against theGenesconstants rather than key strings, so a renamed gene breaks the compile, and an unclassified gene falls to marking if it paints and to no theme at all if it does not. - The base coat is in none of the five. Extension, agouti and shade are what a marking sits on, and a carrot sold as “markings” that turned a black horse chestnut would be answering a question nobody asked. They stay on the unthemed carrot, which promises nothing about what it rolls — and that is the general shape of the whole feature: the themed carrots are the unthemed one with a promise attached, so anything the promise cannot cover stays where no promise was made.
- Two exclusions fell out for free and are worth naming. Silver is a textbook dilution and is not on the dilution carrot; EDNRB is a textbook white pattern and is not on the white one. Neither needed a line of code — MCOA costs hearts and overo lethal white kills, so the blacklist had already taken both. That is the subset property doing its job the first time it was asked.
- The textures are the plain carrot, recoloured by luminance ramp. The leafy top is detected by hue and left alone; the body's own shading is rescaled onto a value ramp of the theme's colour, so the sculpting survives and the five read as one family. The feed particle burst uses the same five colours, so the feedback says which carrot went in rather than only that one did.
Built 2026-09-07, NOT yet looked at: the stable as a table, and an information screen you can read.
Owner-driven, ten requests in one pass. Design: roadmap § the horse browser, breeding § client sync. Checklist §0-AI. Gaps 76, 77.
- Two screens were painting over their own widgets, and it explains the request that opened the session. “Make the barn name box more obvious as a box you can type in” — it was not a styling problem.
Screen.extractRenderStatedraws the widgets and then hands over, which is right for a screen with a background and wrong for one that fills the window with a 94%-opaque panel: theEditBoxwas drawn, and then buried. Same cause on the browser, whereextractLabelsruns after both the widget and the slot passes and the Crafting tab's own button sits inside the panel it draws. Both now put the chrome down first. Worth writing down because the bug was legible in the source from the day it was written and nobody had a reason to read that far — it took a cosmetic complaint to find it, and the honest lesson is about untested GUIs rather than about draw order. - The horse list ships, and it ships against the roadmap's own advice. §18 says the expensive part is a per-player owned-horse index and that sort and filter belong server-side, paginated — “do not sync every horse's genome to the client to filter it there”. This does. The reason it was still the right call: the breeding preview was already sending exactly those rows, with exactly those genotypes, for exactly those horses, so the tab cost one packet field rather than one packet. Building the index first would have meant a second source of truth for the same list while the first one sat there working. The cap is the whole of the difference and it is gap 76.
- One roster, two tabs, so
BreedingRosterisHorseRoster. Keeping a second round trip for the same question would have been two things to keep in step and twice the bytes for the half that overlaps. The entry grew the fields a table needs and a horse's genotype now arrives once for both. - The filter is a language, and it lives in
common/.HorseQueryparses whitespace-separated terms, ANDs them, negates on-, and understands flags (mare,foal,lethal), text keys, numeric comparisons (gen>2) and gene terms. Putting it incommon/was not tidiness: it means the predicate is unit-tested without a game, it survives the backport, and the same code can move to the server the day the index exists rather than being rewritten there.HorseListingis beside it — one horse flattened, with its coat description, four body numbers and disorders resolved once, because a table redraws every frame and a sort compares thousands of times. gene:matches an allele the horse holds, not one it shows. The owner's framing on the roadmap was “show me every horse carryingSB1”, and a filter that read the phenotype would answer a different and much less useful question. A silent carrier is the single most valuable row a breeder can be handed;expresses:is there for when the narrower question is the one you have.- The same argument decided the information screen's new filter. The request was to hide “wildtype genes”. Read literally that removes carriers, which are wild type by phenotype and the whole reason to look at the tab — so the filter tests the alleles: a locus is hidden when the horse is homozygous for the gene's default there. Extension, agouti and shade are kept whatever it says, because “what colour is this horse” has no answer without them. It is also the rule the Coat and Other genes tabs already used, so the screen now has one rule rather than two.
- The full genetic code left the Genes tab and was not moved anywhere. It is a thousand characters; the two surfaces that actually read it are the custom spawn egg and the horse designer, and both take it by paste from somewhere else. On a horse's own screen it was pushing the list it introduces below the fold. Deleting rather than relocating is the point: there was no second place it belonged.
- A foal's inventory screen exists now, and vanilla's reason for not having one does not apply here.
AbstractHorse.mobInteractbails onisBaby()before it reaches the “tamed and sneaking” branch — sensible when the screen is a saddle and two armour slots, wrong when it is also the only door to a horse's genetics. Nothing had to be done about the saddle: the slot'sisActive()already askscanUseSlot(SADDLE), which is false for a baby, so it excludes itself. The handler runs atEventPriority.LOWESTso the carrots, the name tag, the shears and the transfer paper all get first refusal on the click, and a cancelled event never reaches it. - Holding a horse still is a lease, not a flag. A boolean set when the information screen opens and cleared when it closes is correct right up until the client crashes with the screen up — and then that horse stands in a field forever with nothing in the world able to say why. So the client re-asserts once a second and the hold lapses three seconds after the last word. Anything that can freeze a mob has to be the kind of thing that thaws by itself. The freeze goes through the AI (a priority-0 goal claiming
MOVE,JUMPandLOOK) rather than clamping the position, so nothing is fighting it and nothing has to be undone when it lifts. - The i button moved outside the window because there was nowhere inside it. The vanilla horse inventory is a fixed-size sprite with no spare pixels; a button in the top-left corner is a button on top of something. Off the left edge is the only place it covers nothing, which is the same “own exactly one pixel” rule that deleted the grey side panel.
- Every button on both screens is now exactly as wide as what is written on it, and that follows directly from the draw-order fix above. Each had a hand-picked width, each was too big for its label, and while the panels were being painted over the widgets nobody could see it; the moment the widgets came to the front, the browser's “Only what can vary” toggle was sitting on top of the Punnett pane's first line. Sizing from the font means a relabelled or translated button cannot go back to overlapping, which a re-picked constant would.
- Rows carry a real horse now. The My horses table and both breeding pickers draw each horse as a model in its actual coat, and that needed a decision about where the coat comes from: a coat is the genotype and the epigenome, and an epigenome code is around eight thousand characters, so putting one on every roster row would have multiplied that packet tenfold to draw sixteen-pixel horses that are mostly scrolled off screen. So the browser asks for the rows it is actually drawing, once each, and caches them for the session. A player who never scrolls pays for one screenful; a player who scrolls the whole stable pays what the roster would have cost anyway, and only because they looked. The stand-in that the family tree explicitly rejected - inventing a plausible epigenome from the UUID - was never on the table here for a stronger version of the same reason: these are horses you can walk up to, so a wrong coat is falsifiable in about ten seconds.
- Two throwaway horses draw two hundred rows.
HorsePortraitkeeps one adult and one foal, because the render state is built per draw and the coat pushed into it - the entity is only a shape to hang that on.FamilyTreeScreenkeeps one per record, which is right for seven boxes and would be two hundred entities here; the two are not yet one piece of code (gap 78). - The Offspring tab is the pedigree read downward, and it is the only tab in the mod that refuses to refresh itself. Owner's call, and the cost is the argument: the answer is a pass over the whole ancestry table per generation and a full record per descendant, so it happens on a button and the tab says whether what you are looking at was ever asked for.
descendantsOfis grouped by generation rather than flat, because "who came from this horse" is a question about generations and flattening it only makes the caller rebuild it - and it stops at the first horse it has already seen, so a mare bred back to her own grandson is listed once at her first rung instead of looping. - Brindle was showing on every stallion, and the bug is one line of Mendelian bookkeeping. A stallion's X-linked pair reads
n/Y- his one real allele plus the reserved slot he does not have - which is not homozygous for anything, sopair.homozygousFor(defaultAllele())called him a carrier. Every filter in the mod that means "is this horse carrying nothing here" now asksGene.atBaseline, which tests the real alleles. Worth writing down because the reserved-slot scheme is designed to keep sex linkage from leaking into the rest of the model, and this is the first place it leaked: anything that reads a pair's two slots directly is wrong on one gene in sixty-nine. - A gene's tab is now a question about the horse, not about the gene.
KITimplements the eye-colour channel, so every horse alive got aKITrow under Other genes - when only its broad white outcomes ever claim an iris.GeneCategory.of(gene, pair, genotype, epigenome)calls the channel and looks at the answer. The refinement that makes it right rather than merely different: a gene that neither paints nor moves the body - the particle locus, diet, the cutie mark - stays on Other whatever its combination grants, because a silent carrier of one has no business on a tab about colour. SoKITis judged per combination and particle is not, and the difference is what the gene is rather than a list of keys. - The Barn column went, and the barn name did not. It was a column of dashes - almost no horse has one - and it was taking width from Coat and Name. The name is still on the footer line, still on the information screen, and
barn:still filters on it.
Built 2026-09-07, NOT yet looked at: a horse that is a wolf until dawn, a horse that kicks up rainbows, and a particle locus that stopped showing on one copy.
Owner-driven, three requests. Design: LYCAN, rainbow dust, the particle locus going recessive. Checklist §0-AH. Gaps 73–75.
- The werewolf gene swaps the entity, and the requirement is what forced that. The roadmap scoped the
WERElocus “render-layer first, entity-swap second”, and the owner's spec overruled it in one clause: a shifted horse must be interactable in all the ways its animal is and no others, “up to and including breeding with other members of that species”. A horse wearing a wolf skin cannot be bred with a wolf. It also cannot be tamed with a bone, sheared, bucketed, or fed a cookie, and every one of those would have to be hand-written and would still be wrong for the next mob on the list. So at dusk theHorseis discarded and a real animal takes its place. The “and no others” half then falls out for free: while it is a wolf there is no horse in the world, so none of this mod's own horse interactions can see it. The single thing left to enforce by hand is riding. - The horse rides out the night inside the animal, as a tag.
LycanShiftholds the horse's completesaveWithoutIdoutput on the animal — which on NeoForge carries the data attachments, so the record, pedigree, bond, cooldowns and brand all cross the night, and so does theUUID. That last one is the load-bearing part: a shifted horse is the same horse, so the ancestry database and the stalls never notice. The alternative considered was keeping the horse alive off-map and teleporting it back, which needs a holding dimension, a reaper for orphans, and a rule for chunk unloads. A tag that travels with the animal needs none of them, and kills the horse when the animal dies, which is the right answer rather than a compromise. - Two copies of the same allele, or nothing. Thirty-seven shapes at one locus, every one recessive to the wild type and to each other, so
Wlf/Catis a horse and gets its own named outcome. That is what keeps a thirty-seven-allele locus from being a slot machine: if any one allele showed, a shifter would be common and which animal you got would be luck. Requiring the same allele twice makes it a search with a target, and makes two shifters caught in different biomes a disappointment rather than a breeding pair. - The allele set is a mechanical rule, which is why it contains a pufferfish. Every vanilla mob in a non-hostile spawn category, minus the horse family. That is why nobody had to defend the wandering trader and why the golems and the villager are out (they are
MobCategory.MISC) — the alternative was a hand-picked list of “funny” mobs, which is a list somebody has to keep and argue about. The ids are strings, becausecommon/may not import Minecraft; an id this build has never heard of simply never shifts and says so once. - The grudge is one goal because a chicken has no attack. “If struck, they will aggro against anyone who hits them until either sunrise or they lose eyesight” — and
MeleeAttackGoalcallsdoHurtTarget, which reads anATTACK_DAMAGEattribute most of the thirty-seven do not have. Adding it throughEntityAttributeModificationEventwould hand every sheep, cod and wandering trader in the world an attack stat it never had, for the sake of the one in ten thousand that is secretly a horse.LycanTemperGoaldeals a flat three points by hand and touches nothing that is not already ours. - Rainbow dust is its own locus, and that is the whole decision in it. It looks like a forty-first particle allele and must not be one: that locus's premise is that its alleles compete for two chromosome slots, and this one competes with nothing — a horse should be able to trail flames and rainbow dust at once. Filing it there would have quietly cost a flame horse its flames.
- A rainbow needed one new emitter parameter, not a new verb.
cycle: ticks for one lap of the hue circle,0for every other emitter in the mod. Set it and the translator recomputescolorandcolor2off the clock and ignores what the gene put in them, with the second colour a twelfth of a turn ahead so thedust_color_transitionfade reads as “red going orange”. It is a parameter rather than a verb because everything else about a rotating trail is the emitter it already was, and a verb whose only difference is where two ints come from would be a copy of this one. It is the first colour source in the vocabulary that is not fixed. - The particle locus is now recessive to its own wild type, and its founder table stopped being random mating. Both are owner calls and the second follows from the first. Once
Rflm/nshows nothing, a carrier is invisible — and a founder population full of invisible carriers is a locus run in the dark, where you cannot see what a horse has and so cannot choose what to pair it with. So the table now lists only combinations that show: the forty homozygotes and the forty-six codominant pairs, noX/nand no cross-familyDst/Bflm. What you catch is what you watched it do, and the carriers appear one generation down where they belong. The aggregate is unchanged at about one wild horse in thirteen; what moved is that a wild double is now possible (one in twenty-five hundred) and a wild carrier is not. - The golden file moved on eighteen rows, and none of them is a coat. Two new genes with epigenetic schemas shifted the seeded epigenome stream, so every gene that sorts after them and reads epigenetics draws differently — which in the golden set is exactly the magic-sectoral-heterochromia rows. Nothing about how any gene paints changed. Worth writing down because “the golden file moved” and “a coat moved” are not the same event, and the first one will happen again the next time a gene is inserted mid-order.
Built 2026-09-07, NOT yet looked at: an information screen behind an “i”, a breeding preview, and a mare that miscarries.
Owner-driven, five requests in one session. Design: breeding § client sync, § the miscarriage, § every draw written down. Checklist §0-AG.
- The horse inventory screen now owns exactly one pixel of ours. An i button in its top-left corner, and nothing else. The grey 128-pixel side panel behind a collapsible tab is deleted, and so is
GeneInspectScreen; everything both of them said moved intoHorseInfoScreen, a six-tab full-window overlay in the browser's dark chrome. The panel had been growing a row per session — it gained the four body numbers, then a condition list, then a bond line — and it was a column trying to hold a page. Deleting rather than hiding is the risk worth naming: if the new screen fails to open there is no longer any way to read a horse's record in game (gap 71). - The bug the owner reported was one line, and it explains a year of blank panels. “Shift right click a horse, you can still view the family tree menu and genes” — it did not work, and the cause is that vanilla's
AbstractHorse.mobInteractopens the inventory screen without mounting, whileHorseScreenHooksread its horse offplayer.getVehicle(). Riding worked; shift-clicking silently produced a panel with nothing in it. The screen knows which horse it is showing —AbstractMountInventoryScreen.mount, protected, now opened by the access transformer.runServerboots clean with an AT on a@OnlyIn(Dist.CLIENT)class, which was the thing worth checking (api-notes). - Which tab a gene lands on is derived, not listed.
GeneCategoryasks what interfaces the gene implements: an ability, diet, eye-colour or cutie-mark channel wins and puts it under Other genes; otherwise a trait contribution puts it under Health; otherwise it paints and goes under Coat. That is the owner's rule about dhampir generalised — “even though dhampir can affect coat, since it affects MORE than JUST coat, it goes on this tab” — and it means a drop-in gene from the gene creator sorts itself with nobody editing a table. - The Health tab shows each gene's own arithmetic, and that needed a new class rather than a new formula.
HorseTraits.resolvewalks every gene into one sharedTraitBuilderand returns a total;TraitBreakdownwalks them into separate builders and returns the terms. Nothing re-implements a gene and nothing re-derives the sum — adding the terms back up would be a second copy ofbuild()and would drift from it the first time a clamp moved. HMGA2 turned out to be the shape that matters: it moves size, speed, jump and health, so it is listed under all four headings, which a test tried to forbid before the test was wrong rather than the code. - The breeding preview is a Punnett square per locus and deliberately nothing more. The owner's framing was the design: “it doesn't show a baby preview, and it doesn't show all genotypes, because those are more powerful than this needs”. So
BreedingPreviewanswers one question — can this pair throw that allele, and how often — and the odds come out of the same coinGenotype.breedWithflips. The test that matters breeds ten thousand foals and counts, rather than re-deriving the arithmetic, because re-deriving it would only prove the class agrees with itself. - “Group polygenetic genes together” had to be answered without a polygenic system. Philosophy is explicit that every real QTL cluster is modelled as one atomic gene, so there is no grouping to read off. But there are two places several genes genuinely pull on one trait, and both are already declared: the body axes fall out of
TraitBreakdown(every gene that moves speed lands under Speed), and on the coat sideGene.coatDependsOn()nests a silent modifier under the gene that reads it, so the leopard complex arrives as one block. Neither is a hand-kept list, which is the only reason it will still be right in six months. - A miscarriage describes what it looked like and refuses to name the gene. An embryonic lethal used to be a silent non-event; it is now half a heart off the mare, a line to everyone within 32 blocks, and a sentence from
MiscarriageSigns. The withholding is the feature: a pairing that comes to nothing is the only evidence a player ever gets that two of their horses share a recessive lethal, and naming it would retire the pedigree that exists to find it. Two lethals, two visibly different descriptions, and a dozen pairings sorts a herd into two groups without anyone being told the groups' names. The log names it, because a dev reading a log is debugging rather than playing. - Every draw is now written down, which nothing about breeding has ever been.
BreedingReportemits one line per gene — both parents' pairs, the foal's, whether the combination is new, what it expresses, and which parent supplied which copy where that can be said at all. It refuses to guess when both parents could have supplied an allele, because the draw records nothing about that. The log gets all of it always; chat gets the notable loci and only in a dev build. The reason this is worth a class: one foal is a single sample of a distribution, and “every foal came out chestnut” and “five foals happened to” are the same picture from the paddock. - The spawn-egg report could not be reproduced, and three candidate causes were removed anyway. A third party said only that the “custom egg spawner also does not seem to be working”. There is no environment gate anywhere in that path — no
isProduction(), item registered, model present, payload registered unconditionally — so what was fixed is what could plausibly produce that sentence: the egg handled two of the four right-click events, so aiming at a horse fell through to vanilla and mounted it; the Spawn button failed silently in survival and now says creative only on its face; and two server-side refusals were silentreturns and now write chat and log. None of that is a diagnosis and gap 70 says so — the likeliest explanation is still that the tester was in survival.
Built 2026-09-07, NOT yet looked at: the rest of the reference’s disorders, and the first dominant ones.
Closes the roadmap’s §12 gene table. Design: the two shapes a disorder comes in; a page each under Health genes. Checklist §0-AF.
- Nine loci, seven of which were nearly free. HERDA, SCID, lavender foal, cerebellar abiotrophy, CVM, GBED and megaesophagus are all simple recessives, so they are
RecessiveDisorderGenesubclasses of about forty lines each — a name, a carrier rate, aConditionand four numbers. That base class existing is the whole reason the batch was one session rather than several, which is what the roadmap predicted when it said to batch them. - The two dominants are a new shape, and they broke an invariant on purpose. Every disorder before these was recessive, and the layer leaned on two facts: a heterozygote is invisible, and no founder is ever affected. A dominant has neither. The second is the interesting break — if founders could never be affected, a dominant allele could never enter the world and the locus would be dead code, so a wild-caught horse can now be born visibly unwell. The homozygote is still excluded, which keeps the worst outcome something somebody bred. The old test asserting "no founder is affected" is now two narrower tests, and the narrower ones are the true ones: no founder is born with a recessive disorder, and no founder is ever born dying.
- HYPP is the first locus that is two things at once. A survivable heart reducer on one copy and a lethal on two, which nothing in the mod had been, and the reason
DominantDisorderGeneis a second base class rather than a flag on the first. It is also the only locus where the breeding mistake is one you make with your eyes open: an affected horse is visibly unwell and can still be excellent on every other axis, so it is genuinely tempting, and two of them throw a dead foal one time in four. - The reference’s percentages turned out to be unusable as arithmetic. Each disorder carries a health / speed / jump percentage, and the obvious implementation is to multiply. Read literally against a 22-health baseline, HYPP’s "30% health" makes an affected but living horse worse than several of the mod’s outright lethals — which is plainly not what the table means. So every new locus is calibrated against the disorders that already ship and the reference is used for the ordering only. Recorded on
roadmap.html#decisions, because whatever the severity model turns out to be, it does not get to inherit those numbers either. - Two of the nine have no gene behind them, and say so. CVM and megaesophagus are well-attested heritable breed conditions with no agreed causal variant. Every other natural gene in the mod names a real locus with a real published mutation, so rather than invent a respectable-looking symbol they are keyed on the condition itself and both pages state plainly that one recessive locus is the mod’s simplification. The alternative was to leave them out; they earn their place because the simplification is honest at the level the mod models anything.
- The golden coat file moved and not one coat changed. All 1026 lines differed, which was alarming for as long as it took to compare the hash column instead of the whole line: every hash was identical and only the genotype-code label had grown nine
=N/Nsegments. That is the confirmation the health loci really are free —affectsCoat()false, out of the texture key, one catalogue entry each — and it is worth writing down that the golden file's line format makes a no-op look like a total rewrite. - One real interaction fell out.
BreedFounderTestwent red: a Thoroughbred founder can now be born with PSSM1 and be genuinely slower for it, which dropped one seed in sixty under the "near double speed" threshold. The test asks a well horse now. Worth noting because it is the first time a health gene has reached the breed layer at all, and it will happen again to anything that asserts a breed’s numbers. - Left global rather than per breed — the reference associates each disorder with particular breeds and the mod ignores that, consistent with the seven that shipped first.
known-gaps.html#gap-68has the reason and what it would cost.
Built 2026-09-07, NOT yet looked at: epigenetics become literal numbers, foals drift, and a breed stops chasing its own horses.
Owner-driven, and the largest single change to the genetics model so far. Design: the genetics model, breeding § drift, breeds § founder-only. Checklist §0-AE.
- A seed is not a number a player can tinker with, and that was the whole complaint. An allele copy carried one opaque
long, and every gene recovered its numbers by replaying a PRNG off it in a documented draw order. “How much jump does this copy add” could only be answered by running the gene. A copy now carries named literal values declared by the gene inGene.epiSchema()—delta:0.34215,cover:0.4113— stored as text, read by name. The cost is size and it is not small: an epigenome code went from roughly 4500 characters to about 8300, even after dropping the ~25 genes that turn out to store nothing at all. The owner accepted that explicitly in exchange for a genotype you can read and edit. - The draw-order hazard is gone as a side effect, and it was the sharpest edge in the codebase. The position of a draw was the meaning of each number, so inserting one while tuning a gene silently rewrote every horse in every save — which is why the particle locus drew five values on every copy whether it used them or not, and why every varying gene carried a paragraph of draw-order contract. Values are keyed by name now: adding or re-ordering is free, and only renaming orphans a value. About twenty of those contract paragraphs were deleted rather than rewritten.
- Three kinds of value, because drift cannot treat them alike. A
SCALARis a magnitude and drift nudges it continuously. ASEEDis thelongbehind a noise field — there is no “slightly different” splash, so nudging it would replace the pattern rather than adjust it. ACATEGORYis a pick from a list the gene owns, and a boundary nudge would silently move a horse's particles from its mane to its tail. Both of the latter are instead replaced whole, at about one breeding in a thousand. Colours became three channels rather than one packed int so a line's colour can drift — but a founder's still rolls through HSV at a held-up saturation, because a magical mane that rolled a muddy olive reads as a bug. That is the one place a value's roll depends on its neighbours. - Founder distributions had to be declared, or every gene's character would have shifted. The obvious simplification was “a value is a name and a range, roll it uniformly”. That would have quietly changed the wild population on every gene that was deliberately skewed — ednrb's cover is a power curve so most splash horses carry a little white, the magical stat deltas are Gaussian so one copy is usually subtle. The distribution rides on the
EpiValueand onlyEpiRollever consults it. - Drift: every value moves at every breeding, and almost none of it is visible. The magnitude is a two-sided exponential at
EpiDrift.SCALEof the value's design span — half of all drifts under 0.10% of the range, 99% under 0.70%, one in a million past 2.1%. A horse carries a few hundred of these, so roughly one foal in thirty has one value that visibly moved. That shape was chosen over a “rare but large” gate on the owner's call: most pairs breed true, and chaos stays reachable. Drift may leave the range founders are rolled in — only a hard safety clamp holds it — because the design range is where wild horses start, not a ceiling a breeder can never pass. - The breed bug was worse than reported. The complaint was that breeds normalise a cross. They did, and they did it at resolve time:
BreedLineage.statTargets()handed a cross the per-axis average of its two parents' bands, andHorseTraits.resolveapplied it on every single lookup. A Percheron bred to a Falabella had its foal's size recomputed into a mid-size band every time anything looked at it, regardless of which alleles the foal actually inherited, with no way for a breeder to select out of it.BreedFoundernow writes the band onto a founder's two copies as ordinary numbers — unevenly, so the two gametes are worth different things — andbreedBand,BreedStatTargets.average,BreedLineage.statTargets()and the four-argumentresolveare all deleted. A breed label is now cosmetic. - A live bug fell out of measuring the new format.
SpawnCustomHorsePayloadcapped the epigenome atstringUtf8(4096), and the old code was already about 4500 characters across 58 registered genes. NeoForge throws rather than truncating, so custom-horse spawning had presumably been broken for some time and nothing said so. The cap is 65536, andEpigenomeSizeTestnow fails from thecommon/side while there is still headroom — the margin is the point, since registering a wide gene is a normal thing to do. - Values are quantised on creation, and that is a correctness requirement. Every stored number is rounded to what the text format can write the moment it exists. Without it a value would lose a digit on the way to disk, a horse would repaint subtly after a reload, and the two versions would fork the texture cache without ever looking different enough for anyone to work out why. It also makes
parse(toCode())an identity, which is how three round-trip tests found it. - Data-driven genes needed almost nothing.
GeneSpec.Knobwas already this design — a named number with a range — soSpecValues.schemais a ten-line translation into anEpiSchema. The one real change is that aperLegknob draws four independent stored values across a range widened by itsspread, instead of one base plus four correlated jitters; each leg is a number a player can edit. The creator's JS port needed the same change plus the quantisation, or parity failed in the sixth decimal. - The readout is the only window onto any of this.
GeneInspectScreen— the existing “View Genes” popup — grew a per-copy value list under each gene, with the expressed copy marked and an Active/All filter. Extending it beat adding a screen: it was already the per-gene readout. There is deliberately no in-game editing yet; that was staged behind getting the storage right. - Deleted rather than kept:
AlleleRandomness(renamedGeneEpigenetics— nothing about it was random, and the name said otherwise),MidpointRng's role in epigenetics (a schema declares its own midpoint, which is right even when the distribution is skewed), andFamilyTreeScreen's invented-ancestor fallback, which drew a plausible stranger for any record without a stored genome. A blank portrait is the honest answer. - Regenerated: the golden coat file wholesale (every horse moved, deliberately), the spec fixtures, and the designer wasm. Parity is green at 3869 checks;
:common:testis green at 574.
Built 2026-09-07, NOT yet looked at: the walk moves to road level and out in front of the steps, and the cowboy learns to open a door.
Owner-driven, from what he could see in game: the path was in the wrong place, the beds faced the wrong way, and the man could not get into his own barn. Design: Villagers § the barn and § he can work a door. Checklist §0-AC.
- Layer 0 of a building is not the ground, and that is the whole bug. It is the course that rests on the ground — the street's road block is the layer below it, outside the piece — so the lane laid in layer 0 was a block-high curb you walked over rather than a walk you walked on. A structure cannot have a negative layer, so the piece gained a ground course: everything lifts by one and the jigsaw lifts with it, which means nothing about where the homestead lands changes and the new bottom layer falls in the road's own course. Verified by diffing the old and new bakes with the jigsaw as origin — measured from it, not one block of either building moved.
- The walk got a column of its own, and the first answer to "move it forwards" was wrong. It was refused on the grounds that x=0 was the piece's west edge and one column further west is the street's own connector block, which is true — a box reaching over that overlaps the street piece and the placer throws it out, so the homestead would never generate. What that missed is that the barn did not have to own the west edge. Both buildings moved one east, the walk took the vacated column, and the jigsaw stayed put resolving to air above it. The walk now runs the full length of the front, at the foot of the barn's steps and in front of the doorstep and both work posts. The lane between the buildings is gone: that gap is a yard, not a corridor.
- A path under a step is not a path.
DirtPathBlock.canSurviveis false with a solid block overhead and schedules a tick that turns it to dirt, and structure placement runs the neighbour-shape update that fires it. The first attempt ran the walk under the barn's stair skirt and the two posts, which would have laid ten blocks of dirt into the world while the NBT still saiddirt_path. Caught by auditing the bake rather than by playing it, which is the only way it could have been caught — nothing warns you and the file looks right. - The cowboy could not open a door, and needed two unrelated things to. He is an
AbstractVillagerrunning goals rather than a villager brain, so he had neither a goal to swing one nor a navigator that would route through one.CowboyDoorGoalis both halves: it opens both leaves of a double door — one leaf is a one-block gap and the pathfinder rounds a mob up tofloor(width + 1)across, so a single leaf is no use to a horse — and only in daylight, withPathNavigation.setCanOpenDoorsrising and falling alongside it. Vanilla'sOpenDoorGoalwas not usable: withcloseDoorAfter=falseitscanContinueToUseis false immediately and its unconditionalstop()shuts the door it just opened. He closes nothing, deliberately — the herd is following him through. - The partner-leaf rule, because getting it backwards fails silently. A double door's other half is the neighbour along the wall with the same
FACINGand the oppositeHINGE, and both perpendicular sides have to be tried: the hinge says which way a leaf swings, not which side its partner is on. Checked offline against the baked barn — all four openings resolve to two leaves, symmetrically both ways, and the house's single door correctly resolves to none. - Beds turned round so the heads are at the back wall and the feet toward the door. Four blocks.
- Two stale claims fixed while passing.
BarnPoolInjector's frequency note still quoted the old footprint, andCowboyHandlerstill described home as “the place he rides back to at dusk and the box the door code works on” — both the dusk ride and that door code were deleted with the mounted cowboy, and the sentence had become actively misleading now that there is door code again. - Nothing here has been seen in a game. The piece is a column wider than the one that has never been confirmed to generate either, which is gap 62; the door behaviour is gap 63.
The owner queued a run of gene references and left: “don’t ask questions or interrupt this session … use your best judgement, and try to adhere to existing systems as much as possible. Put questions in the verification file.” Every entry below therefore has a Questions for the owner block in its verification section, and every one of them is a real open call rather than a summary.
The standing rule for the whole run: each process/*info.md reference
was absorbed into that gene’s wiki page and the file deleted, and anything a
reference proposed that would have needed a new system was
declined and written down as declined. process/ is now empty and gone.
Nothing here has been seen in a game.
Built 2026-09-07, partly looked at: the cowboy gets off the horse, and the barn grows a house.
A playtest round rather than a build round — the owner played the villagers and reported what he saw, repeatedly, and most of what follows is deletion. Design: Villagers & transfer papers. Checklist §0-AC.
- The mounted cowboy is gone, and that is the headline. Roughly a thousand lines —
CowboyMountGoal,CowboyRemountGoal,CowboyRoutine,CowboyDoors,HerdCollision— deleted at the owner's call after eight rounds of fixes, each of which exposed the next. He is an ordinaryAbstractVillageron foot now; the horses treat him as their herd lead and trail him. The two lessons worth keeping are written up as gap 55 (vanilla navigation will not put a large animal through a two-block door) and gap 59 (a goal that stops for no reason its own predicates explain has had a control flag disabled underneath it). - He survives by not being a target rather than by going indoors. Ten times a villager's health, plus
CowboySafetyHandler, which cancelsLivingChangeTargetEventwhenever anything implementingEnemytries to take him. That covers retaliation as well as acquisition, since both go throughMob.setTarget. AnAbstractVillagerhas no schedule at all, so shelter was never available to him; this is what replaces it. - One job block became two. A single post was meant to hand out a cowboy and then a horseman by alternating, which failed for a reason worth writing down:
Villager.releaseAllPoisis private and death-only, so a villager discarded to become a cowboy holds his job-site ticket for ever and no second villager can ever claim the post. Demoting and calling the publicreleasePoifixed it, and the owner's call was that two blocks were less work than that trick either way. Cowboy Hitch and Horseman's Table are now the same block twice with different names; the hitch is deliberately not a POI, which is what makes discarding a villager at it safe. - Both handlers ask the same question about names.
HorsemanHandler.familySurnamelooks round the village for a cowboy or horseman who already has a surname and takes his, coining a new one only if there is nobody. Order-independent on purpose: which post gets claimed first is down to where the villagers happen to be standing, so neither role can be the one that owns the name. - The horseman's house is baked into the barn's structure piece. It was built at runtime and never once appeared — and the reason it could not have worked is gap 61: the work posts stand on the house, the work posts are what make a cowboy, and a cowboy cannot build the thing that made him.
bake-barn.pynow takes two sources and writes one piece: the barn, plus a reshapedplains_small_house_1beside it with adirt_pathlane between them, a four-wide interior, two beds pushed together, a chest either side of them on a new loot table, and both work posts on the front step with a villager on each. The piece went 15×7 → 15×15, so its pool weight went 1 → 3 to keep the same hit rate against the generator's bounding-box check — which is arithmetic, not measurement, and is gap 62. - A speed floor, set by vanilla's own slowest horse. The owner asked for a minimum "just barely fast enough to outrun a zombie", which does not translate: a zombie's
MOVEMENT_SPEEDof 0.23 is on the walking-mob scale, not the ridden-horse scale, and is above the average horse.HorseSpeedFloorusesAbstractHorse.generateSpeed(() -> 0.0)instead — the slowest horse vanilla itself will ever produce — and clamps only inHorseRecords.applyTraitsToEntity, so the genetic numbers on the info panel are untouched. - Confirmed in game by the owner: the face (the base skin was
villager/type/plains.png, an overlay with a hole where the head goes, not a skin), the barn sitting on the ground with steps up to it, horse prices at 1–3 emeralds, both posts handing out their jobs, the shared surname, the horseman keeping vanilla's brain, and transfer papers end to end. Also confirmed:run/logs/latest.loganddebug.logunderneoforge-26.1.2/run/are readable from here, which settled several disputes in this round outright — including proving a dismount really did fire when the owner could see it had not.
Built 2026-09-07, NOT yet looked at: the test world lands you in a plains village, and the cowboy says hello.
Tooling for the entry below, written straight after it, because testing the two villagers started with a hike and that was most of the cost of testing them at all.
- “Spawn Test Horse World” now teleports into the nearest plains village on login and moves the world spawn there, so dying does not undo it. Plains only, because no other village kind grows a cowboy and a savanna hit would be a wasted trip; the middle rather than the edge, even though the barn is on the outskirts, because from the bell you can see which way the streets run and can walk out along each of them - from a random point on the perimeter you cannot tell which side of the village you are on. The bell is also what the cowboy’s own routine navigates by, so it is the origin of his map.
- It is a synchronous 100-chunk structure search, which is a thing to do exactly here and nowhere else: the handler fires once, in a throwaway dev world, on the one login that created it. A miss is reported in chat rather than silently leaving the player at spawn.
- A cowboy now announces himself in chat when he founds (dev builds only,
FMLEnvironment-gated). He founds the moment his chunk starts ticking, which is usually before the player is near enough to see the barn - so “did one generate in this village?” used to be a question answered by reading a server log while it scrolled past. The absence of a yellow line is now the answer, and a present one carries his coordinates. - The barn bake is byte-reproducible now (
gzip.compress(..., mtime=0)). It was not: gzip stamps the current time into its header, so every run ofbake-barn.pyrewrotecowboy_barn.nbtwith identical content and different bytes. That is gap #45’s defect in a second place, and it mattered more here, becausegit statusis the barn’s only staleness signal - nothing else compares the checked-in structure to the source save it came from. Found by running the end-of-session re-bake and watching a file change that should not have.
Built 2026-09-07, NOT yet looked at: the cowboy rides out — two villagers, and papers that sell a horse.
The roadmap’s §19 villager, a second character the owner asked for alongside it, and the item that connects them. Design: Villagers & transfer papers. Checklist §0-AC.
- The cowboy is not a villager profession, and that was the first real decision. A profession is acquired from a job site by a villager whose brain runs vanilla’s daily schedule - and that schedule would dismount him every morning to go and stand at a workstation. Always mounted, home is a barn rather than a bed, stock is a live herd rather than a trade table: every one of those is a fight with that brain. He is his own
AbstractVillagerinstead, which keeps the merchant screen, the villager silhouette and the trading goals and throws away the schedule. The horseman is a real profession for the mirror-image reason - a shopkeeper who walks to a workstation and restocks is precisely what that brain is for. One ask, two characters, because one piece of machinery does not fit both. - The horse does the walking. A rider that is not a player cannot steer a horse - vanilla only hands control to a saddled mount’s player passenger - so a mounted mob is dead weight while the animal keeps running its own stroll goal. Fighting that from the rider’s side means reimplementing steering; the cheaper answer is to put the itinerary on the mount, as a priority-0 goal holding
Flag.MOVE, which starves every other movement goal the horse has. It holdsMOVEand deliberately notJUMP:JUMPis the horse’s ownFloatGoal, i.e. the thing that stops it drowning with a man on its back. - The barn goes in at the terminator pool, which is where a village runs out of road. The ask was “on the outskirts”, and the jigsaw system has no way to say that. What it does have is
village/plains/terminators- the pool bothstreetsandhousesname as theirfallback, so a terminator is by construction the last piece on a road that had nowhere left to go. That is the outskirts, expressed in the only vocabulary the generator speaks. The barn’s own connector is astreetjigsaw aty=1, matching the relationship the vanilla terminators have to a road. - The append is code, and needed this repo’s first access transformer. A datapack can only replace
terminators.json, which would freeze today’s four vanilla terminators into this mod and silently delete any other mod’s additions to the same pool - a compatibility cost paid forever for a one-line convenience. NeoForge has no template-pool modifier, soBarnPoolInjectoredits the live pool onServerAboutToStartEventand three AT lines make the element lists reachable. ModDevGradle auto-detectsMETA-INF/accesstransformer.cfg, so there was no build change. The append is idempotent, because a single-player session that quits to title and reloads reuses the same frozen pool objects and a blind append would stack a copy every load. - He rides in with the building. The cowboy is in the structure template’s
entitieslist, which is how vanilla puts villagers invillage/plains/villagers/*.nbt- so there is no structure-placement event to hook and no “did a barn generate?” scan to run. A template can only place an entity, not roll one, so he arrives bare andCowboyHandlerbuilds him on his first server tick. That founding is onEntityTickEvent.Postand not the join event, for the reasonHorseFoundingTickHandleralready documents at length: founding spawns horses, and a horse spawn ends in anAttributes.SCALEwrite that can force-load a chunk from inside the chunk system’s own update pass. - A horse is sold as a paper, and the paper is the point. The owner’s framing mid-session settled the shape: buying a signed transfer paper moves nothing, and you walk out to the horse it names to redeem it. That is what makes it worth trading - a claim the bearer has not collected yet is an object, where “pay and receive a horse” is one click that leaves nothing behind. The blank is bound to whoever crafted it and only signs a horse they currently own; the signed one is a bearer instrument anybody can redeem.
bredByis untouched by every path, so a horse still names its breeder after four owners. - His stock had to stop being free. Vanilla lets a player tame any untamed horse by climbing on it until it stops bucking, so a herd of untamed horses standing in a barn is a free stable rather than a shop. A
CowboyBrandattachment refuses every interaction except a paper - and it is the mod’s first synced attachment, because that particular interaction is client-predicted and a server-only refusal makes the player mount and then snap off. - The night has one rule, covering two failures. The owner’s spec had two cases - he never got into the barn, or the barn was breached - and they collapse into one test: is he inside the barn with nothing hostile inside it with him? If not, and something is near, he runs until dawn and comes home whatever the distance. Writing it as one predicate rather than two branches is why the flee behaviour also covers cases nobody enumerated.
- Doors are opened by area, and only his own. A door opened one leaf at a time leaves a one-block gap and a horse will not fit through one, so every door operation works on a box - which gets both leaves of a double door, and all four of the barn’s, without any “find the paired leaf” logic. The first version opened whatever was in front of the horse, which does solve the stated problem and also leaves a trail of open front doors through a village he rides past every day. Cut back to his own barn, plus a standing daylight guarantee that he can never be sealed in.
- The horseman cost almost no code, because 26.1.2 gave the hard part away. Villager trades are datapack registries now, and a trade’s
given_item_modifiersare loot functions that run when the offer is generated. That is exactly the “roll per restock, not per purchase” semantics §19 spent a paragraph arguing for. So the five tiers are JSON and the only Java is one loot function,set_random_gene, whose pool is computed fromGeneRarity- a third-party gene joins automatically, and “legendary and mythic are never sold” is a comparison rather than a blacklist that can go stale. - Deliberately not built: random genotype vials and buying player-made ones back (both need §17’s
collectedFrom; tier 5 buys a stallion seed jar instead), the customHORSE_TRADERgossip type (vanilla gossip works, and it was flagged unverified), and any restocking for the cowboy - his herd is a fixed cast, so a horse he has written a paper for leaves the list for good whether or not that paper is ever redeemed. - Placeholder art, all of it flagged. Both papers reuse the research-paper texture; the Horse Trader’s Post wears vanilla’s barrel; the cowboy and the horseman share one overlay (the owner’s recolour of the farmer hat) over a plains-villager body - shared as a render layer rather than a flattened composite, so there is one file to repaint and no derived copy to go stale.
- Five new gaps (#54-#58), all of them about things only a world can answer: whether terminators really read as the village edge, whether a herd can funnel through a two-block doorway, the two-barns case, brand lifetime across chunk boundaries, and gene-trade pricing that is a plausible ladder rather than a measured one.
Built 2026-09-07, NOT yet looked at: KIT gains the booster group, and it is the common part of the locus.
Four new alleles at KIT, taking it from eight to twelve. Checklist §0-AB.
W35,W32andW34are the same kind of allele asW20- mild, viable homozygous, ranging from no visible white through frank sabino-like spotting, additive beside anything else at the locus. The mod modelledW20as a one-off special case and filed the rest of theWseries as rare founder curiosities. In the one large commercial dataset that measured them,W35is commoner thanW20(0.168 against 0.34 in Thoroughbreds but far lower elsewhere), withW32at 0.061 behind it. Between the four they are most of what this locus does to a population, and the mod had one of them.- So the cascade has a booster group rather than four
pair.has(W20)branches. One booster copy isminimal-white, two - the same one twice or two different ones - ismodest-white, and any booster lifts a stronger allele one step. The three new alleles therefore cost the table nothing. W15is the one strong allele with a homozygote actually on record, and it is all white. It matters that the locus can distinguish “no homozygote has been found” from “a homozygote has been found”; every other strong allele here is on the first side of that.W23moved the other way: it was the locus's “viability unknown, therefore allowed” example atbroad-white, but its source row reads all-white or near-all-white with a suspected lethal homozygote. It is a strong allele now and cannot double.- Founder frequencies are the reference's ordering at roughly half its magnitude, because its dataset is horses whose owners paid for a colour test - a population selected for having something to find. Breed tables carry the rest, and the Thoroughbred gets the 34%
W20the survey actually measured plus traces ofW5andW22, being the breed most of the rare series was first described in. - Declined: haplotype phase (several
KITchanges can sit on one chromosome, and a 2024 dataset found six across one horse's two copies - but an allele pair is the model and linkage is a system);W34’s possibly stronger effect on chestnut, which would cost it an outcome of its own for a “may be”; and the remaining twenty-odd named alleles, which stay on the roadmap where the owner already wants the full set eventually. WhitePatternGenesTest’s count assertion now readsKitGene.nonviableHomozygotes()instead of quoting four. A test that quotes a derived number goes red for the wrong reason the next time the locus grows - the same lessonW4taught this morning.
Built 2026-09-07, NOT yet looked at: the two splash loci swap roles - the common allele was on the wrong gene.
MITF and PAX3. Checklist §0-AA.
- The two genes were right and the two alleles were the wrong way round.
PAX3carried the mod's near-ubiquitous minimal splash allele - 90% of founders wereSW2/N- on the reasoning that a mild splash allele is what gives an ordinary horse its socks and its blaze. The reasoning was right and the allele was wrong.SW1onMITFis the widespread one: a several-hundred-year-old promoter change predating most modern breeds, found across a dozen of them.SW2is a PAX3 coding change found chiefly in Quarter Horses and American Paints. So the common-allele job moved next door andPAX3became rare and breed-clustered. SW1/Nis painted over a range, and it is the only outcome in the white loci that is. That is the reference's central claim about the allele: one copy can be a snip and one white foot, or a full blue-eyed splash, on the same genotype - the paper that identified it says a minimally expressed splash cannot be told from common white markings by eye. Painting it at a fixed strength would make the widespread splash allele the one thing it is documented not to be, which is predictable.WhitePattern.splashgained a two-strength overload; the horse's roll picks a point in the range and then sets the waterline, so it costs no extra draws.- Being honest about the cost:
SW1is now doing two jobs. Ordinary markings in life are polygenic, and the same study found no splash allele at all in 112 horses picked for minimal white. The mod has no markings system, so the one allele that genuinely does hide in plain sight is standing in for one. The honest fix is a small markings contribution running before the white loci - roadmap §10.2b, and a new system, which is why it is there and not in the gene. PAX3’s javadoc and wiki page both still said deafness was not modelled. It has been for some time - the shared informationalsplash-deafnesscondition comes from both loci. Corrected; no behaviour change. A mangled paragraph ingenetics-model.htmlabout founder tables was repaired while it was being updated.
Built 2026-09-07, NOT yet looked at: grey becomes a copy number, and gets four rates instead of one.
Grey - three alleles, and the mod's first coat gene that is also a health gene. Checklist §0-Z.
- Grey is not a point mutation. It is a copy-number variation in and around intron 6 of STX17, and what varies is how many copies a horse carries:
G3a triplication,G2a duplication,Nnormal. Either makes a horse grey, so grey stays dominant - but dosage (G3counts two,G2one) runs 0 to 4, and that is the greying rate. - A rate needs a clock and there isn't one, so the dosage picks a window. Aging stays out; instead each dosage class is drawn at the stage that rate would plausibly have reached on an ordinary adult, and the horse's own epigenetics pick where inside its window it sits - fixed for life. The windows overlap on purpose, because individual variation inside a real genotype class is large and what is reliably true is only the ordering of the means.
- An inverted region rule, found and fixed. The head used to take a point-retention boost - it held colour on the one region a greying horse loses it from first. White hairs round the eyes are the usual first sign. The head now takes a lead, the opposite sign, strongest at the forehead.
- The mane and tail grey from the root. Greying changes hairs as they are produced, so new hair at the root is whiter than old hair at the tip - and long hair is the only place that has room to show. They also carry an independent stage that can run ahead of or behind the body. Plus chubari spots (18%, light greys only) and a bloody shoulder (7%), which are the bounded variation the reference asks for and are visual only.
- Melanoma is reported and never simulated. The same STX17 change drives melanocyte proliferation, so grey emits one
Condition.informational, tiered by copy number and raised a step on a black-based coat, and nothing else: no lesions, no stat docked, no death. Melanoma in life is a lifetime probability that mostly resolves as benign nodules on an old horse, and there is no age here to hang one on. The mod says what the DNA test says and stops. Whiteness is never a diagnosis - the tier reads the genotype, not how pale the horse came out. - Declined: the reference's five-stage health pipeline, whose last two stages (age-related lesion onset, then lesion burden and severity with health effects) need an age clock and a disease-progression system that do not exist.
Built 2026-09-07, NOT yet looked at: dun's mane and tail get the midtstol.
Dun - a dark band down the centre of the long hair with pale guard hair either side. Checklist §0-Y.
- A dun's primitive markings are one continuous system - forelock, crest, spine, tail dock - and the mane and tail were the one stretch of it drawn as a flat point. The marking is not breed-specific: every true dun has it, and the Fjord is only famous for it because the breed clips the mane upright so the cross-section is unmissable.
- The value is set by the mesh, not by taste. The band reuses
HairPattern.centreStripe, so it lands on the part's own axes;MIDSTOL_HALF_WIDTHis 0.25 because a mane is four texels across and that puts the feather exactly between the two texel rows. At 0.20 it caught the dark rows and left the centre 3% off black. - A bay dun had no mane stripe at all, and the cause was an early exit.
alreadyAPoint(red, black)returns 1 for a bay's absolute-black mane, so the painter bailed before drawing anything - a grullo got a perfect stripe and a bay dun was byte-identical to a plain bay. On the long hair the mask now caps the point term instead of losing to it. Everywhere else the maximum is still right: a leg is a point because it is already black, not because a mask said so. - The guard hair is an overshoot scaled by how far each channel is being diluted at all, which is what keeps it a dun feature - a non-dun
d1horse dilutes nothing, so its guard factor is exactly 1 and its painter stays the byte-for-byte no-op the test pins. - Declined: the reference's fictional
F“Fjord mane pattern” allele plus three hidden quantitative modifier loci gating the effect. A new system for a marking every dun already has, and it would have let a genuine dun show no mane stripe - which the same document says is not how dun works.
Built 2026-09-07, NOT yet looked at: sunshine and snowdrop, two more dilution alleles at MATP.
MATP gains sun and sno. Checklist §0-X.
- Both found in 2019, both recessive, both at the same locus as cream and pearl - which is the consequential part, because it means they compete for a horse's two slots and can compound with cream.
sun/sunreads closer to champagne than to a double cream, so its numbers sit near champagne's rather than at a point between pearl and the double dilute. What shows is the black: sunshine leaves enough for real taupe points on a bay where a double dilute leaves almost none, and it keeps a pale amber eye rather than the double-dilute blue.- The look-alike trap is the reason the alleles are worth having.
Cr/sunandCr/snolook like a cremello or perlino and carry one cream allele. A trueCr/Crpasses cream to every foal, a compound heterozygote to half - two identical-looking pale horses, completely different breeding propositions, and no amount of looking separates them. - Two undocumented compounds had to be decided and both say so on the page:
prl/sunresolves to classic pearl (the milder of the two claims) andsun/snoto a double dilute. - Tried and reverted: declaring cream last would have made the gallery's double-dilute pen read
Cr/Crrather thansno/sno, becauseallPairsOfwalks the allele list backwards - but declaration order is also what a genotype code prints, andprlCreverywhere a horse is inspected is a worse trade than one pen label.
Built 2026-09-07, NOT yet looked at: manchado - white with islands in it, and its own locus.
The rare Argentine pattern: broad clean white over the topline with rounded islands of base colour inside it. Checklist §0-W.
- Close to the inverse of a leopard - a leopard puts dark spots on a white horse; this leaves dark islands in a white field. Head, belly and lower legs stay dark; the tail is mostly white.
- Its own locus on purpose. Manchado is not a leopard-complex modifier and not a
KITallele - it is not one of the mapped white-spotting pathways at all, and no white-pattern panel can confirm it. Filing it under an existing locus would have claimed something nobody has earned. - Modelled as the long-proposed rare recessive, the hypothesis that explains the observations: it turns up sporadically out of solid-looking parents across several Argentine populations without spreading like a dominant pinto gene. Carriers only in the wild (~1.2%) and no affected founder, so the only way to see one is to breed two carriers.
- The islands came out as slivers first, and the cause was arithmetic rather than art: which cells are islands was sampled at the cells' own frequency, so it varied inside each one and cut it in half. Sampled far lower it picks island-rich regions and leaves the ovals whole. (Grey's chubari spots reused the fix later the same day.)
Built 2026-09-07, NOT yet looked at: rabicano - dominant to inherit, a roll to see.
White ticking from the tail dock and flank forward. Checklist §0-V.
- The design is the separation of inheritance from appearance. Pedigrees behave as if rabicano were a simple autosomal dominant, but no causal variant is identified, there is no test, and expression runs from a few frosted hairs to something that reads almost roan - so a parent can be minimal enough to have been recorded as solid. The allele inherits like a dominant and what it shows is a per-horse roll that can come out at practically nothing. A plain horse throwing a heavily ticked foal is the commonest thing rabicano does, and a locus whose phenotype is fixed by its genotype cannot model it.
- The homozygote raises the floor rather than forcing the maximum, with the ranges deliberately overlapping - a dosage effect has never been demonstrated because nobody can identify an
RRhorse. - Head, neck, mane and legs get nothing. Rabicano makes no blaze and no socks, and that absence is diagnostic against sabino and splash.
- No health association of any kind, stated at length because every other white pattern in this mod carries something - and asserted through the trait resolver rather than an
instanceof: a rabicano horse resolves to exactly the same body as a plain one.
Built 2026-09-07, NOT yet looked at: pangare, sooty's mirror - and the medial helper gap #51 asked for.
The pale muzzle, eye rings, belly, flanks and inner legs of an Exmoor or a Fjord. Pangaré; checklist §0-U.
- Working on red rather than on lightness is the whole design, because it makes every base-colour rule in the reference fall out of the arithmetic instead of needing a table: a chestnut shows it brightest, a bay's black points are spared automatically (a point is red 0, black 1 - there is no red on it to take), a black horse shows nothing at all, and a double dilute is visually redundant. None of those is a special case in the gene.
- No confirmed mutation and no test - the nearest evidence is a 2018 population study of an EDN3-adjacent region whose marker sat at 99% in Fjords, 98% in Exmoors and 1% in Thoroughbreds. That is an association with a breed group rather than a cause, so it is a dosage with a roll and the population evidence lands in the breed frequencies, which is where it belongs.
CoatRegions.medialcloses known gap #51. Every other region helper addresses a part; “the inside of a leg” is a face of a part, which is why it went undrawn - a leg is a box and all four sides are the same distance from its axis. TheFacewas in the visitor's hand the whole time and unused. It is a shared helper because the medial face is mirrored left-to-right, which is exactly the thing one of four legs quietly gets backwards.
Built 2026-09-07, NOT yet looked at: sooty - a dark cape, made by declining to remove pigment.
- Phase 1 only ever pushes pigment down, so a gene that adds black cannot exist here. Sooty adds none: it declines to remove what agouti and the dilutions were about to take, and the painter walks the black level back up toward - never past - the load the texel began with. That is the honest mechanical description and it happens to be what the biology is thought to be, since the region sits beside ASIP, whose job is telling a follicle to stop. The test asserts the ceiling directly, because if it breaks the pipeline's downward-only guarantee goes with it.
- Two consequences fall out rather than being coded: the same dosage looks different on every base (black over a bay, bronze over a palomino, liver over a chestnut, because the chart's warm edge stays brown a long way down), and it does nothing on a plain black, which has had nothing removed to keep.
- Deliberately not the shade locus, though they model the same 2020 paper. Shade is scoped to bays, where the evidence is; sooty is scoped to the phenotype, which turns up on palominos the evidence says nothing about. Folding them together would have claimed one mutation causes both.
Built 2026-09-07, NOT yet looked at: flaxen - a chestnut's pale mane, as a dosage rather than as folklore.
- There is no identified flaxen mutation and no DNA test for it, and that is the single most important fact about the gene. Breeders have modelled it as a simple recessive for a century on pedigree evidence, which has never been confirmed: there is no demonstrated one-copy-versus-two dose series and the intensity varies far too widely for a two-allele locus. So it is three alleles at 0/1/2, summed - a simulated polygenic modifier that says so in its own description. One strong copy and two mild ones are the same horse, which is what lets two mild carriers throw something paler than either.
- Chestnut only, so a bay or a black carries the whole dosage silently. It takes red away and leaves black alone, which on a chestnut is the whole move: the chart's top row runs brick red to white without passing through grey, so a pale mane lands blond with no tint term - which is exactly what silver cannot do and why silver has one.
Genotype.coatCode()now asks whether a gene paints on this horse rather than whether it can ever paint. About half the population carries flaxen and it is invisible on every non-chestnut, so the old key would have multiplied the bay textures by six for six identical bakes.
Built 2026-09-07, NOT yet looked at: W4, the Camarillo white - a second all-white allele at KIT.
- It paints the identical horse
W22does - pure white, pink skin, everything else hidden - and it is a separate outcome anyway, because the two differ in the only thing a breeder can act on:W22’s homozygote is thought nonviable andW4’s is not known to be. - It is a named line rather than a generic white horse - one stallion, Sultan, 1912, bought at the California State Fair in 1921 and bred to Morgan mares - so the gene dictionary says “Camarillo white”, and the Morgan is the one breed carrying it. That makes it the only breed here that throws an all-white foal rather than a grey.
- A test restated rather than renumbered. “
KIThas eight alleles, thirty-two carryable combinations and eight outcomes” went red on an addition that broke nothing, and a reader cannot tell that from a regression. It became two relationships instead - and survived the locus growing twice more the same day. - Roan:
roaninfo.mdturned out to be the reference the page was already rebuilt from, so the only thing left in it was the seasonal half - real roan reads strongest in a short summer coat and washes out in a faded winter one. Written down as explicitly not modelled, because it is the commonest reason a photograph disagrees with the page.
Built 2026-09-07, NOT yet looked at: the dhampir, a recessive whose carrier is the advertisement.
- One copy gives red eyes with glowing scleras and costs nothing at all. That is the point of the gene: two red-eyed horses in a field are a promise rather than a lottery ticket, and a silent dhampir carrier would be a locus nobody would ever meet. Two copies give the animal - white nose to tail, red eyes, triple health, 1.5× speed, double jump, and it cannot be fed.
- “Cannot be fed” is not a special case. It is
Diet.NOTHINGthrough the diet channel built earlier the same day, which keeps the last claim incodeOrder()- so this gene sorting after the diet locus wins even over a horse homozygous for the lava diet. The same value carries one step further: gated regen now asks the diet handler, so a horse nothing can feed does not quietly heal beside a hay bale either. - Behaviour is three hand-written pieces in the game module, not
effectsverbs - each would be a verb with exactly one user, and that vocabulary is for data-driven genes. It burns in open daylight at half a heart every two seconds (vanilla ignite would kill it before the treeline), runs for shade at the top goal priority, and bites one living non-undead thing a day for half a heart. One bite a day against a five-heart cow means a dhampir sustains itself beside livestock and starves alone. - Two bugs it found:
EyeColor.depigmented()wasrank >= RANK_DEPIGMENTED, indistinguishable from equality until something outranked blue - a magical claim would have given a dhampir one red eye and one ordinary one. And the overlay phase having the last word over the iris is right for the light gene and the leopard sclera rim and wrong here, so a dhampir carryingLPcame out black-eyed.
Built 2026-09-07, NOT yet looked at: a diet locus - twelve narrow appetites, and a channel other genes override.
- Thirteen alleles: an ordinary wild type and twelve narrow diets. Each needs two identical copies, so two different narrow alleles is an ordinary horse carrying both surprises - the same rule the LUT locus uses. The trade is breadth for efficiency: eating anything gains least per item, the single-family diets a hay bale's worth, and cake, potions, lava, one metal and one gem take the horse to full health off one item. Which metal and which gem is a per-horse roll off the expressing copy's seed, fixed for life and inherited with the allele.
HorseDiet.resolvekeeps the last claim incodeOrder(), which is why the locus sits at priority 5, ahead of everything that paints: a later gene saying “this horse cannot be fed at all” wins with no special case.Diet.NOTHINGexists for that and is not an allele.- The handler intervenes only for a special diet, ignores anything that is not a feed attempt (a saddle must still saddle), and lets vanilla love items through - the locus governs healing, not breeding, so a narrow-diet line is not a dead end.
- Wild only, and never a carrier: new
Gene.feralOnly(). The founder table listsn/nand the twelve homozygotes with nothing between; the splice table is its mirror image, because an unasked-for horse that only eats gold is a trap and a carrier is a gift. - Four tests moved, all of them gap #47 - registering a locus at priority 5 renumbers every epigenetic seed after it, and four assertions were calibrated against a specific draw rather than a property. All four restated as properties.
Built 2026-09-07, NOT yet looked at: bay is a shade continuum - blood, bay, liver and seal off one score.
Shade, a new locus that paints nothing; agouti rewritten. Checklist §0-P.
- Bay used to be one expression whose darkness was a bare uniform roll on the
Acopy, with a constant body colour - so “seal brown” meant nothing but “the black climbed high”, and a liver bay could not be drawn at all. - Blood bay, bay, liver bay and seal brown are phenotype descriptions, not loci - they all test as the same
E_A_foundation and no validated allele separates them. What the evidence supports is a regulatory region on chromosome 22 between ASIP and RALY, causal mutation unidentified, plus MC1R and ASIP dosage effects and a polygenic tail. So: three haplotypes at −1.5 / 0 / +1.5, summed, plus anE/Ebonus, anA/abonus and a per-horse roll narrower than a band. - Band edges are placed so every
Sh/Shbay is an ordinary bay whatever the dosage terms do - otherwiseE/Ewould read as a test for liver bay, which is exactly the claim the research does not support. - Every horse carries shade, chestnuts included, so a chestnut line can throw a seal brown two generations out; agouti names it in
coatDependsOn(), which folds it into a bay's texture key and leaves it out of a chestnut's. - Two fixes found in passing: the hoof floor raised the whole leg band to 0.4, so every bay whose black did not reach the knee had four identical socks; and
CoatSampleTool’sSEEDShad drifted two entries pastSAMPLES, silently handing every sample after the gap someone else's seed.
2026-09-06
Built 2026-09-06, NOT yet looked at: a shadow pass at the end of phase 3 - nothing magical is painted true black.
Owner: “true black should never be painted on a horse, because then you lose all the painted-on shading, and it just looks bad. This is especially noticeable with the magic zebra gene.” ColorField.liftShadows, called after the magical loop; checklist §0-O, mechanism on Pipeline.
- The rule is a floor on the colour, not on the composed pixel. Phase 4 is a multiply, so flooring the output would have collapsed every shading level below the floor into one value - the opposite of the ask. Flooring the colour before the multiply leaves the multiply small but non-zero, and the template's strand detail comes through proportionally. A texel at or below
SHADOW_FLOOR(#151515) in all three channels is scaled so its brightest channel lands on the floor, so hue survives and only a texel with nothing in it at all comes out neutral grey. - Applying it to the whole colour field was wrong, and the tests said so in a way worth keeping. Phase 2 already answers the same problem from the other side -
NEAR_BLACKsoftens the alpha of a coat the gradient resolved dark, which is why a black horse shows hair shading at all. Running both over one texel lifts it twice, and four tests went red: a bay's black mane composited brighter than the dark bay body under it, and the same for a foal's tail and a roan's mane. SoColorFieldnow records which texels anapplyd delta wrote and the pass considers only those. One mechanism per layer is the rule that came out of it. - The blast radius is measured, not asserted. 48 of 654 golden cases moved and all 48 carry
magic_zebra; every natural coat is byte-identical. That is the check that the scoping actually holds, and it is cheaper than reasoning about which genes reach zero. - A magical black is now darker than a natural black -
#151515times the template, against#000000at 80% opacity, which is ~20% of it. Intended (a stripe should be the darkest thing on the horse) but it is a visible reordering and is on the checklist as the thing to reject if it reads wrong. - The gene stays wrong on purpose. Magic zebra still subtracts 200% and still aims at zero: committing past the cap is exactly what makes a stripe base-independent, and it must not learn about the floor. The composer decides where paint lands; the gene decides how hard it commits.
- It did not collide with the new chart, and that is luck worth noting.
horselut.png's black corner is#161515- one above the floor - so the natural side never meets the pass even where the scoping would have allowed it. - Found while mirroring the pass into the creator: gap #50.
wiki/gene-creator/js/fields.jsnever got phase 2's alpha ramp and still testsrgb === 0, so it draws dark coats a few percent darker than the game. Recorded rather than fixed, at the end of a session. The general point is that this file has no oracle - the parity check covers the spec engine, not the composer - and this is the second divergence found in it by hand.
Built 2026-09-06, NOT yet looked at: four dilutions set to measured restriction targets.
Owner, now reading numbers straight off the probe: single cream to 20% red / 29% black restricted, classic pearl to 20% / 60%, double dilute to 47% / 95%, champagne to 31% / 80%. Set in MATP and champagne; checklist §0-M.
- The targets are the gene's own constants, not a reading off one horse, and that is forced rather than chosen: a single pair per outcome cannot describe per-horse levels, which differ by base coat. Verified on a chestnut, where a constant reads back directly because there is no black for the tint term to act on. All four land exactly.
- The tool changed the shape of the request. Earlier instructions this session were directional - "much closer to the top", "reduce much more red" - and had to be interpreted, once wrongly. These arrived as numbers, because the probe added the same day turns the chart into something readable rather than something to describe. That is worth more than the values themselves.
- Champagne's
KEEP_REDheld three values in one day: 0.55, then 0.32 when the old chart made a gold champagne barely distinguishable from a chestnut, then 0.69 once the gradient itself was replaced. Nothing was wrong at any step. The point is that a dilution constant is meaningless except against a particular chart, and this is now written on the gene page so the next session does not read 0.69 as a considered absolute. - Champagne is mostly a eumelanin dilution now - four fifths of the black, under a third of the red - which is the ratio that leaves it warm rather than washed out. Gold champagne
#C6AF94, classic champagne#734833. - Single cream and classic pearl are now identical on a chestnut, both at
#B69679. Correct rather than broken - they restrict red equally and a chestnut has no black to tell them apart - but it means one base coat can no longer distinguish two outcomes, and that is on the checklist to judge in play. - One divergence from the stated target, recorded rather than silently fixed. Single cream keeps 0.80 of red and adds a 0.30 black tint, which sums past 1 and clamps, so a black horse keeps all its red rather than 80%. The target holds on a chestnut and not on a black. Left alone because a smoky black belongs on the chart's red edge anyway and the tint was not part of the ask;
SINGLE_CREAM_TINTis the lever if it matters. - Golden regenerated; 0 rows changed that do not carry a champagne or MATP variant allele. Parity re-baked to no diff, wasm re-baked, 478 tests green.
Built 2026-09-06, NOT yet looked at: the LUT chart can be clicked, and searched by colour.
Owner: clicking the chart should show “the amount of restriction of red and black in that location”, and a box to type a colour code into that finds the closest point on the LUT and reports the same thing. Both mark the spot. On the lab; checklist §0-L.
- The inverse of the axis mapping went next to the mapping, not into the page.
GradientLutalready ownedchartX/chartY; it now ownsredAtChartX/blackAtChartYas well. Working the inversion out at the call site would have been a second statement of one fact, free to disagree - and it would disagree by mirroring the chart, which is the most plausible way to get this wrong and the hardest to catch, because a mirrored answer is still a plausible number in range. There is now a test for exactly that: more red must be to the left, more black must be downward. - Both numbers are shown, because a gene is written in one and reasoned about in the other. Constants like
keepRedsay what is kept; the question being asked while tuning is usually how much was taken away. They are complements, so the panel prints both rather than making the reader do it. - Colour → position is a search, not arithmetic, and the page says so. The chart is a painted image: the mapping is not invertible and not one-to-one, and a desaturated chart has whole regions of near-identical greys. So
GradientLut.nearestscans and returns a distance along with the position, and a poor match is reported as a poor match instead of a confident pin. Pure green against the current chart comes back at distance 372 and is refused; a colour genuinely on the chart round-trips to within 4%. - Matching is redmean rather than raw RGB distance - three extra multiplies, and much better at the thing this is actually for. The new default gradient is largely low-saturation browns and greys, which is precisely where a plain Euclidean metric stops agreeing with an eye.
- The amber mark is deliberately the same shape as the footprint's white crosshair. They answer different questions and will often sit close together - one is where this horse's coat averages out, the other is wherever the reader just pointed - so they are distinguished by colour rather than by form, and the amber one carries a dark halo so it survives on the pale corner of a chart.
- Verified offline: all four corners invert exactly, restriction complements what is left, the mapping is not mirrored on either axis, three positions round-trip through the colour search, and every input form is handled -
#RRGGBB, bare hex,#RGB, whitespace, lower case, junk, empty. The drawing is unlooked-at, as ever.
Built 2026-09-06, NOT yet looked at: a new default gradient, and the coat palette changed under every gene at once.
Owner supplied horselut.png and asked for it as the new default LUT. Installed into all three places the chart lives; gap #49 restated against it. Checklist §0-M.
- The chart lives in three places, not two. The source is
common/src/main/resources/.../coat/redblackgradient.png;:web:bakeDesignerAssetscopies it towiki/horse-designer/assets/for the designer, the gene previews and the LUT lab; and:common:bakeCreatorAssetsinlines it, downsampled to 256px, as a data URI inwiki/gene-creator/assets/textures.js, because the creator has to work with no network. Miss the third and the creator draws every horse against the old palette while everything else has moved. - The filename did not change, and that was deliberate.
redblackgradient.pngis referenced from six places across three modules and the build; the name is the default-LUT slot, and the contents are what the art is. Renaming it would have been churn inGeneticCoatTextureFactory,CoatSampleTool,CreatorAssetTool,java.jsandweb/build.gradle.ktsfor no behavioural gain. - The golden file did not move, and that is correct rather than suspicious.
CoatPipelineGoldenTestcomposes against a synthetic gradient by design, so swapping the shipped art cannot turn it red - which is exactly the blind spot gap #48 describes. The whole coat palette of the mod changed and the test suite stayed green at 478. - The near-black ramp held. The new chart's bottom-left is
#161515, not#000000- which is precisely the shape of the bug that made every bay go dead flat earlier the same day, when the rule was an equality against the art. It is a ramp now, and it degraded gracefully: a black horse still bakes 27 distinct colours over a 0–255 luma spread, so the template's hair shading survives the multiply instead of being crushed to a slab. - The palette is much more desaturated, and the golds are what went. The old chart peaked at 68–70% saturation across its whole gold ramp; the new one starts at 58% at the red edge and only declines. There is no gold band at any X, so palomino reads
#E5DBC7, buckskin body#B1A590and gold champagne#E9E4D8- creams and greiges where the names say gold. Left as delivered: the gene constants are unchanged, this is an art choice, and a naturalistic chart is a defensible one. - Two comments claiming a "champagne-gold column near the horizontal middle" were deleted - one in
GradientLut's javadoc, one on the LUT lab page. Both were true of the old art and false of this one, and both were the kind of description that goes stale in silence. Replaced with a pointer at the tool that measures it rather than a new claim that will rot the same way.
Built 2026-09-06, NOT yet looked at: three dilutions retuned off the footprint overlay, and the gradient turns out to be the real constraint.
Owner, reading the new overlay: prl/prl should sit “basically all the way at the top of the lut … nearly to white”, cream should be “MUCH closer to the top”, and champagne “should reduce much more red than it does”. Retuned in MATP and champagne; gap #49 is what the exercise uncovered.
- The overlay paid for itself in one sitting. These constants had never been measured against the chart, only eyeballed on horses, and the numbers were plainly wrong once drawn: a “near-white” cremello resolving at black 0.38, half way down a chart whose bottom half is dark, and classic pearl at black 0.52 - barely lighter than the horse it started from.
- Every
keepBlackcame down hard, because the black axis is what pins a dilution to the dark end: single cream 0.62 → 0.44, classic pearl 0.52 → 0.16, double dilute 0.38 → 0.09. Pearl's red came down with it, 0.55 → 0.17, which is the “nearly to white” part. On a black horse pearl moved from chart 17%,52% to 70%,16% and double dilute from 59%,38% to 85%,9%. - Reading the ask precisely mattered. “Closer to the top” was said of cream with “top is whiter, bottom is blacker” attached - the black axis. Cutting cream's red as well, which a looser reading invited, walked palomino off the gradient's warm ramp and turned it grey; its
keepRedwas put back to 0.45 and only the black moved. Champagne's ask named red explicitly, so red is what moved there. - And then the real finding. Champagne at
KEEP_RED0.32 lands at chart X 68% and comes out#D1CBC2- a grey. Not a bug in the gene: measured off the raw top row of the gradient, saturation holds at 65–70% out to X 50% (gold peaks#EAB74A), then collapses to 43% at X 55%, 25% at X 60% and 3% by X 70%. The right half of the chart is greyscale. A dilution is a move rightward, so there is nothing warm to move into. - Which retroactively explains the old constants. They look timid until you notice they were calibrated to that ramp - champagne's 0.55 put it at X 45%, exactly the
#E3B045the wiki page quoted as its gold. The genes were not badly tuned so much as tuned to a chart that has no pale warmth in it. Extending the cream ramp out to about X 85% is an art change that would free every dilution at once, and is now the blocking item for this line of work. - Golden regenerated, and its diff is the proof of blast radius: 66 rows moved and zero of them carry anything but a champagne or MATP variant allele. Parity re-baked to no diff, wasm re-baked,
:common:testgreen at 478.
Built 2026-09-06, NOT yet looked at: the LUT chart shows which part of itself a coat reads.
Owner, on the lab below: draw on the gradient “where it shows what area of the LUT it's pulling from for that specific coat … that'll also help me tune all the other genes.” It does now. The overlay; checklist §0-L.
- It had to come from phase 1, and that is the whole design decision. The obvious implementation — match each baked pixel back to the nearest colour on the chart — cannot work, and not merely imprecisely: by the time a texel is a pixel its colour has been multiplied by the hair template and moved again by any phase-3 tint, so the position read back is a position the pipeline never sampled. The pigment pair is the thing phase 2 is actually handed, so the pair is the thing to export.
CoatTextureComposer.pigmentField()is phase 1 on its own, andbake()now calls the same extracted helper instead of holding a copy of the loop — so the overlay and the horse beside it cannot drift apart the first time a gene changes how it restricts.coat-golden.txtdid not move, which is what says the extraction was behaviour-preserving.- The axis convention now lives in one place.
GradientLutgainedchartX/chartY, whichsample()scales up and which the new export reports in. Nothing in JavaScript restates the mapping, because an overlay that mirrored the gradient it was explaining would be worse than no overlay at all — and this is the same page that has to warn people about editing the wrong corner of a PNG. - What it draws. The chart is dimmed everywhere the genotype does not sample and the art shows through where it does, brightness carrying density; then a dashed bounding box for the range, a solid bar on the bottom and right edges so X and Y read on their own, and a crosshair at the texel-weighted centre of mass. Underneath, the same span twice — in pigment levels, which is what a gene tunes, and in chart X/Y, which is where to click in an image editor.
- The numbers turn out to be a good description of what a dilution is. On a chestnut, champagne sits at chart X 45% and cremello at 92%: same lookup, read further right. A gene occupying one histogram cell is painting one flat colour; dun occupies 29, which is its dorsal stripe and leg bars sitting at different depths. A gene landing somewhere the gradient has nothing interesting now shows up before a horse does.
- White is off the chart, and the overlay says so out loud. Unpigmented texels are excluded exactly as phase 2 excludes them, so a tobiano reads the chart at 4 160 texels against a solid black's 11 904. Same for an eye colour and for anything painting in phase 3: they do not read the gradient, so they do not appear. That is a limit worth knowing before using the page to tune a magical gene.
- And re-baking demonstrated gap #48 rather than arguing it.
:web:bakeDesignerAssetscopies the coat PNGs as well as the wasm, so the run that shipped the export also overwrote an uncommitted gradient edit sitting in the generated location — which is precisely what that location cannot safely hold. The two copies are back in step and both committed; the gap stays open, because nothing checks that they match.
Built 2026-09-06, NOT yet looked at: a page for trying a gradient on the real pipeline before shipping it.
Owner: a page to “upload and view different LUTs”, with pre-built buttons for the base coats, then the dilutions, then one white gene. wiki/gene-lut.html?view=lab, plus the widget under wiki/lut-lab/. Checklist §0-L.
- It is the answer to a bug from earlier the same day. The near-black regression below was an art edit to the gradient silently switching off a rendering rule, and the only way to have caught it was to launch the game and go looking at a bay. The lab makes that loop a page refresh:
DesignerApi.setGradientalready existed for boot, nothing caches a baked coat, so pointing it at a file off the reader's disk redraws every horse on the page against the new chart. That is the entire mechanism — one export, already exported. - Almost none of it is new, on purpose. It stands on the same three files the per-gene preview window does — the creator's geometry tables, mesh emitter and orbit viewport, plus
common/as wasm — so the coats are the game's bytes and there is no second opinion about what a champagne looks like.java.jsgrew exactly one export, its PNG decoder, rather than the lab holding its own view of channel order. - The page names six gene keys and an order, and nothing else. Every button, label and token inside a locus row is asked of
genePreviewJsonat run time, so a locus that gains an allele gains a button with no edit here. The rows stack rather than being exclusive, because champagne-on-cream is a different path across the chart than either alone and that is exactly where a palette fails. One white pattern only: every white locus paints unpigmented white, which is off the chart entirely, so a second would be a second copy of the same answer. - Driving it in node found the one thing it had promised and could not deliver. MATP's buttons came back
prl/prl,Cr/N,Cr/Cr— noCr/prl, which had been asked for by name. Not a bug:Cr/prlshares thedouble-diluteexpression withCr/Cr, sodistinctPairsOfcollapses them and a coat preview is right to. But MATP also contributes an eye colour, and thereCr/prlhas a constant of its own. Measured over all three bases, the two bakes differ in exactly eight texels — the two eye blocks, atCREAM_PEARL_GREEN#59A08F— and the body is byte-identical. The button was added explicitly, drawn dashed, and labelled with why it looks the same; it is added only when the registry genuinely lacks it, so the day the mod tells the two apart it will not double. - An eye colour is the one thing on that page a LUT cannot move, which is worth saying out loud on a page about palettes: it is a fixed constant, not a gradient lookup. Same for every white patch. The dilutions are the rows that actually exercise a chart.
- Verified offline, the way §0-J was, because there is no browser here. The full call sequence ran against the baked wasm in node: textures accepted, three bases, all six keys resolving and stacking, a stale key reporting missing, a bad token refusing. The load-bearing check is that a synthetic 64×64 chart moved 4 791 texels on a dilution-stacked horse and 11 776 on a plain bay, that an odd-sized LUT is accepted at all, and that switching back to Natural is byte-identical. Everything visual is unlooked-at, including a rotated axis caption and an 860px breakpoint.
- And it surfaced a live inconsistency in the tree. The gradient has a generated second copy under
wiki/horse-designer/assets/, and at the close of the session the two were 118 614 and 6 113 bytes, both edited, neither committed. So the lab's “Natural” button — and every gene preview on the wiki — is currently drawing against a chart the game has never used. Left alone rather than re-baked over, because it is uncommitted owner work and could not be told apart from a deliberate test palette. Gap #48; note the golden test cannot ever catch this, as it composes against a synthetic gradient by design.
Built 2026-09-06, NOT yet looked at: two striped genes stopped sharing a painter, and a third one was written.
Owner: the zebra and brindle genes “shouldn’t use the same painter, and they both currently look awful.” Both true, and the two halves are the same fact. Brindle is white now; magic zebra reads a real zebra’s body map; and the natural counterpart it had been promising since 2026-09-02, zebra striping, exists. Checklists §0-Z and §0-G.
- “Generic” was the bug.
BodyStripesis one function of X for the whole animal, and both genes used it because both wanted “stripes”. Run a zebra through it and you get vertical bars across the rump and up the face — the two things every field guide names as what a zebra does not have — because a zebra’s pattern is a body map: the same machinery landing on differently-shaped anatomy, so the direction changes with the region. Run brindle through it and you get a horse whose two sides match, which is the one thing brindle structurally cannot be. The field was reusable and neither gene wanted what it did. ZebraStripes— the map. Vertical bands descending from the spine on the barrel; arcs round the hip at the rear; rings on the legs; tighter bands on the neck (leaning back as the column tapers) and tighter again on the face; solid dark on the dorsal stripe, the muzzle and the tail; pale at the belly. The rump turn is one lerp and no special case: a vertical band is a plane of constantxand a hip arc is a circle of constant radius from a point on the hip, both are a phase, so the two are mixed — and every value between them is one of the angled, curving bands over the flank that neither shape draws on its own.- It returns the dark coverage, and that is what lets both zebras share it. The biology is unambiguous: dark hair is the default state and a white stripe is locally suppressed melanin. So the natural gene whitens
1 - cin phase 1 and the magical one blackenscin phase 3 — same shape, opposite directions. Regions a real zebra keeps solid dark come back as coverage 1, so neither caller special-cases them and the natural gene cannot whiten the tail by accident. - Natural zebra is codominant, which the roadmap had already settled and which turns out to be the whole gameplay of it. One copy paints the map at 45% — a plains zebra’s shadow stripes, faint bands a shade off the body colour — and two take the gaps to white. Both outcomes run the same painter and draw the same five knobs, differing only in a strength argument, so a shadow-striped mare and her fully striped foal wear the same pattern at two intensities and the expressions cannot drift apart. 1 in 60 per allele: ~3% of wild horses show something, 0.03% show all of it.
- And the gene never picks a colour. It runs at priority 68 — after everything that decides what the horse is, before the white-spotting loci — and only decides where there is less of one. A striped black is black and white, a striped chestnut red and white, a striped palomino gold and white. None of that is code.
BlaschkoStripes— and the asymmetry is the gene, not a garnish. Real BR1 streaks follow Blaschko lines, the paths skin-cell clones trace as they migrate: a V at the topline, long descending S-like streaks on the barrel, shorter ones on neck and hip, transverse strokes on the upper limb. But the load-bearing part is why they are there at all — a brindle mare is a functional mosaic, each patch of her skin having silenced one of her two X chromosomes, so her two sides were coloured in by different draws. Every generic stripe field phases on|z|and is therefore symmetric by construction. It cannot express the trait. So each band here is hashed once per side and the two blended over two body units either side of the spine.- Brindle is white now. It was “a lighter shade of the horse’s own colour”, which is defensible from the literature — real BR1 is as much a hair-texture trait as a pigment one — but the mod has one channel and it is colour, so the pigment half is drawn at its most legible and the texture half is a paragraph on the gene page. It goes through
PigmentField.whiten, which keeps a streak through a black horse on the gradient’s neutral column instead of walking it into the browns. - The white needed a gain, and the reason is worth keeping. Coverage is multiplied up before it is whitened, because the field multiplies a soft edge profile by a per-side presence roll and a break field, so a streak core almost never reached 1 on its own. Without the gain every streak came out a tan smear — which is to say a diluted stripe, which is exactly the thing it had just stopped being. Saturating takes the core to white and leaves the edges, and the streaks the rolls genuinely damped, soft.
- Measured by looking, because that is the only way this kind of change can be measured. A throwaway tool unwrapped each baked sheet back onto orthographic elevations — right side, left side, top, front — and the tuning ran off those: three passes on brindle (wood grain, then tan, then white) and two on the zebra map (the face was noise at 0.42× spacing, and the belly read as a white line ruled along the flank at a 0.34 cutoff). The top elevation is the one that earns its keep: it is the only view that shows the dorsal stripe with the side bands meeting it.
- The floor on stripe spacing is the sheet, not the animal. A real zebra’s facial stripes are finer than anything here and curve purposefully round the eye and nostril. At two texels to the body unit, below about half a unit per band the pattern stops being stripes and becomes static. The narrow spacing and the dark muzzle carry the read; the rest is below the resolution and is written down as not-modelled rather than attempted.
- Registering a gene renumbered every epigenetic seed above it, and three tuned tests were sitting a hair from failing.
Epigenome.randomwalksGenes.codeOrder()drawing as it goes, so a locus at priority 68 changes what every gene above 68 draws and a horse at seed 13 is a different horse. ThreeWhitePatternGenesTestassertions went red, all by margins under 0.05 and none of them a real regression. Confirmed by moving the gene to 99 and watching them pass. Fixed by widening the seed samples to twenty and, where the claim was about shape rather than magnitude, restating it as a proportion — a “framed above” gap stated in absolute white moves every time the mean white level does. The frame leg check gained a second assertion on the mean, so it now catches a coronet band on every horse as well as a sock on one. Gap #47. - What is deliberately not modelled, in both genes. Zebra: species (the knob ranges span plains to Grévy’s instead), facial stripes that curve round the eye, and a reaction–diffusion simulation — what a Turing model buys at this resolution is spacing and contrast, both of which are knobs, and the consequence that does matter (pattern shape follows body geometry) is the whole point of the map. Brindle: hair texture, seasonal change, and the real-world rule that a hemizygous stallion and a homozygous mare have sparse manes rather than stripes — that last one is a deliberate inversion, because the true rule makes the visible phenotype an exclusively heterozygous-mare affair with no breeding target, and it is now written down as a trade on the gene page rather than left as an accident.
- Two source documents were folded into the wiki and deleted.
process/brindleinfo.mdandprocess/naturalzebrainfo.md— the BR1 region-by-region table, the X-linkage and mosaicism argument, and the zebra stripe map and species differences now live on the two gene pages, which is where the source-of-truth rule puts them. BodyStripesstays. No built-in gene calls it any more — dun’s bars went toCoatRegions.legBarearlier, and these two have their own fields — but it is still the data-drivenSTRIPESmask, which is what it is genuinely right for: a gene author with no Java who wants parallel bands with a chevron. Parity re-baked and green.
Built 2026-09-06, NOT yet looked at: an art edit switched off a rendering rule, and every bay went dead black.
Owner: “the black markings on bay are pure black when they shouldn’t be, they should be dark but not #000000.” True, and nothing to do with agouti — it was the same session’s LUT edit reaching a place nobody would look. Fixed in CoatTextureComposer; coat-golden.txt regenerated, wasm re-baked. Checklist §0-D.
- The composer softens black on purpose. The composite onto the white template is a multiply, so a fully opaque black texel scales the template to nothing and the coat loses its hair shading exactly where the horse is darkest — a flat void.
PURE_BLACK_ALPHAcomposites black at 80% instead, letting a fifth of the template through, and that is the whole of why a black horse renders as#2C2C2C/#262626/#292929strand detail rather than one#000000slab. - The test for “is this black” was
rgb == 0. An equality against one pixel of the gradient art. It worked because both bottom corners of the chart were#000000: a black horse is(red 1, black 1)and a bay’s points are(red 0, black 1), and the two corners agreed. Nothing recorded that the rule depended on that. - Recolouring the chart moved one corner and the rule silently stopped firing. The low-red column was cooled toward blue-grey for grullo (below); its corner went
#000000 → #000104. Not zero, so full opacity, so the multiply killed the template: measured, a bay’s mane rendered#000103flat across every texel while a plain black horse next to it kept its#2C2C2Cshading. Every bay, seal brown and bay dun in the mod, from a two-channel change to an image file. - It ramps now instead of switching. Alpha runs from
PURE_BLACK_ALPHAat black to fully opaque byNEAR_BLACK, so no future art edit can turn it off. The width is chosen so the effective multiply is near-constant across the ramp — 0.200 at black, dipping only to 0.184 around#202020before climbing — because the naive fix, moving the equality to a threshold, would have put a cliff from 0.225 to 0.035 wherever a coat crossed it. A dun’s leg bar fading into a grullo body crosses it. - Bay’s points come back at
#28282A/#222324/#252628— the same shading a black horse has, a touch cooler because the corner they resolve to genuinely is a little blue now. A black horse is byte-identical: it was already on the softened path. - The lesson is the shape of the bug, not the constant. A rendering rule was expressed as an exact equality against an asset, so editing the asset disabled a behaviour with no test, no error and no visible connection between cause and effect. Worth checking for others:
CoatOverlayandEyePatchboth describe the eye as “a 2×2 block of pure black” and weight by1 - luma, which degrades gracefully — but the pattern is the same one.
Built 2026-09-06, NOT yet looked at: dun re-derived from a field guide — grullo goes blue, and the leg bars stop being bracelets.
Owner handed over a reference write-up and two specific complaints: the leg markings did not look correct, and dun on black should be grulla, a grey-blue. Both were real and both had the same root. The whole locus is rebuilt around one dilution and one mask; :common:test green, :neoforge-26.1.2:build green, parity green against freshly baked fixtures, coat-golden.txt regenerated, creator and designer assets re-baked. Closes the dun row on roadmap §1; checklist §0-D.
- Grullo was a mouse-brown, and it is the same bug as the whiten one above, seen from the other side. A black horse stores
(red 1, black 1)— pheomelanin the eumelanin masks. Scale both pigments together and the black comes off first, unmasking that red on the way out and walking the sample down the chart’s gold diagonal.PigmentField.diluteNeutralis the fix and the deliberate opposite of the existingdilute(): scale the visible red (red × (1 - black)) and only then the black, so a black base slides down the neutral column instead. Same invariantwhitenkeeps, with the two pigments free to move separately. - It deleted a special case rather than adding one. The old painter had to keep a leg bar’s black only, with a comment explaining that re-introducing red on a black leg turned the bar into a warm patch. That was this bug wearing a workaround. Under
diluteNeutrala bar is just “less diluted” like every other marking and the special case is gone — which is the usual sign the new abstraction is the right one. - The owner had edited the LUT, but only the copy that is generated.
wiki/horse-designer/assets/redblackgradient.pngis output of:web:bakeDesignerAssets, copied fromcommon/src/main/resources. The edit — cooling the low-red column so the neutral ramp is actually blue-grey — existed only there, so the game never had it and the next bake would have reverted it. Promoted intocommon/and both consumers re-baked. Worth remembering in both directions: the designer is downstream, so a change made there is a change that has not happened. - Dun takes black off; it barely touches red. Second owner correction, mid-session: red dun was coming out a pale golden horse. That one asymmetry turns out to be the whole gene, and it gives three very different bases from one pair of numbers — a black horse has no visible red so only
keepBlackapplies and it goes grullo; a bay body is red under a third of a load of black, so removing that black unmasks the red it was damping and the body swings red-brown to tan without the red moving at all; and a chestnut has no black to take, so red dun is very nearly chestnut.keepRedwent 0.72 → 0.93 and is not a dilution knob: it is just enough countershade to let the markings read. A red dun is a chestnut with primitive markings, not a pale horse. - The leg bars were bracelets. A plain function of
y: evenly spaced, equally strong, identical the whole way round the limb, and sitting low on the cannon. Every clause of that is wrong about a real dun, whose bars are short transverse strokes — uneven, frequently broken part-way round, concentrated at and above the knee and hock, and not matched between legs.CoatRegions.legBarnow stacks four separate irregularities on one band phase: a 3D noise warp, a per-band thickness and strength hash, a second noise sample frozen to the band index so the break runs round the limb rather than up it, and a window peaking at the joint. The gene seeds and rolls it per leg. - Freezing the noise to the band index is the trick worth keeping. Sampling
(x, band, z)instead of(x, y, z)gives a field that varies as you walk round the limb and is constant as you walk up it — which is exactly what turns a ring into a slash, and one band into two fragments, with no extra machinery. Measured over 40 seeds: every leg carries a bar, and the large majority of those bars fade to nothing somewhere round the limb. A near-complete band that does encircle the leg is possible, which is also what the reference says. - The dilution spares the points, and
black × (1 - red)is how it knows. A dun dilutes the body coat; the points go with the base colour, which is what makes a bay dun a tan horse with black legs. Mane, tail, ears, muzzle and the lower leg are masked by region — but a bay’s black climbs its legs and face by a rolled amount that region cannot know. The pigment model already said it: a gene that paints a point paints it absolutely (BayCoatsetsred = 0, black = 1), and black-with-no-red-under-it occurs nowhere else, while a black horse is(1, 1). One multiply separates them, and the partial values through a bay’s leg ramp turn out to be exactly the fade the mask wanted. Square-rooted, because without it the top of a seal bay’s black leg diluted to a grey cuff between the tan body and the black point. - New markings, and they are deliberately not on every horse. A shoulder bar and a face mask or cobwebbing (a strong mask replaces the finer web rather than joining it), plus the per-leg bar rolls, all drawn off the expressing copy’s epigenetic seed. The reference is explicit that leg barring, shoulder bars and forehead rings occur only “sometimes” and that the dorsal stripe is the one diagnostic feature — so the accessory roll is not a uniform: most duns that have a shoulder bar have a hint of one. The gene is non-deterministic now; the draw order is a contract, and both marked outcomes draw all eight numbers so a
d1horse that gains aDcopy keeps the stripe it had. - Deliberately not drawn: flank mottling and rib shading (the reference calls them supportive, never diagnostic, and half of duns have none — a blurry patch costs a marking’s worth of noise on a 128px sheet and buys no recognition); a fan of fine shoulder lines, which at two texels to the body unit is three pixels of noise, so it is one smudged stroke; and separate neck bars, since the dorsal stripe already runs the neck and the mane is a point, so the crest comes out darker for free. The dorsal stripe also stopped running down the face — it never was a dun marking, and the head has a mask instead.
- Rendered, not measured. Side, top and per-leg unrolled views at 6–40× across a dozen seeds, on all three bases. The dorsal stripe is invisible from the side — it is on the top face — so a side view alone would have said the stripe was broken, and a leg bar is two texels wide, so the atlas says nothing about it at all. Third session running where the view chosen decided whether the bug was visible.
Built 2026-09-06, NOT yet looked at: white fading through both pigments was browning every marking on a black horse.
Owner: “it’s fading through both black and red pigments, leading to brown edges on otherwise black horses … if we’re doing edging on a black marking, it removes ALL red pigment to do the edging, so the edges should be shades of grey.” That is now PigmentField.whiten, one verb every white marking goes through, plus a WHITEN op so data-driven genes get it too. :common:test green (470), :neoforge-26.1.2:build green, parity green against freshly baked fixtures, coat-golden.txt regenerated (66 rows), wasm re-baked. Closes the “odd 1px gold fleck” line on known gaps; checklist §0-F3.
- The bug is that a black horse stores red it is not showing. Its pigment pair is
(red = 1, black = 1)— a full load of pheomelanin that the eumelanin masks. The gradient’s whole bottom row is#000000, soblack = 1is the only place a red level is invisible. Scale both channels toward zero together and the red is unmasked on the way out, straight down the chart’s gold diagonal:#5F330Bat a quarter white,#885517at three eighths. Chocolate and tan, around the edge of every marking on a jet-black horse. - The fix holds the visible red constant, not the stored red. Red only shows where black is absent, so what whitening should scale is
red × (1 - black); the stored red is then whatever reproduces that against the black that is left. On a black horse the visible red is zero, so the stored red drops straight to zero and the fade runs down thered = 0column — the chart’s one neutral ramp,#212121 → #414142 → #A6A6A7 → #FFFFFF. On a chestnut nothing was masked and the expression collapses tored × keep, so strawberry roan did not move at all. - It is an identity at
amount = 0, and that is load-bearing. A correction applied flat to every marked texel would change the base coat wherever a soft edge tapers to nothing, and a smoky black or a diluted point would develop a seam at the edge of every marking. Expressing it against the black that is there is what makes the two ends meet. - Measured on a side view, not on the atlas. Warm texels (saturation > 0.20) over the right-side projection: blue roan 194 → 65, black leopard 211 → 59. A plain black horse measures 64 — the template’s own hoof and nostril shading surviving the multiply — so both are now at the floor. Bay roan went 1147 → 1155 and varnish bay 915 → 945, i.e. unchanged; the red-based coats keep every bit of their warmth. Same lesson as the frame session: the atlas is not readable enough to judge this, the side view showed it in one look.
- Only two genes actually partially whiten, and the golden file proves it. Of 624 rows, 66 moved, and every one carries
roan=Rnorleopard=LP. Every hard-edged white marking — tobiano, both splash loci, the wholeKITladder, frame, dominant white — is byte-identical, which is the check that the refactor onto the shared verb changed nothing it should not have. - The hard markings moved onto the same verb anyway.
whiten(1)lands on exactly(0, 0)and still takes the composer’s transparent path, soWhitePattern,EDNRB,KIT,TobianoGeneandCoatRegionsall call it instead of a hand-writtensetRed(0); setBlack(0)pair. There should be one way to make a horse white, not seven — and the next white gene should not have to rediscover the diagonal. - Both genes already carried an approximation of the fix, and that is why it survived. Roan and leopard each pulled red down faster than black (
red *= keep * (1 - 0.6a)) with a comment explaining the gold diagonal. It narrowed the visible damage enough that the residue got logged as “the odd 1px gold fleck” and left alone — but it only reaches zero red when the texel is already fully white, so the whole middle of the ramp stayed warm. A partial fix with a correct-sounding comment is harder to see than no fix. - Data-driven genes get
WHITEN, a new op with oneamount. The four-file contract:SpecSchema,SpecPainter,js/{schema,spec-engine}.js(pluswhiteninjs/fields.js),gene-format.html.SET_PIGMENTto(0, 0)was how the example tobiano spec drew its patches and its socks, both with soft-edged masks, so the creator was previewing exactly the brown fringe the built-in genes had; that example is onWHITENnow and the parity fixtures bake it.
Built 2026-09-06, NOT yet looked at: tobiano’s coverage knob was lying too, and fixing it changed nothing visible.
Owner’s call, after frame: bring tobiano in line without changing how it works. It had the same defect — a cover knob cutting a bell-shaped noise field at a constant — but it landed on a different part of the bell and so never produced a broken-looking horse. A range written as 0.40–0.56, which reads like a 1.4× swing, sat across the steepest part of the field and delivered 31%–86% coverage: a 2.8× swing. Frame’s sat outside the field’s spread and came out blank-or-flooded; tobiano’s sat across the middle and came out over-sensitive. :common:test green, :neoforge-26.1.2:build green, parity green, coat-golden.txt regenerated (12 rows, all tobiano=To/to), wasm re-baked. Closes the second half of #30.
- Holding the look steady was the constraint, not a side effect. The new constants are the range the old code measurably produced, and the two biases were re-tuned against it over three passes. Over 400 epigenetic seeds, before against after: markable surface 0.31–0.86 both ways, back median 0.64 both ways, legs median 0.74 against 0.76. Individual coats moved — the threshold is computed differently, so the golden file had to move — but the population did not. A 24-seed side-view sheet was rendered before and after and compared.
- The topline bias was landing on the mane. It ramped across
HorseSkinGeometry.bodyBounds, the whole-horse box that runs to the ear tips — on which the barrel’s spine sits only 35% of the way up. So the bias whose entire job is to push patches over the back was delivering about a third of its nominal strength there and its full strength on the mane. It ramps up the barrel’s own height now and saturates above it, which is what a bias toward the topline should do: the crest and mane are over the topline, not beyond it. Same class of defect as frame’s old ceiling, and the last white painter still on the whole-horse AABB. - Patch size is periods along the barrel now, not an absolute body-space frequency. The adult numbers are unchanged by construction; the foal is the thing that changes, and it now carries the same relative patch scale as the adult it grows into rather than a coarser version of the same field.
- Tobiano had no shape tests at all — only a check that the gene composes with
KIT. That is why this sat there. It has four now: that the coverage lands in the band its knob declares and reaches both ends of it, that the white crosses the topline, that the legs go white, and that the head stays coloured. The first was verified to fail against the old bare threshold before being kept, so it is a regression test rather than a restatement of current behaviour. The topline one is the deliberate mirror of frame’s framing test: tobiano runs over the back, frame is framed by colour above and below. - The test reads the gene’s own constants (
COVER_MIN/COVER_RANGEare package-private for it) rather than restating the numbers, because the claim being pinned is an identity between what the knob says and what the coat does. Restating it would have been one more number to go stale. - Not audited: the other painters. Any painter that cuts a noise field at a constant is making the same assumption, and
WhitePattern’s shared ramps are where to look next. Left open on #30 rather than quietly closed.
Built 2026-09-06, NOT yet looked at: frame overo was drawing a dipped horse, for three separate reasons.
Owner, on the frame rebuild earlier the same day: “the way the markings are drawn is totally wrong … yours is still looking like the horse was dipped.” It was — white rising to a frayed waterline with the coloured coat reduced to scraps. Frame is big splotches across the middle of the side, framed by colour above and below. Three independent faults made the dipping, each individually plausible, and all three had to go. :common:test green, :neoforge-26.1.2:build green, parity green, coat-golden.txt regenerated (18 rows, all ednrb=O/N), wasm re-baked. gene-ednrb.html#paints.
- The threshold was never calibrated against the field it thresholded.
PatchNoise.fieldaverages three octaves, so it concentrates near0.5and rarely leaves[0.25, 0.70]— measured over the body texels at three seeds. Cutting it at1 - coverover a range that ran up to0.86and down to0.24put most rolls outside the field’s spread entirely: a frame horse came out blank or flooded, with nothing in between.paintFrameis two passes now — score every eligible texel, sort, cut at the quantile matchingcover— so the knob is an area fraction by construction and cannot drift when the field or the geometry moves under it. - The band was open downward, and that is what made it a dipping. A floor below the underline with no penalty above it is a bottom-anchored gate: every texel from the belly up was equally eligible, so white arrived as a rising tide with a frayed top edge. Retuning the noise could never have fixed it, because the band and not the noise was drawing the shape. It is a soft centre now, measured against the barrel’s own height, with the bar rising quadratically away from a per-horse band centre — so the strongest peaks push a patch down over the belly or up toward the hip while everything else stays gathered around the middle of the barrel.
- The jag ran finer than one texel. A barrel texel is half a model unit; the jag’s frequency was
3.4per unit, a period of a third of a texel. It was not an edge treatment at all but per-pixel white noise, and it is what dissolved the coloured coat into scraps. Measuring the texel grid first — 880 texels over a 21.5×9.5-unit barrel side — is what turned that from a guess into a number. - Two of those were one knob wearing two hats, which is why neither could be set correctly. They are separated now:
coverdecides how much white there is, the band and the side lean decide where it pools, and a bias that only re-ranks texels cannot change how many of them clear a quantile. - The tests passed the whole time, and that is the lesson. They asserted a dark spine and a white belly — and a dipped horse has both. The missing claim was that frame’s white is framed: colour above it and below it.
frameWhiteIsFramedAboveAndBelowreplaces the belly assertion, which had literally demanded the dipped shape (“the belly must be whiter than the flank”). Swept over 400 seeds: no seed puts white on the spine or the crest, and no seed comes out with a belly whiter than its own flank. - What actually found it was a picture. Every offline band measurement agreed with the broken code at every step, twice. A throwaway orthographic side-view renderer — composite each
Face.LEFTtexel at its body-space position, sheet 24 seeds — showed the fault in one look. Worth rebuilding for any future coat work; the texture atlas alone is not readable enough to judge a pattern. - Noise frequencies are periods across the barrel rather than absolute units, so a foal is marked at the same relative scale as the adult it grows into. Same change tobiano got later in the day.
- In-game and catalogue text moved with it —
GeneDescriptionsand theframe-overoexpression both described the sideways-along-the-belly shape, which is no longer what the gene draws.
Built 2026-09-06, NOT yet looked at: the two roans stopped being the same pattern at two strengths.
A region-by-region reference on classic roan and varnish roan, read against the code, said the same thing about both: they had converged. Classic roan had a front-to-back gradient that left a dark shoulder, which is a varnish trait; varnish had the same noise frequency and the same gradient and no anatomy at all, which made it a weaker classic roan. The two are told apart by shape, not by how much white there is, and now they are. :common:test green, :neoforge-26.1.2:build green, parity green, coat-golden.txt regenerated with three new roan cases, wasm re-baked.
- Classic roan is even over the whole trunk now, and that removed a shoulder shield nobody had noticed. The intensity used to fall from the hindquarters forward starting at mid-barrel: measured shoulder 0.27 luma against hip 0.80 on a black horse, a 3x gradient where the reference asks for "broad, relatively even distribution". Worse, "a strongly defined shoulder shield of dark hair" is the thing a classic roan is confirmed by not having - it is a varnish trait - so the gene was drawing the wrong pattern's diagnostic. The body weight is a flat 1.0 now and the five trunk zones measure within 0.08 of each other.
- The taper moved to the neck, where it belongs. Something had to fade or the roaning would end on a hard line against the solid head, but the reference puts the neck in the pattern ("roaned, often substantially, especially at its lower and middle portions") and the head out of it. So the neck thins toward the poll, measured against its own height rather than the barrel's, and the head/neck contrast is the "dark mask" classic roan is spotted by across a field.
- The inverted V, and the version of it that did not work. Roan's most characteristic edge is the dark lower leg rising into the roaning in a point rather than on a level line. The first attempt was a cone measured from the leg's axis, and it drew a perfectly flat boundary: a leg is a box, so every texel is on one of its flat sides and is always at the outer radius. Measured across the face being painted instead (
Face.spanA()is the horizontal axis of whichever side you are looking at) it comes out as a real V - mid-face still base-dark at 0.20 while the face edges have roaned to 0.35-0.58. - One noise octave, not two. Classic roan is "a distribution of individual white hairs, not necessarily a collection of white patches".
PatchNoise.fbm2carries most of its weight in the low octave, which clumps the white into flecks; a singleBodyNoise.valueoctave at about one lattice cell per texel makes neighbouring texels near-independent. Measured patchiness fell from 0.15-0.27 to 0.07-0.10. Patchiness is varnish's job. - And the density came down. Making the body uniform made every roan much whiter, because the old average was a gradient. Retuned so a blue roan reads slate - a dark horse with white through it - rather than grey: a mixture that reaches three quarters white has stopped being one, and it would have collided with grey.
- Varnish roan got the trait it is actually recognised by. A varnish horse whitens around its bones without erasing them, and the residual dark zones - cheekbones, shoulder blades, elbows, hips, stifles, knees - are what the pattern is named for.
varnishMarksderives a set of body-space spheres from the mesh's own part boxes (so a mesh change moves them), andpaintVarnishsubtracts that retention before it roans anything. Measured: the shoulder and hip shields come out 0.36-0.54 luma darker than the barrel around them, against 0.02-0.09 for classic roan, which is the difference the reference leads with. - Two details that keep the marks from looking drawn. The strongest mark wins rather than the sum, so two overlapping marks cannot add up to a solid patch; and the distance test is wobbled by a noise field, so the island's outline is ragged - a real varnish mark follows the bone loosely. How deeply the marks hold is rolled per horse, because "varnishing out" is a process and different horses are at different points along it.
- Varnish's own gradient went, and its mottle got coarser. The front-to-back taper was doing the shaping; the marks do it now, so the region weight is nearly flat and all the structure is anatomical. The mottle frequency dropped from 2.4 to 1.15, roughly half classic roan's, so varnish reads as irregular pale areas where roan reads as salt-and-pepper - which is what "uneven or incomplete roaning" means when a real horse is being assessed.
- Varnish reaches the head now. It was at a flat half strength and measured barely above the base colour; it is at 0.85 with the cheekbones carved out. A dark head over a roaned body is the classic-roan tell, so a varnish horse having one was the loudest way the two could be confused.
- The tests are the reference's identification checklist.
RoanPatternsTestasks, in order, what a person assessing a photograph asks: check the head and distal legs, look at the body distribution, look for the inverted V, look for the anatomical shields. Eight assertions across six seeds, all measured as mean luma on a black horse so "darker" and "more roaned" are one number.
Built 2026-09-06, NOT yet looked at: frame overo re-derived from the reference, and it closes a real defect.
Partly superseded the same day by the dipped-horse fix above. The topline reference below is still the fix it says it is; the shape it built on top of that — a white belly, a band open from below, five-to-one anisotropy — drew a dipped horse and has been replaced. The belly assertion described here was the test that had to go.
The owner supplied a locus-by-locus description of what frame overo actually looks like on a horse. Read against it, the painter got two things right (jagged margins, a bold face) and most of the rest wrong. Every constant moved, the outcome id is frame-overo rather than frame, and the shape is now written as a list of the reference's claims with the code that answers each — gene-ednrb.html#paints. :common:test green, :neoforge-26.1.2:build green, parity green, coat-golden.txt regenerated, wasm re-baked.
- The band was measured against the wrong horse, and that was the defect (#30, which predicted it and deferred it).
BAND_HI = 0.74was a fraction ofbodyBounds— the whole-horse box, hoof to ear tip — which is 1.19 of the topline. So frame's white could cross the spine, which is the one thing frame never does; measured back white was ~75%. It is a fraction ofWhitePattern.toplineHeightnow, where 0.52 is the underline and 1.0 the spine, and measured spine white is 0.00 at every seed.toplineHeightis public for it; this was the last white locus still on the whole-horse AABB, and splash and sabino were moved off it a session earlier for the same reason. - The gap entry said the obvious fix would break the neck, and it was right. Switching to
bounds(skin, Part.BODY)alone zeroes frame's legitimate neck white, because the neck stands above the barrel and is out of range of any band expressed in barrel heights. So the neck gets a band of its own, against the neck's height, with a ceiling that leaves the crest dark. Frame horses had a completely dark neck before, which nobody had noticed because the barrel band was flooding everything anyway. - The belly was excluded, and the belly is the point.
BAND_LO = 0.28was documented as "above the belly". The reference is unambiguous that the lower barrel is one of the two hallmark locations and that white climbs from the belly toward the hip. The floor sits below the underline now, and the test asserts the belly is whiter than the flank rather than merely non-zero — the direction of the climb is the claim, not the presence of white. - The white now runs along the horse. The single most diagnostic thing about frame is that it spreads sideways, where tobiano runs top-down over the back and splash comes bottom-up from the feet. The patch field is sampled with
xsquashed andystretched, about five to one. Sampled isotropically it was a paint splat, which is what every white gene is. Pinned as edge anisotropy on the barrel's two side faces — a horizontally elongated patch crosses fewer margins scanned across than scanned up — which is measurable without unprojecting anything. - Cryptic expression is now reachable, and it is not a bug. The old cover roll was 0.52-0.74, so every frame horse was obviously frame. The reference is emphatic that a carrier may show little more than facial white and a blue eye, and that "no obvious frame pattern" does not rule out
N/O— that is why the locus is DNA-tested. The roll now reaches low enough that nothing clears the threshold, skewed toward the bold end so roughly one in five is near-invisible. The face is rolled independently, so a cryptic frame still has a bold face and a blue eye, which is exactly how one is spotted. - Two smaller ones from the table. A per-horse lean across the width, because a frame horse's two sides genuinely do not match — one flank can carry a bold hip patch while the other is largely dark. And an occasional coronet band, drawn with a wobbled edge rather than through
CoatRegions.whitenLowerLeg, whose hard cut ends in a perfect ring; nothing else on a frame horse has a clean margin. - Half of the
covercalibration gap goes with it. #30's second bullet -coverknobs written as ifPatchNoise.fieldwere uniform when it is a bell - is closed forEDNRB, whose range was re-picked against measured coverage.TobianoGene's 0.40-0.56 still delivers 23%-68% and is still open. - The measurements are assertions now, not prose. Five tests in
WhitePatternGenesTest: the spine is white-free at every seed, the belly is whiter than the flank, the legs and crest stay dark, the patches are wider than tall, and the range reaches both cryptic and textbook. A number written into a wiki page goes stale the next time the painter moves; a number in a test fails.
Built 2026-09-06, NOT yet looked at: green and hazel eyes, both heterochromias, and a magic locus that only exists in the heterozygote.
The eye channel was built two sessions ago with one colour for both eyes and a note that said the rest was “content, not machinery”. It was not: one blue eye is not a colour, and neither is a wedge. This session finished the channel and gave it its own page, Eye colour & heterochromia, which is now the single source of truth — pipeline.html#eye-colour and the tiger-eye page both shrank to pointers. :common:test green, :neoforge-26.1.2:build green, parity green, coat-golden.txt regenerated with nine new eye cases, wasm re-baked.
- The ranks are now the three biological routes, not two numbers. A whole-body dilution that catches the iris (
RANK_DILUTION: cream, pearl, champagne) loses to a gene aimed at the iris and nothing else (RANK_PIGMENT: tiger eye), which loses to an iris that never got melanocytes (RANK_DEPIGMENTED: the four white loci). That is straight off the reference table the owner supplied, and it settles a real ordering for free: a cremello that also carries tiger eye has amber eyes, because tiger eye is about eyes and cream merely arrived there. - Green and hazel come from champagne, and they are epigenetic. The literature is explicit that an adult champagne's eye is “amber, golden, hazel, light brown, or occasionally olive/greenish-hazel” — a range, not a colour — so it is rolled once off the expressing copy's seed: amber 40%, hazel 35%, light brown 17%, olive 8%. An olive-eyed champagne is now something a breeder can chase. The cream–pearl compound
Cr/prlgives the other green, a blue-green, and it is the one row of that locus where the two dilutions disagree about the eye. - The cost of that, and why it was the right trade. Champagne was the mod's one fully deterministic dilution and is now
varies(), because the eye is baked into the coat texture and an undeclared variation lets two visibly different horses share a cache entry. It looks expensive and is not: almost every horse already carries something non-deterministic (bay's black climb, grey's dapples), so the marginal number of forked textures is small. Recorded on the champagne page rather than left to be rediscovered. - Complete and sectoral heterochromia are one mechanism, and it is not a gene. The white loci already answer is this horse blue-eyed, and that is an allele question. How far did the blue get is not — one
MITFcopy produces anything from one blue eye to extensive splash — soEyeSpreadrolls it once off the epigenetic seed of the locus that won the claim: both whole 62%, one whole 22% (complete heterochromia), a wedge somewhere 16% (sectoral). One roll, not one per locus, deliberately: a horse carrying splash and frame would otherwise stack two masks and become unable to have a single blue eye at all, which is backwards. - That forced the painting order to change, and the change is the insight. The pigment claim is now painted over both whole irises first, and the blue laid over as much of each as the spread reached. A depigmented iris is not a colour painted over the pigment, it is pigment that never arrived — so what shows where the blue stops is the horse's own eye, and it has to still be there underneath. A splashed white tiger-eye horse with one blue eye has an amber other one, and that falls out rather than being special-cased.
- Four texels, twelve shapes. An iris is a 2×2 block, so a sector is a four-bit mask (
EyePatch). Every mask that is neither empty nor whole is a wedge except the two diagonals, which at four texels read as two stray pixels rather than as a sector. Which texels are iris is worked out from the coat itself — dark texels, bounding-boxed and halved both ways — because the two eyes' faces are mirrored on the sheet and the iris sits at a different offset in each. - Magic sectoral heterochromia (page): six colour alleles plus a wild type, and it expresses only when the two are different. Every other gene in the mod gets easier once you own one good animal; this one does not — breeding gold to gold produces nothing, so its fifteen combinations are fifteen separate hunts. Chaos takes its colour from its own copy's epigenetics, in HSV rather than three raw bytes because a four-texel iris has to stay a colour.
chaos/chaosstill shows nothing: the rule is about alleles, not about what they happen to look like. - Fifteen outcomes, not one. The obvious shape was a single
sectoral-heterochromiaexpression for every expressing pair. That would have been wrong: anExpressionis what the gene looks like, and green-and-gold is not blue-and-brown. Everything downstream reads the outcome list rather than the alleles — the gene dictionary, the wiki's preview widget, the genotype catalogue — so collapsing them would have shown a breeder one button where there are fifteen results. - It broke
variantPair, in both twins. “A gene you just added should show” was implemented as “first non-baseline homozygote”, and every homozygote of the new gene is silent by design — so adding the row in the spawn screen or the designer would have shown nothing. Both now try candidates in order of obviousness (each variant homozygous, then two different variants, then one against the baseline) and take the first that is not a wild type. Behaviour is unchanged for every other gene.CustomHorseSpawnScreenandweb/HorseEditorchanged in the same commit, per the twin rule. EyePatchContributionis the second hook and the reason the magic locus does not fight with the channel: patches are absolute writes over the finished iris, applied in code order, last writer wins — they compose where colour claims compete. It is also why a matched pair or a carrier leaves a champagne horse's champagne eye alone: a non-expressing combination contributes nothing, rather than contributing a wild-type dark eye.- An hour went into a distribution that measured 100/0/0 and was correct (#44).
SeededRngwrapsjava.util.Random, whose firstnextFloat()off sequential seeds spans only about 0.22 to 0.59 — so a test loopingnew SeededRng(seed++)and asking for one float never sees either end of the range. Harmless in play (a real epigenetic seed is a full random long) and lethal in a test. The fix in a test is to pull many rolls from one generator; fixing it properly means mixing the seed insideSeededRng, which moves every horse's coat and wants to be a deliberate pass. - Two things were deliberately not built. Filmy and blind eyes, because cloudiness is cornea and lens rather than iris pigment and belongs with the conditions, not this channel. And champagne foals' blue-green eyes: the hook is not told which skin it is baking, and with aging out of scope a foal that is permanently blue-green may be worse than one that never was. Both on the roadmap.
- A stale count went with it.
SpecGeneTesthard-coded the built-in gene count and had to be edited every time a gene was added; it readsGenes.codeOrder().size()before registering now. Same for the golden test's “20 genotypes” on the pipeline page.
Built 2026-09-06, NOT yet opened in a browser: gene pages show the gene, and how to make its carrot.
Two drop-in widgets, each one <div> and one <script> on a page, both running the same wasm the horse designer does. :common:test green, :neoforge-26.1.2:build green, parity 3868/48, coat-golden.txt untouched. Three commits, in that order.
- The preview window (
wiki/gene-preview/): this gene on a black, a bay and a chestnut, as a horse you can spin, with a dice that rerolls the epigenetics. Almost none of it is new code. The coat iscommon/compiled to wasm - the bytes the game bakes - the horse is the gene creator's ownmodel3d.jsover the game's geometry tables, and it is shown in the creator's ownviewport.js, which gained a backdrop option, a grid option and aresetViewand is otherwise unchanged. What this file adds is three buttons, a dice and the wiring, which is the only part that is about previewing a gene. - Nothing about a gene is written into a page. The window takes a gene key and asks the registry for the rest: display name, one button per outcome that looks different (
GenotypeCatalog.distinctPairsOf), and anyConditionthe genotype resolves. Tobiano gets one outcome becauseTo/toandTo/Topaint the same;KITgets seven;EDNRB'sO/Oshows its lethal-white chip with nobody listing it. A gene that gains an allele gains a button and no page is edited; a stale key says so instead of drawing a plain horse. BaseCoatsis the new common class: black, bay and chestnut as genotypes, built from theAlleleobjects rather than parsed from tokens, so a renamed allele breaks the compile.DesignerApigrew a stateless half -coatOf,traitsOfJson,previewGenotypeCode,withGene,newEpigenomeCode- that takes a genotype and hands back a coat without touching the editor. Two callers sharing one mutable editor is how a wiki page ends up showing the designer's last horse.- The gene-carrot recipe card (
wiki/gene-carrot/), now on every gene page: the 3×3 grid inmenu/SpliceRecipeDisplay's slot order, the arrow, the carrot that comes out, and what feeding it does. There is one parameterised recipe rather than N generated ones, so what differs per gene is exactly four things - carrot or no carrot, the rarity tier that prices it, heterozygous or homozygous, and whether the Unknown splice may land on the locus - and all four are asked of the mod. A tier written into fifty pages would be a wrong price the first time a gene was re-tiered, and wrong silently. - Two things about that recipe are not per-gene and are held in the card: the four-slot layout, canonically
SpliceRecipeDisplay, and the tier→item table, canonicallyserver/recipe/RarityItems, which lives on the recipe side wherecommon/cannot see it.carrots.htmlnames both authorities, andCLAUDE.mdnow carries it as a third multi-file contract. - No item icons on the card, on purpose. The vanilla textures are Mojang's and not this repo's to ship, and half a grid of real icons beside half a grid of text reads worse than text throughout. The two slots that are this mod's own items are tinted instead, which makes the house rule - every recipe has a modded ingredient - visible rather than stated.
- Which pages get a preview is the designer's own answer, not a list.
DesignerApi.showsAsalready classifies every gene, so the window went on the ones that saycoatand the ability, stat, size and condition loci got none. A window that draws a plain horse under a gene's name is worse than no window. - The leopard complex forced a real addition. Its own
expressionOfsees one pair and answers varnish roan for everyLPhorse; the pattern you actually get is one of eight and turns onPATN1andPATN2, which it names incoatDependsOn(). A window offering the leopard locus alone would have shown one corner of that table and called it the gene. SogenePreviewJsonreports the modifier loci a gene declares it reads, with every combination each can take, and the window renders a button row per locus. Walked offline it reaches all seven distinct looks in that page's eight-cell table. Two consequences of the same fact: a gene that reads modifiers lists its own combinations rather than collapsing them by expression (collapsing was discardingLP/lp, half the table), andwithGeneexists so the page can compose three loci without ever assembling a code itself. PATN1andPATN2have no pages - they are written up on the leopard page - so they get no window, and are reachable through leopard's instead.java.jsstopped being the designer's. It derives its wasm and asset paths from its ownscript.srcrather than from the document, because the includer is now a different page every time, andHG.java.load()is memoised so a page carrying both widgets loads one wasm rather than two.- What was checked, since none of it has been looked at. The baked wasm was driven from node with the four PNGs decoded and handed in exactly as the page hands them: the three bases build the genotypes they claim, every coat gene's outcomes and modifier options resolve and paint something different from the base coat, the patches move on a dice roll and not when the base changes, bad keys and bad allele tokens refuse rather than falling back, and every gene page's
data-generesolves to a registered gene. That is the logic. Everything visual is unlooked-at -wiki/verification.html§0-J and §0-K. - A handful of genes are correctly invisible on one base, and it wants to read as informative rather than broken: mushroom dilutes red and cannot touch a black, dun's markings-only allele draws a black stripe on a black horse, and agouti and extension are invisible on the base coats that are made of them. That is what three bases are for.
- It turned up a real gap (#43): no built-in gene overrides
Gene.rarity(), so all six tiers are unpopulated, every carrot costs a gold ingot and the research-paper loot weighting that sorts on the same axis is flat. Nothing is broken - the axis is simply unused - and it was invisible until each gene's tier started appearing on its own page. Tiering wants doing as one pass over the registry, because the tiers are relative to each other.
Follow-ups 2026-09-06 on the designer, from the owner's first look.
Seven items; :common:test 439 green, :neoforge-26.1.2:build green, parity 3868/48.
- The "3D mesh disagrees with the game" error was my JSON writer, not the mesh.
Json.val(double)printed the whole part withscaled / 10000L, and integer division truncates toward zero - so every value between -1 and 0 lost its minus sign, and the live geometry check read a body-spacezof-0.75back as0.75. The JS port and the Java agree on every probed texel (262 adult, 133 foal), and did all along. The lesson is the ordinary one about a checker being code too: a false alarm from a watchdog costs the watchdog its credibility, so the fix carries a comment naming the trap. - The horse got stuck spinning, and it was the same bug as "make it come back to the block". Past
FIELD_RADIUSthe oldheadHome()fired again every time the state timer expired, so the horse re-enteredturnforever andgaitnever rose. Rewritten: there is no fence and no fixed-duration spin. A heading is chosen with a spread that narrows with distance from the block - free to go anywhere beside it, straight back at the edge - and turning is a rate toward that heading which ends when it arrives. So a turn always resolves into a walk, and the horse circles the reference block instead of leaving. - The ground was radioactive. The grass PNG is a bright top-face texture authored to be read under Minecraft's own baked lighting; under a hemisphere plus a key light it glowed.
GROUND_TINTmultiplies it down. Darkening the material rather than the lights was the point - the horse was lit correctly and had to stay that way. No new texture needed. - Every horse has a name now, rolled from the real
HorseNameGenerator. The word tables come in from the page like the textures do, becausegetResourceAsStreamis TeaVM's weakest spot and the generator has a public constructor taking the lists. The name is the header, in two halves, and each half is its own button - so "reroll either half" costs no widget in the column; Reroll name does both. A breed pick or a Randomize renames, since that is a different horse. - Export JSON (
DesignerApi.horseJson) writes the horse as a file: the two code strings, which are the whole horse, plus name/sex/breed and areadableblock that is explicitly for a person, not for a loader - a loader must re-resolve traits or a re-tuned gene could never reach a saved horse. Nothing in game reads it yet; that is the newwiki/roadmap.html#horse-files, which argues for an Import horse file button beside the spawn egg's existing Paste code (a Minecraft screen has no file picker, so the clipboard is the route). - A vanilla-proportioned player stands beside the reference block - 1.875 blocks against the cube's 1.0 and an adult horse's 2.11 to the ear tip. Untextured on purpose: the default skin is Mojang's asset, not this repo's to ship, so it is flat colours in roughly its palette. A ruler, not a character.
- The gene list background went from
0x88to0xE0(owner request) - and the same change went intoCustomHorseSpawnScreen, because the two are twins and the names sit over a busy world in both. - Genes the page cannot draw are struck through, and clicking one toasts (owner request). Which genes those are is derived, never typed -
DesignerApi.showsAsasks each gene whether it paints, whether any pair moves its scale, whether any pair declares aCondition, whether it is aCutieMarkGeneproducing aMark, and whether another gene names it incoatDependsOn(). That last check is what keepsPATN1/PATN2out of the struck list: they paint nothing themselves but the leopard complex folds them in, so a horse carrying them does look different. Verdicts: coat 26, size 6, condition 3 (all shown) against ability 4 (cutie mark, particle, milk, verdant - item icons, particle types, world blocks) and stats 8 (CKM, MET, MSTN, PDK4, RYR2, magic speed/health/jump). A struck gene is still added when clicked - it is real and belongs in an exported horse - it just says you will not see it. This is gap #39's lesson applied before the fact: a hand-written list would have been wrong the first time a gene was added. - Import JSON joined Export, so the page round-trips its own format. The JSON is parsed in JavaScript (a browser format, free there) but every field is applied through Java -
pasteCode,setEpigenomeCode,setName,setBaby,setSex,setBreedByName- so nothing genetic is decided in JS. It is deliberately tolerant: an unknown gene drops, a missing gene reads as its wild type, an unrecognised breed loses only its label, and each is reported rather than refused.stampBreedis notsetBreed: picking a breed from the dropdown is a request for a new horse, importing one is not, so the import stamps the label without rolling. Verified by exporting a Falabella, wiping genes/name/sex/epigenome/breed, importing it back, and getting an identical genotype, epigenome, name, sex, breed, short form, scale and epigenetic fingerprint. - A second TeaVM gap, found by that work:
Long.parseUnsignedLong(s, 16)is not in TeaVM's class library, andEpigenome.parseneeds it. It had compiled fine until now only because the wasm build compiles the reachable graph - nothing had calledparsebefore, so it was eliminated. Replaced with a written-outparseUnsignedHexinEpigenome(the shift is what makes it unsigned; a seed uses the full 64 bits). Same shape as theSystem.getLoggerfix, and the same side benefit for the Java 8 backport. The lesson worth keeping: adding an export can surface a gap that was always there, so a new@JSExportdeserves a build, not an assumption. - The scale figure's hair was z-fighting its scalp - the hair box's top face sat at exactly the head's top, two different colours competing for one plane. It now wraps the scalp: wider on every side and 0.2 proud on top, with its underside buried inside the skull. Same trick as the horse mane's deliberate
z = 5.01. The limb joints are left butted, which is vanilla's own arrangement - sinking them turned out worse, exposing coplanar slivers down the sides. - Toasts dismiss themselves after 30 s, and hovering one holds it.
- The roadmap was renumbered by the insertion (1-23). Numbers there are explicitly not stable; link by anchor.
Built 2026-09-06, NOT yet opened in a browser: the horse designer runs the mod itself.
wiki/horse-designer/ is now the browser twin of the custom horse spawn egg, and it runs common/ compiled to WebAssembly by TeaVM - not a port of it, the actual bytecode - so all 48 genes are live and the coat is byte-identical to the game's. :common:test 439 green, :neoforge-26.1.2:build green, creator parity 3868/48, coat-golden.txt untouched. Six pieces:
- Why compiling and not porting. The page is on GitHub Pages, which is static, so there is no server to run the pipeline on; and the owner's rule is that a subset of pre-baked coats is not acceptable, because the premise of the mod is a functionally infinite space (
GenotypeCatalog.size()is 11 098 128 386). That leaves compiling. The spike is in the previous entry: TeaVM swallowed all 151 files, and the same class run on the JVM, as JavaScript and as wasm gives twenty identical coat digests. Use the wasm backend:BodyNoise's hash is 64-bit integer maths, which JavaScript emulates with 32-bit pairs at ~100x cost (dapple grey 521 ms vs the JVM's 7); in wasm alongis a native i64 and it drops to 4 ms. - The UI is a deliberate redraw of
CustomHorseSpawnScreen(owner request): same gene list alphabetically by display name with the sex locus absent, same click-a-row-to-add, same two allele buttons andx, same dropdown past three alleles, same right column in the same order (Age, Sex, Breed, Randomize, Reroll epi., Copy code, Paste code, Clear genes), same green-when-expressing row with its outcome underneath, same short-form line and epigenetic fingerprint below the list, samesize 1.14xreadout under the preview. Every layout constant and colour injs/gui.jsis copied from the Java by name and value, which is why it is drawn on a canvas rather than built from DOM widgets - the two layouts can be diffed.- The keep-in-step note the owner asked for is in three places: the class javadoc of
CustomHorseSpawnScreen("This screen has a twin - change both"), the top ofweb/HorseEditor.java, and the top ofjs/gui.js. Plus the convention below. - Two deliberate divergences: there is nothing to spawn in a browser, so Spawn / Cancel are Wander / Reset view; and cutie marks and particle emitters cannot be drawn, because both come out of the game's own registries.
- The keep-in-step note the owner asked for is in three places: the class javadoc of
- The model is Java, not JavaScript, and that is the whole trick.
web/HorseEditor.javais a twin of the screen's state machine -variantPair,enforceSexLinkage,applyGenome,randomize,paste, all under the same names. So "keep them in step" is a comparison between two Java files in the same language, and the JavaScript contains no genetics at all: it draws, and forwards clicks as a method name and some ints.web/DesignerApi.javais the@JSExportfacade; structured answers cross as JSON, coats cross asint[](which arrives as anInt32Array). - Nothing is read from a resource. The page decodes the four real PNGs and hands the pixels in, which sidesteps TeaVM's known weak spot and is the better design anyway. The gradient is the real 500x500 chart, not the creator's 256px downsample - the creator only has to look right, this has to be exact.
lutbluepink.pnggoes in too, keyed asLutContribution.lutResources()keys it, so the LUT locus resolves properly. - A new
:webGradle module withbakeDesignerAssets, which copies the wasm, its loader and the four PNGs intowiki/horse-designer/- checked in and regenerated, the same arrangement astextures.jsandexpected.json. The page cannot run from afile://path: loading wasm is a fetch and a local file is its own opaque origin. Serve the repo (python -m http.server) or use the published wiki. The gene creator is unaffected and still opens from disk. - The last JS port on the page is guarded live. The 3D mesh is still built by
geometry.js+model3d.js(a mesh builder is view code), soDesignerApi.geometryProbeJsonwalks 262 texels through the realHorseSkinGeometryandjs/java.jscompares them on every load - a drifted port is now visible in the tool rather than only at a terminal someone forgot to open. That is known gap #13's failure mode, closed for this page. - What went away with the rewrite: the six-gene JavaScript base-coat fallback, and the opening toast that had to admit it. The honest limits are now three Minecraft-side things - cutie-mark item icons, particle emitters, emissive texels - and nothing genetic.
- Also fixed, from the owner's first look: the
hiddenattribute does nothing against an authordisplay:rule (author origin beats the UA stylesheet regardless of specificity), so.nowebgl'sdisplay: gridkept a full-window "No WebGL" panel painted over a perfectly good horse on every machine.[hidden] { display: none !important; }is in both stylesheets now, with a comment saying why it must stay. - Docs:
wiki/verification.html§0-I (rewritten),CustomHorseSpawnScreenjavadoc,wiki/nav.js,index.html.
Built 2026-09-06, NOT yet looked at in a browser: wiki/horse-designer/ - a second browser tool that stands a horse in a grass field; plus the vanilla model tables folded into wiki/body-space.html and the loose javahorsemodelinfo.md deleted.
No Java changed: :common:test 439 green, :neoforge-26.1.2:build green, creator parity 3868/48, coat-golden.txt untouched. Four pieces:
- The docs half.
javahorsemodelinfo.mdsat at the repo root giving "the Java horse model" as a4x9x4upper leg +3x5x3shin +4x3x4hoof. That is the pre-1.13ModelHorse; vanilla 26.1.2 uses one4x11x4box per leg, and following the doc is what nearly made the gene creator's preview disagree with the game about which texel is a hoof (the 2026-09-06 rebuild caught it). So the content is now awiki/body-space.htmlsection - the two coordinate systems and the flip between them, the full 26.1.2 part table read offHdHorseModel(pivot / origin / size /texOffs/ rest pitch), the hierarchy, why the mane sits atz = 5.01, the rest-pose-AABB caveat with its measured ratios, the atlas-not-a-wrap point, and the model-is-not-the-hitbox point - with the leg correction as adangerbox rather than a footnote. The markdown file is deleted; the two markdown files in the repo areREADME.mdandCLAUDE.mdagain. Fixed in passing on the same page: a stale note sayingwhitenBlaze"is the only face marking there is", which shipped a day before the face-marking family did. - The designer runs the mod's own coat pipeline, and it does it by sharing the creator's files rather than copying them.
wiki/horse-designer/index.htmlpulls../gene-creator/js/{geometry,noise,fields,schema,spec-engine, base-coats,model3d,preview}.jsand../gene-creator/assets/textures.jswith plain<script src>. That is the load-bearing decision: a second copy of the pipeline is a second thing to keep in step with the Java, and known gap #13's whole lesson is that the second copy silently rots. Only three files are the designer's own -scene.js(world + camera),animation.js(the gait) anddesigner.js(the panel). - Three additive changes to the shared code, each verified not to move the creator:
model3d.buildParts- the same quads asbuild, split per part and measured from each part's own pivot, so a leg can be swung. Both callers now go through oneemitPart. A model-space pitch is a body-space z rotation, negated (model y and z both run backwards against body y and x); the derivation is in the javadoc. Checked:buildPartsis byte-identical tobuildin every vertex position and UV, 288 vertices adult and 240 foal, max delta0.00e+0.baseCoats.compose(skin, field, config)- the base coats as a per-locus config (extension/agouti{leg,face}/dilution/grey{4 knobs}/white) run innaturalOrder(), with the 15 presets rewritten as configs on top of it. So the creator's "Bay" and the designer's "Bay" are one code path. Checked: all 30 preset bakes (15 x adult/foal) are byte-identical to the builders they replaced.preview.baketoleratesspec: nulland takes abaseCoatConfig. The designer's ordinary case is no gene under test - it is looking at genes the mod already has - which the creator's bake could not express.
- It renders size honestly, which is why the world is measured in blocks. One scene unit is one block, the grass tiles one tile per block, and there is a one-block reference cube beside the horse. The rig scales model units by
sizeScale/16and nothing else - no normalising scale hidden in the mesh builder, which is exactly whatmodel3d.builddoes for the creator and would have made the size slider meaningless here. Adult reads 2.11 blocks hoof-to-ear-tip, foal 1.60. The slider spans 0.1-3 and names the natural clamp (0.45-1.75) so a magical size is visibly outside it. - The gait is an approximation and says so. Vanilla's swing is
AbstractEquineModel.setupAnimdriven bywalkAnimationPos/walkAnimationSpeed, neither of which exists outside Minecraft, soanimation.jsis a hand-tuned diagonal-pair swing on the real pivots. The one thing it borrows deliberately is that phase advances with distance, not time, and stride scales with the horse - which is whatGeneticHorseRenderer.stretchGaitToSizebuys in game, and what stops the feet skating when speed or size changes. A toast says all of this on open. - What it cannot show, it says out loud. Toasts, dismissible, upper right: only six genes exist in the JS port (extension, agouti, MATP, champagne, grey,
KITdominant white) against 48 built-ins;effectsare not simulated, and a loaded gene's verbs are named back at you; emissive /glowtexels are not drawn; particles, cutie-mark item icons and eye-colour tinting have no port. A "Show the full list" button prints the lot. The alternative - a tool that silently omits half a horse - teaches the wrong thing confidently, which is this repo's recurring failure mode. - Verified offline, in node, against the real ported code (there is no headless browser here): the two byte-identity checks above, plus an orthographic render of the posed rig textured with a real bake, which comes out a recognisable bay (black points, mane, tail, muzzle), a palomino foal (no mane - the foal mesh has no
MANEpart), a dapple grey, and Waterborn with its blue mane and tail streaks. Same technique the creator's rebuild used. What that cannot cover is three.js, the CSS, and every input - hencewiki/verification.html§0-I. minecraftgrass.jpgmoved towiki/horse-designer/assets/grass.jpgand is inlined toassets/grass.jsby a newtools/bake-grass.mjs- the samefile://constraint that makes the creator inline its templates. The.jpgstays checked in as the source;grass.jsis generated.- Docs:
wiki/body-space.html(the new section + one stale note fixed),wiki/verification.html§0-I (new),wiki/nav.js,index.html.
Fixed 2026-09-06: the gene creator was dead on open, drawing bounding boxes instead of a horse, and had been previewing every coat with the spine and belly texture patches swapped.
:common:test 439 green (+3 CreatorMetadataRoundTripTest), :neoforge-26.1.2:build green, parity 3868/48 (up from 3832 - the effect vocabulary is checked now too). No game code changed; coat-golden.txt untouched. Six pieces:
- It threw on load.
js/preview.jsexportedexpresses: expressesand there is no such function - a leftover from the combination-table rewrite (080054a) that deleted it and left the export line behind. AReferenceErrorat script-evaluation time killed the wholeHG.previewmodule, soHG.ui.start()then died onHG.preview.loadand the page rendered nothing. One dead line; every other symptom the owner saw was downstream of it. - The JS geometry port had missed the 2026-09-05 top/bottom UV swap, and the parity net could not see it because the fixtures were stale too.
HorseSkinGeometry.faceMapsOfhadFace.TOPandFace.BOTTOMexchanged and was fixed in Java that day;wiki/gene-creator/js/geometry.jswas not, andfixtures/expected.jsonwas never re-baked.expected.jsonis a checked-in snapshot of the Java, so a stale one makescheck-parity.mjsgreen by definition - it was comparing the JS against a record of the Java as it used to be. Re-baking turned up 313 mismatches, i.e. the creator had been drawing every horse with its spine and belly patches swapped since 2026-09-05, confidently. Swapping the two JS entries restores 3832/48 green. This is known gap #13's failure mode with a new twist: not a hand-written number that rotted, but a generated one nobody regenerated. The lesson is in "Build & test": re-baking is part of the check, not a chore beside it. - The 3D preview drew each part's rest-pose AABB, not the part.
HorseSkinGeometrystores every part as an axis-aligned bounding box, because the coat pipeline only ever projects texels onto a box and never asks what shape the horse is. For a part with a pitch that box is far bigger than the part: measured, the mane came out 4.5x its true volume, the tail 2.6x, the neck 2.0x, the head and muzzle ~1.9x. So the preview was a pile of oversized blocks rather than a horse.js/model3d.jsnow walks the raw cuboid (origin + size, rotated about the pivot) exactly asHdHorseModelposes it and only then flips into body space, via a new additivegeometry.toBody(skin, mx, my, mz)+mesh.modelMax(no computed value moves, so parity is untouched). Verified two ways: an offline round-trip that takes each face's UV centre back throughgeo.sampleand gets the right part on all 72 adult and 60 foal quads, and an orthographic render of the result, which reads as a horse.- The model doc's three-box leg is the classic model and was not followed.
javahorsemodelinfo.mdgives upper4x9x4+ shin3x5x3+ hoof4x3x4; vanilla 26.1.2 - and soHdHorseModel, which copies itstexOffsand box numbers verbatim - uses one4x11x4box per leg. Drawing the trio would have made the preview disagree with the game, which is the one thing this tool must never do. Everything else the doc gives (the 30-degree head pitch, the pivots, the mane's5.01offset, the tail's stepped hang) is what the rewrite restores. - The camera is framed from the built model rather than fixed, so an adult and a foal are both in shot and both stand on the grid.
- The model doc's three-box leg is the classic model and was not followed.
- Typing was one character per click. Every edit calls
changed(), which rebuilds both panels from scratch and destroys the focused input. Rather than make the render incremental,focusPath/restoreFocusremember the caret by its position in the panel's own tree and put it back - a text edit does not change that tree's shape, so the path still names the same field. - The format-3 metadata has forms now (roadmap
#designer§9.1):blurb,rarity(the sixGeneRaritytiers), thecarrotblock (enabled, one-copy / two-copies, flavour item ids) and an optionalsplicetable seeded from the founder shares.tidy()drops anything still at its default, so a gene that ignores all of it still exports a short file, andretokencarries the splice table through an allele rename exactly as it carries founders - it is keyed by combination too, and orphaning it would only show up as a load error. - The
effectsblock has an editor (same §9.1): one card per effect, all eight verbs, every parameter, the four trigger shapes,minDose, and awhenbuilt from the condition flags.js/schema.jsgained anEFFECTStable mirroringAbilityType, andSpecFixtureToolnow bakes the ability vocabulary into the fixtures so parity compares it - kinds, defaults, choices, required-ness and the flag list - which is the only net effects have, since they do not paint and the probe cases say nothing about them. Thewhenform covers a flatall/anyof optionally-negated flags; a nested condition is preserved byte-for-byte and shown read-only rather than silently flattened. - The parity check runs in the page.
js/parity.jsis now the single implementation and both callers run it:check-parity.mjs(which lost its duplicate copy of the logic) and the creator itself, on boot, printing the verdict under the coat sheet.SpecFixtureToolalso emitsfixtures/expected.js- the same fixtures as a classic script, because afile://page cannot fetch the.jsonbeside it, the same reason the textures and examples are inlined. - Two things the parity gate structurally cannot cover are covered by a new
CreatorMetadataRoundTripTestinstead: the metadata and theeffectsblock never paint, so they are exported bytools/bake-export-fixtures.mjsand parsed by the realGeneSpecParser. It asserts the metadata reads back as typed, that re-exporting Waterborn through the creator leaves its abilities equal (the export is textually shorter - every dropped key equals a game default - so a string comparison would be the wrong test), and that a gene using every verb parses and that the verb count still equalsAbilityType.all().size(). - Docs:
wiki/roadmap.html§9.1 (all three items deleted, one honest remainder left),wiki/gene-effects.html+wiki/gene-format.html(the creator writes these now),wiki/verification.html§0-H (new - and it is a browser checklist, since none of this has been looked at).
Fixed 2026-09-06, from the first play session against the sex-linked + eye-colour + white-recalibration work: one eye rendered backwards, and the custom spawn egg could build a Brn/Brn stallion.
:common:test 436 green, :neoforge-26.1.2:build green, creator parity 3832/48. Two independent bugs, both owner-caught inspecting the new eye-colour work:
- The adult eye-colour rect was two texels short, and that flipped one eye.
CoatRegions.EYE_RECTS_ADULT's east-face rect was{28, 42, 4, 2}; the real pupil+sclera pixels sit at{30, 42, 4, 2}- the declared rect grabbed 2 background texels plus the pupil and missed the sclera entirely. The template's raw column order is deliberately mirrored between the two eyes (white-then-black on the west face, black-then-white on the east - inherited unmodified from vanilla's ownhorse.png), which is what makes the pupil land nose-side on both faces once the standard Minecraft box-UV unwrap reverses one face's U-axis relative to the other. Cropping two texels short broke that compensation: the west eye rendered correctly (full 4-texel match) while the east one, missing its true white half, effectively read with the wrong raw order and rendered backwards- pupil toward the ear instead of the nose. Fixed by moving the rect to
{30, 42, 4, 2}; the JS parity port (wiki/gene-creator/js/fields.js) moved with it (node .../check-parity.mjsstill 3832/48). This also feedsCoatOverlay.tintIris(the eye-colour channel reads the sameCoatRegions.eyeRects), so the blue/amber tinting was landing on the wrong 2 of the 4 east-eye texels too.coat-golden.txtregenerated (every row's eye texels moved). Not yet re-verified in-game.
- pupil toward the ear instead of the nose. Fixed by moving the rect to
- The custom horse spawn egg let a stallion carry two real copies of an
X-linked gene (aBrn/Brnstallion, which cannot exist - a stallion has oneX).CustomHorseSpawnScreenbuilt every row's pair from two freely-cycled allele slots with no notion of sex or inheritance mode. NewCustomHorseSpawnScreen.enforceSexLinkage(Row)readsGene.inheritance().copiesIn(sex)and snaps a sex-linked row back into shape - a stallion's second slot is forced to the gene'shemizygousPlaceholder(), a mare's placeholder slot is replaced with a real allele - called after every allele-button/dropdown edit, after adding a row, after the Sex: Mare/Stallion toggle, and after a clipboard paste. Brindle is the onlyX-linked gene today, so this is currently a one-gene fix, but it is written generically offGene.inheritance()so a futureY-linked gene needs no screen change. Not yet re-verified in-game. - Two more items from the same play session are confirmed and closed - see the Owner-verified in-game (2026-09-06) block below.
Built 2026-09-06, NOT yet play-tested: sex-linked inheritance + brindle, the eye-colour channel + tiger eye, a cutie-mark modifier hook, the splice blacklist, the Feral Mixed rename, and the white-belly recalibration.
:common:test 436 green (+20 BrindleGeneTest, +11 EyeColorTest, +6 SpliceSafetyTest, +4 CutieMarkGeneTest), :neoforge-26.1.2:build green, creator parity 3832/48 untouched, runServer boots clean (50 segments, 48 built-in + the 2 shipped spec genes). Old saves will not parse (50 vs 48). Seven pieces:
- The white climbing the belly is fixed (
wiki/roadmap.html#defectsdefect 1, the most-seen defect in the mod). Both painters inWhitePatternmeasured againstbodyBounds- the whole-horse AABB, hoof to ear tip - on which the legs reach only 0.326 and the belly 0.326. So a splash waterline written as "0.35 of the horse" landed at 11.8 units, above the underline: four white legs and white onto the barrel, for what the gene calls a minimal marking. WithPAX3'sSW2on 90% of founders that was most horses alive. NewWhitePattern.toplineHeight(skin)=bounds(skin, BODY).yMax()(20.99 of 33.75 on the adult), and every vertical constant re-expressed against it, so the fractions mean something anatomical (0.05 coronet, 0.10 fetlock, 0.26 knee, 0.52 underline, 1.0 spine).- The splash ramp is convex,
s^SPLASH_GAMMA(1.7) off a coronet floor of 0.035, which puts each outcome where its owndescribe()says it should be. Measured, single-copySW2/N: 14% overall, 41% legs, 0% belly, 0% back (it was a dipped horse); bold 41% / 95% legs / 31% belly ("past the elbow"); extensive 69% / 100% / 100% / 11% back ("almost everything below the topline"). A plain square was tried first and put bold below the elbow, contradicting its own prose. - Sabino's belly patch is anchored at the underline (
BELLY_ANCHOR0.44 +BELLY_REACH0.42) rather than at the ground - it is the belly spot alone, the leg white being drawn per leg.SB1reaches 0.62, sabino-white stops at 0.83, so a near-white sabino keeps a coloured topline. - "White finds white" had to change with it. The stacking bonus is a linear read of
alreadyWhite, and once the ramp is convex a smallsaddition buys almost no height - measured,SW1/N + SW2/Ncame out 0.156 against 0.136 alone, i.e. the two-locus split had become invisible, which is the exact thing it exists to show. Splash now reads the signal through a saturating curve,w / (w + STACKING_HALF)(0.10), because the two cases differ by 3x in coverage and should differ far less in response: a second splash locus arrives at ~14% white and must reach belly-deep, while a splash allele on a tobiano arrives at 45-70% and must not take the rest. Sabino keeps the plain linear read - its coverage did not move. - Still open, same root cause:
EdnrbGene'sBAND_LO/BAND_HIand everycoverknob are still whole-horse fractions (gap #30).
- The splash ramp is convex,
- Sex-linked inheritance, and brindle as its proof. New
common/genetics/Inheritance(AUTOSOMAL/X_LINKED/Y_LINKED) and fiveGenedefaults:inheritance(),hemizygousPlaceholder(),isPlaceholder(),realAlleles(pair),sexConsistent(pair).- The genotype does not change shape. A hemizygous horse fills the slot it does not have with a reserved placeholder allele the gene declares (
Yon an X-linked gene, meaning "this locus is not on the Y"). SoGenotypeis untouched,Epigenomealignment is untouched, the code string keeps its shape, and parsing needs no special case - the placeholder is a declared allele like any other. - The placeholder must be declared at the right end of
alleles()(Ylast,Xfirst), becauseAllelePaircanonicalises on declaration order.Genes.register->validateInheritancerefuses one that is not, and it runs fromrebuild()so it covers built-ins too. The failure it catches is not an exception: a mis-declared placeholder sorts into slot 0, so the horse's real allele lands where the epigenome and everyexpressionOftreat it as the second copy, and the gene misbehaves for exactly one sex. breedWithgrew two cases. The sire's copy is decided by the foal's own sex rather than drawn - sex is priority 1, so it is always resolved first. The second coin is still flipped and thrown away: two booleans per gene is an invariant the golden coats,GameteBiasTestand every "adding a gene shifts the stream by N" claim lean on, and a locus that quietly consumed one would make the random stream depend on a foal's sex. Which parent is the dam is read off the parents' own sex loci, sincebreedWithis otherwise symmetric.BrindleGene(horsegenetics.brindle,MBTPS2, priority 36 with the dilutions, natural, non-deterministic).Brn/n/Y, 6 combinations of which 5 occur, 3 outcomes.expressionOfcountsrealAlleles- "every real copy isBrn" covers recessive in mares and always shows in stallions in one sentence, no special case. The founder table is sex-aware: a stallion's is written out (Brn/Y2%,n/Y98%) because Hardy-Weinberg is a statement about diploid loci and this one is not.- Measured over 4 000 foals per pairing: brindle stallion x plain mare -> 0% brindle colts, 100% carrier fillies; carrier mare x plain stallion -> 50% / 0% / 50%; brindle mare x plain stallion -> 100% brindle colts; carrier mare x brindle stallion -> 50% / 50%. Over 200 000 founders: 2.000% of stallions, 0.049% of mares - 40:1.
- It paints a banded dilution, not a second colour - which is what makes it expressible as a natural gene at all, phase 1 being downward-only.
BodyStripes, shared with magic zebra, so it gets the chevron slant free. Part weights barrel 1.0 / neck 0.55 / legs 0.40 / tail 0.30 / head 0. Retuned once: the first constants ran a warp of 1.6-3.0 against a spacing of 1.5-2.8, so adjacent stripes bent into each other and it baked out as convincing wood grain. The warp has to stay well under half the spacing. Y_LINKEDis built and has no gene. The mirror case works and is waiting for one.
- The genotype does not change shape. A hemizygous horse fills the slot it does not have with a reserved placeholder allele the gene declares (
- Eye colour is a channel -
common/genetics/EyeColor(a ranked claim) +EyeColorContribution, resolved once by the composer at the top of the overlay phase and applied with a newCoatOverlay.tintIris.- The iris is the DARK texels, not the sclera (owner's rule, and it is what the template actually holds: an adult eye is a 2x2 block of pure black beside a 2x2 near-white block; a foal's is the black block alone).
tintIrisweights by1 - luma. The obvious tool is exactly backwards:shadeTowardweights by brightness so a gold hoof keeps its shading, and pointed at an eye it colours the sclera and leaves the iris black. It stays right forlight's glowing eye and the leopard complex's white rim. - Ranked, not blended, because the claims are different kinds: a pigment gene says what colour the iris is, a white locus says there is no pigment in it.
RANK_DEPIGMENTED(20) >RANK_PIGMENT(10) - blue beats amber, because a depigmented iris has nothing left to recolour. - The white loci claim blue, and it is not a new gene.
MITF/PAX3claim on any expressing combination (splash is diagnostic even when the white is modest);EDNRBlikewise;KITonly frombroad-whiteup - a plain sabino has dark eyes. Plus a shared fallback inWhitePatternEyes: past 55% resolved coverage the eyes follow however the horse got there, which is the case no per-locus test can see (two mild alleles stacking). The composer measures that from the resolved pigment field and passes it to every claimant. TigerEyeGene(horsegenetics.tiger_eye,SLC24A5, priority 60, natural, deterministic).TE1/TE2/N, recessive;TE1-bearing -> amber#C8811E,TE2/TE2-> yellow#D6B341. Its expressing outcomes areExpression.marker()s, not wild types - the eyes are baked into the texture, so it belongs incoatCode(); being deterministic it costs the cache one entry per outcome, not one per horse. Wired into the Puerto Rican Paso Fino (26% carriers, ~4% expressing) against ~1 wild horse in 5 000. The breed reference filed tiger eye under coat patterns; it is not one.
- The iris is the DARK texels, not the sclera (owner's rule, and it is what the template actually holds: an adult eye is a 2x2 block of pure black beside a 2x2 near-white block; a foal's is the black block alone).
- Cutie mark is a modifiable channel (owner request). New
common/genetics/CutieMarkContribution;CutieMarkGene.markFordraws the base emblem then folds every implementor over it incodeOrder().Markgainedemissiveand sixwith*helpers.- It can afford an open hook precisely because the emblem is the last thing drawn - nothing composes over it, nothing reads it back, and it is not in the baked coat at all, so a modifier cannot corrupt an accumulator, surprise a later painter or move a texture key. That is the argument the cross-locus discouragement elsewhere turns on.
- The fold is deliberately NOT gated on
expressionIn(...).wildType(): most of the magical genes that would want to reach the mark - particle, milk, the body-stat loci - paint nothing and so declare every outcome a wild type, and that filter would exclude exactly the genes the hook is for. The implementor is handed its own pair and decides. LightGeneis the first user: any variant copy makes the mark glow, andCutieMarkLayerdraws it atFULL_BRIGHT. It is the case that makes the hook obviously right rather than merely general - a horse with four burning gold hooves wearing the one dull thing on its body reads as a bug.- A
tintfield was considered and dropped:ItemStackRenderState.submithas no per-instance colour, so a tint would need a custom render type. The standing rule is that everyMarkfield is one the layer honours - a field nothing draws is a promise the game does not keep.
- The Unknown Gene Splice carrot has a blacklist (owner request), and it is derived, not typed -
common/genetics/SpliceSafety. A hand-written list of gene keys is wrong the day someone adds a gene and forgets it, and wrong silently: the failure is a dead foal in someone's world, not a build error. So every combination the carrot could roll is resolved throughHorseTraitson an otherwise wild-type horse, and the locus is dropped if any produces a condition worse thanINFORMATIONALor leaves the horse short of baseline health.- Hearts are the line, exactly as asked: a slower horse is not a damaged one, so
HMGA2's pony allele and magic speed'sSluggishstay in- a surprise you did not want is what the carrot is for.
- It evaluates ALL combinations, including
!canOccurones, and that is the subtlety:METandKITboth declarecanOccurfalse for a homozygote, butmet/metis a real lethal genotype the model refuses to create (and declares a lethalCondition) whileW22/W22simply never existed (and declares nothing). Reading conditions over all combinations drops MET - whose allele would silently poison a breeding line - and keeps sabino and dominant white in the carrot. - Drops 13 of 48: sex, the seven disorder loci,
METandMILK(both embryonic lethals),EDNRB,SILVER,MSTNand magic health. None is named in the file.Gene.spliceable()is the manual override for harm resolution cannot see. The Known Gene Splice carrot is deliberately unfiltered - the player names the gene, and roadmap#settlednow records that a player may splice into a lethal genotype on purpose.
- Hearts are the line, exactly as asked: a slower horse is not a damaged one, so
Breeds.UNKNOWN->Breeds.FERAL_MIXED, and it is absorbing (owner request). Tokenunknown->feral_mixed, display "Feral Mixed",BreedLineage.Kind.UNKNOWN->FERAL,getOrUnknown->getOrFeral. Every cross involving it is plainMIXED, Feral x Feral included, so there is no back door to a "pure feral" line. As Unknown it combined like an ordinary distinct breed, which forced the model to answer whether a wild loner had a stat band, a pool and a claim to purity; absorbing it deletes the question instead of answering it. Closes that roadmap decision.GeneCodeDisplay.trailingOrder()is self-maintaining now, and it had already rotted: it was a hand-written list plusGenes.loaded(), so every built-in gene added after it was written vanished from the short form silently - by the time it was noticed it had swallowed the leopard complex, the LUT locus, the cutie mark, the particle locus and the three magical body-stat genes.GenotypeCatalogTest.noTwoEntriesShareADisplayLabelfinally tripped on it when tiger eye added a third coat outcome. It now appends every other registered gene in(priority, key)order after the curated head, so a new gene shows up on its own - known gap #13's cheap fix, applied to one list. Also: a hemizygous copy prints with itsX-/Y-prefix (X-Brn), and a plain stallion prints nothing at the locus rather thanX-n.- Numbers moved. 46 -> 48 built-in genes (50 in-game), code segments 48 -> 50,
GenotypeCatalog.size()1 849 688 066 -> 11 098 128 386,totalGenotypes()-> 42 875 903 837 175 872 297 619 456 000 000. Genes that paint 22 -> 24 (both new ones do; tiger eye through a marker); genes that never paint stays 24.coat-golden.txtregenerated - 558 rows, +48 for 8 new brindle / tiger-eye cases; every row moved, because two genes inserted at priorities 36 and 60 shiftEpigenome.fromSeed's positional per-gene seed stream. - Two brittle tests widened rather than re-pinned.
CoatTextureComposerTest.howFarGreyingHasGoneIsPerHorsecompared two named seeds and became a coin flip the moment the stream moved; it now measures the range over eight seeds, which is the actual claim.GenotypeCatalogTest.allPairsOfIsEveryUnorderedPairAHorseCanCarrylearned aboutsexConsistent. - Deliberately not built: a Y-linked gene; per-eye colour (one blue eye, which splash horses often have - it would want to be epigenetic and would put the locus back in the per-horse fingerprint); blue-eyed creams and champagne's amber (both two-line
EyeColorContributions, on the roadmap); a cutie-mark tint; item-pool biasing; hemizygous wording in the gene-inspect popup and the paper dump (only the short form has it). - Docs:
wiki/gene-brindle.html,wiki/gene-tiger-eye.html,wiki/compatibility.html(new - the mod-compatibility surface, and the settled "attachments, never a subclass" decision),wiki/nav.js,wiki/genetics-model.html(new sex-linked section + 2 table rows + counts),wiki/pipeline.html(new eye-colour and topline-reference sections),wiki/modding.html(new "Channels" section),wiki/api-reference.html,wiki/carrots.html(the blacklist),wiki/breeds.html(Feral Mixed + the Paso Fino),wiki/gene-cutie-mark.html,wiki/gene-light.html,wiki/gene-{kit,mitf,pax3,ednrb}.html(blue eyes),wiki/roadmap.html(§4 and §5 rewritten to remainders, five decisions moved to#settled, "Where to start" rewritten),wiki/verification.html(§0-G),index.html. All 68 wiki pages validated: zero broken links or anchors.
Docs 2026-09-06, no behaviour change: the roadmap was rewritten and renumbered, and the built work moved onto its own pages.
No Java touched; :common:test 405 green, :neoforge-26.1.2:build green. Shape of it:
wiki/roadmap.htmlis 2 370 -> 1 532 lines and contains only unbuilt work. Every shipped section was deleted rather than annotated - the page had become a second description of built behaviour, which is known gap #13's exact failure mode. Reordered by priority into six parts and renumbered 1-22 in reading order; the old numbers are gone. See the docs-split entry above for the new shape and the anchor rule.- Audited every claim against the source, not the prose. Confirmed genuinely absent:
RegisterHorseGenesEvent,AbstractNaturalGene/AbstractMagicalGene,TraitRule, any inheritance-mode declaration, eye colour, gestation, the villager, aBreedJSON spec, bareback steering, a brush item, the owned-horse index, the per-copy epigenetics editor, hash-keyedCoatTextureId, the split corridor, the sleeping bond source and saddle scale-inheritance.attributestill hitswarnUntranslated; no built-in overridesspliceTable(). - Two new pages for built systems that had no home outside CLAUDE.md:
wiki/items.html(all 20 items + every recipe; shearing, the hair chain, whistles, stall signs, tickets, seed jars, papers, the spawn egg) andwiki/horse-dimension.html(hay portals, the disposable plot,ShowcaseGenotypes, the pen layout, why the gallery died). Both added towiki/nav.jsunder Gameplay. wiki/carrots.htmlextended, not duplicated - it already covered the magic carrots in full. Two real drifts fixed: the Known Gene Splice recipe takes exactly four items (KnownGeneSpliceReciperequiresfilled == 4), not the documented ">= 5 with a flavour ingredient"; and the "View splice recipe" button is gone - selecting a gene auto-ghosts the recipe, andViewSpliceRecipePayload/fillSplicePreviewno longer exist.index.htmlwas three genes' worth of stale and missing nine pages: 38 -> 46 genes, 3.0x10^18 -> 1.4x10^30 genotypes, 462 422 018 -> 1 849 688 066 distinct coats, plus a new Gameplay section and cards for breeds, horse care, carrots, items, the dimension, gene effects, the trait architecture, Suntouched and Waterborn. Every wiki page is now reachable from the index.- ~50 cross-references re-pointed across 15 files rather than left to rot
- dead anchors (
#polymorphism,#random-pens,#priority,#modder-api,#care,#traits,#multi-allele,#third-party) now go to the page that owns the content, and stale§Nprose was replaced with names. Two more shipped-but-documented-as-planned claims fell out:gene-silver.htmlsaidZ/ZMCOA awaited the health system, andmodding.htmlsaid status effects were "not built yet" - both shipped 2026-09-04.
- dead anchors (
- Validated: all 63 wiki pages well-formed, zero broken links or anchors.
Built 2026-09-06, NOT yet play-tested: the leopard complex (appaloosa) - three new loci + a cross-locus-read hook; plus the custom-egg Breed button becomes a dropdown.
:common:test 405 green (+14 LeopardComplexGeneTest), :neoforge-26.1.2:build green, runServer boots clean (48 segments). Old saves will not parse (48 vs 45 code segments). Shape of it:
LeopardGene(horsegenetics.leopard,LP/lp, priority 73, natural, non-deterministic) +Patn1Gene(horsegenetics.patn1, priority 93) +Patn2Gene(horsegenetics.patn2, priority 94) - both extendingAppaloosaModifierGene, both all-wild (paint nothing themselves). 46 built-in genes (48 in-game). Full detail:wiki/gene-leopard.html.- Eight LP outcomes, chosen by
expressionIn(pair, genotype)reading the two modifier pairs + LP zygosity:mottled/varnish-roan(LP alone, het/hom),leopard/fewspot(+PATN1),blanket/snowcap(+PATN2),semi-leopard(both).expressionOf(pair)is deliberately coarse -WILDorVARNISH_ROANfor any LP pair - so the catalogue keeps two leopard pens, not eight. - New
Gene.coatDependsOn() -> List<String>(default empty). The PATN modifiers haveaffectsCoat()false, so they'd be dropped fromGenotype.coatCode()and aLP/LP PATN1/PATN1horse would collide in the texture cache with a plainLP/LPone.LeopardGene.coatDependsOn()names both;coatCode()folds their alleles in only when LP actually paints on that horse (checked viaexpressionIn(...).wildType()), so a PATN carrier with no LP still shares its texture (golden test asserts byte-identity).BreedFounderalso treats acoatDependsOngene like a coat gene (forced wild unless the breed names it). It is the model's only cross-locus gene, andwiki/roadmap.html#settleddiscourages it for third-party genes. - Painters (all phase 1, off the expressing LP copy's seed):
paintSpotted(whole horse white, round base-colour spots fromBodyNoise.cellDistanceWorley - fewspot = wider spacing, smaller radius),paintBlanket(white from the rump forward to acoveragefraction of theBODYX-span, wobbled edge, Worley spots inside; snowcap sets density 0; a big blanket creeps onto the hind legs),paintVarnish(a hair-by-hair white mix weighted 1 on the barrel/neck, ~½ on the head, 0 on mane/tail/ears, slightly asymmetric; mottled also drops a few crisp snowflake spots). "White finds white" is NOT applied - leopard composes at priority 73 (early), so leopard+sabino just stacks. Flagged. LeopardGene implements CoatOverlayContribution- striped hooves (vertical light/dark bands over the bottomHOOF_FRACTIONof each leg, light bands blended toward a horn colour) + a white-rimmed eye (out.shadeEyestoward a cool near-white, luma-scaled so the pupil stays dark). Drawn for every LP horse. Mottled skin is NOT built (the coat has no skin layer) - a settled deferral.LeopardGene implements HealthContribution-LP/LPaddsLeopardGene.CSNB, an informationalCondition(night blindness; no heart cost, no mechanic - the deafness precedent). Closes the CSNB gap inwiki/roadmap.html#health-genes.- Founder rates:
LPhardyWeinberg(LP, lp, 1/40)- ~4.9% carriers, ~0.06%LP/LP. PATN is conditional on LP:AppaloosaModifierGene.founderTablereturnsalways(n,n)unless theFounderContextshows the founder already rolled a non-wild LP (priority 93/94 > 73, so LP is in hand). A 20k-draw sweep test pins that no founder ever carries PATN without LP. GenotypeCatalog.size()andget()widenedint->long. The two-outcome coat gene doubled the reduced catalogue to 1 849 688 066, pastInteger.MAX_VALUE(gap #27 come due). No production caller indexes the catalogue (random pens), so it was a free widening;entries()stays aListand truncates atMAX_VALUE. Tests updated (SpecGeneTestlongarithmetic,GenotypeCatalogTestlongloop vars,sampleIndices->long[]).totalGenotypes()-> 1 429 196 794 572 529 076 587 315 200 000 (×27: three loci, three combinations each).size()last figure 924 844 034.- Breeds wired: Appaloosa (~75% LP, PATN1-dominant), Knabstrupper (~82% LP, PATN1-heavy), Kiger Mustang / Mustang / Shetland (light). Stale "NOT BUILT" notes removed.
Breeds.javaclass javadoc updated. coat-golden.txtregenerated (510 rows, +60 for 10 new leopard cases × 3 seeds × adult/foal). Every non-deterministickit=/mitf=/pax3=/ednrb=row moved - inserting LEOPARD at priority 73 shiftsEpigenome.random's per-gene seed stream for every gene after it (the stream is positional; the "keyed by gene, not position" claim only holds for a gene inserted after every painter). Deterministic coats byte-identical.WhitePatternGenesTest.averageWhitewidened from 3 seeds to 8 -sabinoWhiteIsAtLeastNinetyPerCentWhitewas a knife-edge (the 3-seed mean landed at 0.898 after the seed reshuffle; the true SB1/SB1 mean is 0.913). Not a KIT regression - a too-tight test.SpecGeneTest.BUILT_IN_GENES43 -> 46;HorseTraitsTesttrait-contributor count 18 -> 19 (CSNB).CoatSampleTool: 8 newlp_*samples; also removed the long-brokenchestnut_testsample (referenced the deletedtestgene, so:common:bakeCoatSampleshad been failing).- The custom-egg Breed button is a dropdown now (owner request).
CustomHorseSpawnScreen:cycleBreed()deleted;openBreedDropdown/pickBreedFromDropdown/drawBreedDropdownadded, a parallel of the existing many-alleleddRowoverlay (shares theDD_*geometry, ownbreedDdflag and widerBREED_DD_W= 118).breedDdis checked ahead ofddRowinmouseClicked/mouseScrolled/ the render override; any click resolves or dismisses it. Client-only, NOT play-tested. - Deliberately not built: mottled skin (§4.4 - no skin layer); "white finds white" for leopard; a per-outcome epigenetic branch into visibly different sub-patterns (the enumerated-expression approach covered the family without it); PATN2's own zygosity mattering (any
PATN2copy = "present"). - Docs:
wiki/gene-leopard.html(new),wiki/nav.js,wiki/genetics-model.html(registry + counts),wiki/pipeline.html(cross-locus note),wiki/api-reference.html(coatDependsOn,longsize()),wiki/breeds.html,wiki/roadmap.html(§4.2 / §5.4 shipped, §4.4 CSNB, §10),wiki/verification.html§0-F,index.html,README.md.
2026-09-05
Built 2026-09-05, NOT yet play-tested: spawn-egg preview controls + a many-allele dropdown + a Randomize button; cutie-mark placement/rotation tuned.
:common:test unaffected (391), :neoforge-26.1.2:build green. Client-only, CustomHorseSpawnScreen + CutieMarkLayer.
- The preview no longer follows the cursor. Click-drag over the preview orbits the horse (
previewYaw/previewPitch, pitch clamped ±80°); scroll-wheel over it zooms (previewZoom0.3–4x, on top of the size-locus scaling). NewmouseDragged/mouseReleasedoverrides + adraggingPreviewflag;mouseScrolledgained a preview-zoom branch and a dropdown-scroll branch ahead of the existing list-scroll one. - The preview horse is always mid-stride -
walkAnimationSpeed = 1,walkAnimationPos/ageInTicksadvanced off wall-clock,animateTailtrue. - Particle emitters now preview. The real
emittertranslator is server-side (GeneAbilityHandler,EntityTickEvent.Post, client-guarded) and the preview entity never ticks, so the screen runs its own tiny GUI-space mote system:drawPreviewParticlesreads everyparticle-kindemitterfromHorseAbilities.activeFor(genotype(), epigenome)(epigenetic colour resolved through the live editor epigenome), spawns coloured 2px motes on a ~0.1s beat in a band keyed loosely to the emitter's body anchor (hooves low, body/spine mid, head high), drifts + fades + clips them to the preview panel, capped at 160. It is a stand-in - a colour-correct cloud round the horse, not world-placed particles - enough to preview "this horse trails something, in this colour".Moteis a private nested class;previewRng+lastFrameNanos+emitAccumulatordrive it. - Genes with more than 3 alleles get a dropdown instead of a cycle button (particle 41, KIT 8, ACAN 5, MITF 4). An in-screen overlay (
ddRow/ddSlot/ddScroll/ddX/ddY,DD_VISIBLE8,DD_ROW_H12,DD_W76), drawn last inextractRenderStateso it sits over the widgets, with clicks/scroll intercepted at the top ofmouseClicked/mouseScrolledwhile open. Any click resolves or dismisses it. Genes with ≤3 alleles still cycle. Randomizebutton (right column, under Breed).applyBreedPresetwas refactored to a sharedapplyGenome(Genome, boolean adoptSex);randomizeGenes()rollsGenome.randomwith Breed: (none) orBreedFounder.roll(breed, rng, sex)with a breed selected, so the draw stays inside the breed's pools/targets. Keeps the chosen sex; baseline loci don't become added rows.- Cutie-mark emblem repositioned + de-cluttered.
CutieMarkLayerwas stamping at body-localz = -6(the centre of the body box, z∈[-17,+5]); it now anchors mid-haunch (z = 0), high on the flank (y = -4), a touch proud of thex = ±5.6/16side face. Multi-icon marks were overlapping with co-planar z-fighting ("weird clipping") and the row was wide enough to spill past the rump:SPACING0.16 -> 0.085blocks, icons now shrink with the count (countScale = 1/(1+0.28*(count-1))), each successive icon stepsDEPTH_STEP = 0.012blocks toward the viewer so the stack order is unambiguous, and each is fanned5°in-plane for a "stacked stickers" read.layout()widened its slot pitch to 1.1 and centres the row/triangle on the anchor. Also the icons were upside down: entity-model space is drawn rotated 180° about Z, so an item submitted inItemDisplayContext.FIXEDcame out inverted - a+ (float) Math.PIon the existingAxis.ZPtilt rotation spins it upright in its own plane (rotation, not reflection, so no mirroring). Still first-guess constants; checklistwiki/verification.html§0-D. - Not done: baking the cutie mark into the coat texture. The owner asked whether the emblem could be stamped into the 128px coat bake instead of the 3D item render. Left as the render layer: per-frame it is not a measured problem (cutie-mark horses are ~0.36% of the population), and a 16px item icon stamped onto the ~14-texel flank region would be lower fidelity than the current item models; the bake path also needs a new cache-key branch (the gene is a wild type, out of
textureKey()) and item-atlas pixel extraction (multi-layer item models, per-side mirroring) that can't be verified without a play session. A follow-up if the stamp look is wanted after the reposition is seen in-game.
Built 2026-09-05, NOT yet play-tested: name-tag rename window.
Right-clicking a horse (with a real record) with any name tag - not just an anvil-renamed one - now opens client/HorseRenameScreen: two fields (first / last name) prefilled from ClientHorseRecordCache, a Rename + Cancel button. Confirm -> RenameHorsePayload -> server re-checks range + a tag still in hand + not both-blank, then HorseRecords.rename and consumes one tag (creative exempt); Cancel consumes nothing. HorseInteractionHandler's name-tag branch now fires for any Items.NAME_TAG, cancels on both sides (no vanilla custom name), and server-side sends OpenHorseRenamePayload (playToClient -> HorseRenameScreen.open). The old handleNameTag (split-on-first-space, needed CUSTOM_NAME) is deleted. Lang: gui.horsegenetics.rename_horse / .rename_confirm. :neoforge-26.1.2:build green, runServer boots clean (both payloads register). Checklist: wiki/verification.html §0-E.
Built 2026-09-05, NOT yet play-tested: the cutie-mark gene + Horse Browser splice-recipe rework.
:common:test 391 green, :neoforge-26.1.2:build green, runServer boots clean (45 segments, no errors). Two pieces:
CutieMarkGene(horsegenetics.cutie_mark, priority 196, magical, recessiveCutmrk/n).Cutmrk/Cutmrkwears an emblem of 1-3 flat item icons on both flanks, drawn over everything including white patterns. 43 built-in genes (45 in-game). Owner-approved approach: use the game's own item icons, restricted to flat (non-BlockItem) items, modded ones included; cross-install determinism is explicitly not a goal.- Paints nothing in the pipeline - all three outcomes are wild types (the particle pattern), so
affectsCoat()is false, it's out of the texture key, and the catalogue is unchanged (924 844 034).totalGenotypes()x3to 52 933 214 613 797 373 206 937 600 000. CutieMarkGene.markFor(genotype, epigenome)(common/) draws the spec off the expressing copy's seed, fixed order:nextInt(3)count,nextBoolean()triangle (count 3 only), 3xnextFloat()normalised item picks (always 3 - the draw-order contract),nextFloat()scale,nextFloat()tilt. ReturnsOptional<Mark>, empty unless homozygous.common/names no item - the picks are[0,1)and resolved on the client.client/FlatItemCatalog- every registered item that is not aBlockItem, built once per session off the frozen registry (a new mod / version is a new session = rebuild, which is all the gene needs).client/CutieMarkLayer- aRenderLayer<HorseRenderState, HorseModel>twin ofEmissiveCoatLayer, added after it inGeneticHorseRendererso it draws last. ReadsCutieMarkGene.markForoffcoatData, resolves each pick to an item,updateForTopItem->ItemStackRenderState.submitat a per-item offset out from the animatedbodypart (root().getChild("body"),translateAndRotate), rotated to face outward, scaled small, row or triangle. Placement is a first-guess fixed offset and wants tuning against a live horse - flagged inwiki/verification.html§0-D.coat-golden.txtregenerated (only the newcutie_mark=n/nsegment; hashes unmoved). NewCutieMarkGeneTest(5).SpecGeneTest.BUILT_IN_GENES42 -> 43. Old saves won't parse. Docs:wiki/gene-cutie-mark.html(new),wiki/nav.js,wiki/genetics-model.html,wiki/api-reference.html,index.html,README.md.
- Paints nothing in the pipeline - all three outcomes are wild types (the particle pattern), so
- Horse Browser splice recipe - removed the "View splice recipe" button; selecting a gene now auto-ghosts its Known Gene Splice recipe into the Crafting grid's empty slots (client-only, no inventory movement), and hovering a ghost shows the real item's tooltip (
extractTooltip->setTooltipForNextFrame). "Craft research paper" stays (both tabs) and no longer self-hides when the gene isn't in the client DB. The default splice recipe is now exactly four items: horse hair + the gene's research paper + a golden carrot + the rarity ingot (gold at the default tier).KnownGeneSpliceReciperequires exactly those (filled == 4, one of each), dropping the old ">= 5 with a flavour" rule;SpliceRecipeDisplaymatches. DeletedViewSpliceRecipePayload+HorseBrowserMenu.fillSplicePreview. Lang: droppedgui.horsegenetics.view_recipe.
LUT is owner-verified in game 2026-09-05
(see the Owner-verified block).
Built 2026-09-05, NOT yet play-tested: white top/bottom UV swap + two Horse Browser gene buttons.
:common:test 386 green, :neoforge-26.1.2:build green.
- The horse's top and bottom cube faces were UV-swapped in
HorseSkinGeometry.faceMapsOf-Face.TOP(body-space yMax, the spine) was mapped to the texture's bottom patch andFace.BOTTOM(the belly) to the top patch. So any white painter that whitened "from below" (splash, sabino belly, frame off the underline) was flooding the topline while the belly stayed coloured - the standing "white over the whole back" gap. Owner diagnosed it visually; the twoFaceMapentries are now the right way round. Measured (bold splash, per-BODY-texel): back white 90%+ -> ~35%, belly -> 100% (a proper dipped-from-below coat). OneFace.BOTTOMconsumer (WhitePattern.covers, "no blaze under the jaw") flips to the correct face for free.:common:teststill green (incl.FaceMarkingTest'sFace.BOTTOMjaw assertions);coat-golden.txtregenerated - most rows moved (any BODY/NECK top or bottom texel). A secondary frame-band calibration remains (gap #30):EdnrbGene'sBAND_LO/BAND_HIare still whole-horse-AABB fractions so frame still leans topline-ward (~75% back onO/N); the per-part fix would also strip frame's legitimate neck-side white, so it's deferred. Tobiano over the topline is correct. - Two gene buttons in the Horse Browser (shown when a discovered carrot gene is selected - detail-pane bottom on Gene database, right of the grid on Crafting):
- "Craft research paper" - the existing
WriteResearchPaperPayload/ResearchPaperWriterflow (consume one book -> aresearch_paperfor the gene), just surfaced as its own button on both tabs. Still requires the gene discovered (in the DB) even in creative, because the server writer checks that. - "View splice recipe" - new.
network/ViewSpliceRecipePayload->HorseBrowserMenu.fillSplicePreview(gene): empties the 3x3 grid back to the inventory, then pulls one of each Known Gene Splice ingredient the player actually has into the grid (golden carrot, this gene's paper viaResearchPaperItem.geneOfmatch,horse_hair, theRarityItemsingot,sugaras a flavour).menu/SpliceRecipeDisplay.forGeneis the canonical 9-slot layout, client-safe so the screen shows the same list as ghosts (dimmedg.fakeItem) in whatever slots the player couldn't fill. It never crafts - with all five real, the result slot shows the carrot to take normally. Ghosts clear on gene change / switching to Gene database. Lang:gui.horsegenetics.craft_paper,.view_recipe(replaced.write_paper).
- "Craft research paper" - the existing
- Checklist:
wiki/verification.html§0-B (buttons) and the "Open issues" note (white swap - main bug fixed, frame calibration deferred).
Built 2026-09-05, NOT yet play-tested: the LUT gene + the Horse Browser becomes a container menu with a Crafting tab; plus two fixes on owner play-test feedback (browser UI redesigned, splash face-marking boosted).
:common:test 386 green, :neoforge-26.1.2:build green, runServer boots clean (44 segments, loaded 2 data-driven gene(s), menu type + payloads register with no error). Pieces:
LutGene(horsegenetics.lut, priority 190, magical, allelesn/Blupnk) - a gene that swaps the phase-2 colour gradient instead of painting. A horse homozygous for a variant allele resolves its surviving red/black pigment against an unnatural LUT (Blupnk/Blupnk->assets/horsegenetics/textures/coat/lutbluepink.png, a blue/pink palette); one copy, or two different variants, is a silent carrier (bluepink-carrier, a wild type). 42 built-in genes (44 in-game). It isaffectsCoat()true (the swap is a real coat change) and deterministic, so it is inGenotype.coatCode()/ the texture key but out of the per-horse fingerprint. Machinery:common/genetics/LutContribution- a new gene capability interface (the fourth thing a gene can be, besideExpression/TraitContribution/AbilityContribution):alternateLut(pair, genotype)->Optional<String>(non-empty only for a true variant homozygote) +lutResources()->Map<key, "textures/coat/....png">(plain strings -commonnames noIdentifier). Exactly one gene implements it and always will - owner's rule, mirroring KIT: a new palette is a new allele on this gene, never a new gene, because a horse has two chromosome copies and shows at most one alternate palette.common/coat/pattern/LutSet-record LutSet(GradientLut base, Map<String,GradientLut> alternates)+LutSet.of(base)+resolve(key)(unknown/null key -> base).CoatTextureComposer.bake/composegained a(LutSet luts)overload; the old(GradientLut lut)one delegates withLutSet.of(lut), so every pre-LUT caller (CoatSampleTool, the golden test's synthetic LUT path) is untouched. The composer'sselectAlternateLut(genotype)walksGenes.codeOrder()for aLutContributionwith a non-wild, non-empty answer and resolves phase 2 against that LUT.Expression.Builder.marker()- finishes an outcome that is not a wild type but has no painter of its own (bothrestrictandtintnull). The LUT swap is applied out of band;bluepinkis the first user.GeneCoatHookTest.aMagicalExpressionReturnsADeltaAndLeavesTheAccumulatorAlonenow skipsLutContributiongenes.client/GeneticCoatTextureFactory- loadsredblackgradient.pngas the base plus everyLutContributiongene'slutResources()textures into aLutSet(a failed alternate logs a warning and falls back to base), and threads it throughcompose/bake.- Founder table: carriers only (
Blupnk/n~1.7%,n/nthe rest); a variant homozygote is never a founder. No gene carrot (hasGeneCarrot()false). coat-golden.txtregenerated: every existing row gained alut=n/ncode segment but no hash moved (per-gene epigenetic seeds are keyed by gene, not stream position - the magic-speed precedent); four newlut=Blupnk/*cases added, with a synthetic blue/pink alt LUT built in the test.SpecGeneTest.BUILT_IN_GENES41 -> 42. NewLutGeneTest(6).GenotypeCatalog.size()462 422 018 -> 924 844 034 (LUT is a two-outcome coat gene, so it doubles the unmasked entries);totalGenotypes()x3to 17 644 404 871 265 791 068 979 200 000.bakeCoatSamplesgained threelut_bluepink_*samples.- Old saves will not parse (code 41 -> 42 built-in segments). Dev only.
- Docs:
wiki/gene-lut.html(new),wiki/nav.js,wiki/genetics-model.html(table + counts),wiki/pipeline.html(phase 2 swap note),wiki/api-reference.html. LUT owner-verified in game 2026-09-05 (§0-A retired).
- The Horse Browser (H) is a real container menu now. It was a client-only
Screen; it is nowclient/HorseBrowserScreen extends AbstractContainerScreen<HorseBrowserMenu>over a newneoforge-26.1.2/menu/package:ModMenus(aDeferredRegister< MenuType<?>>, registered from the mod ctor),HorseBrowserMenu(AbstractContainerMenu- a 3x3TransientCraftingContainer= slots 1-9, aResultContainer= slot 0, the 36 inventory slots added by hand so they too can go inactive), andHorseBrowserRecipes(the result logic). Pressing H now sendsnetwork/OpenHorseBrowserPayloadand the serveropenMenus aSimpleMenuProvider;ClientSetupbinds the screen viaRegisterMenuScreensEvent.HorseBrowserKeyHandlersends the payload instead ofsetScreen.- Two tabs, drawn from a strip at the top-centre of the window. "Gene database" is a full-window reference (list left, scrolling detail right, "Write research paper" button) with no slots - every menu slot goes inactive here. "Crafting" is a compact centred panel (3x3 grid + result + player inventory) with the gene list still down the left. (The first attempt crammed everything into one 316x244 panel and overlapped badly - owner feedback - so it was redrawn this way the same day. All custom drawing is in screen coords:
extractLabelsundoes the containerleftPos/topPostranslate first.) - Crafting tab - a private 3x3 grid that makes only this mod's recipes (no vanilla recipe lookup anywhere): (1) a single book in the grid + a discovered gene picked in the left list -> that gene's
research_paper(creative sees every carrot-bearing gene); (2) the paper-parameterisedKnownGeneSpliceRecipe; (3)CarrotCombineRecipe.HorseBrowserRecipes.resultFor/.consumeimplement it; the sharedKnownGeneSpliceRecipe/CarrotCombineRecipeinstances are stateless. Gene selection is menu state set from the client bynetwork/SelectBrowserGenePayload(sent on every list click, so the two tabs stay in sync);slotsChangedrecomputes the result server-side and pushes aClientboundContainerSetSlotPacket. - On the Gene database tab every slot (grid, result, and the player inventory) goes inactive via an
isActive()override tied toHorseBrowserMenu.setCraftingVisible, which the screen toggles each frame- so that tab renders no slots, no highlights, no tooltips;
slotClickedis guarded too. Switching away from Crafting while holding a picked-up stack is blocked.
- so that tab renders no slots, no highlights, no tooltips;
- Lang:
gui.horsegenetics.horse_browser. No new items, no datapack changes. - Not done / risks (still an untested GUI against the 26.1.2 retained-mode screen API): the redraw's geometry is verified only on paper - check for overlap at GUI scale 2/3/4;
quickMoveStackedge cases; thebook -> paperresult-slot consume path. Checklist:wiki/verification.html§0-B.
- Two tabs, drawn from a strip at the top-centre of the window. "Gene database" is a full-window reference (list left, scrolling detail right, "Write research paper" button) with no slots - every menu slot goes inactive here. "Crafting" is a compact centred panel (3x3 grid + result + player inventory) with the gene list still down the left. (The first attempt crammed everything into one 316x244 panel and overlapped badly - owner feedback - so it was redrawn this way the same day. All custom drawing is in screen coords:
- Splash face fix -
WhitePattern.splashwas scaling its face marking with its body strength (~0.35 for one copy), which on the shared face ladder is star/snip/nothing, so splash horses came out bare-faced (owner feedback). It now boosts the face strength bySPLASH_FACE_BOOST = 0.34: one copy -> a blaze, homozygote / two-locus -> a bald face. Same RNG draw count.coat-golden.txtregenerated (splash rows moved);KIT/EDNRBuntouched. NewWhitePatternGenesTestcase. Checklist:wiki/verification.html§0-C.
2026-09-05 follow-ups to the carrot pass, NOT yet play-tested
- Carrots renamed (owner's call, dev-only, no saves): "mutinogenic" -> Unknown Epigenetic Splice carrot (
CarrotEffect.EpigeneticSplice, tokenepigenetic_splice); "chaos" -> Unknown Gene Splice carrot (CarrotEffect.GeneSplice, tokengene_splice); "magic gene carrot" -> Known Gene Splice carrot (CarrotEffect.KnownGeneSplice, tokenknown:<gene>:het|hom, itemknown_gene_splice_carrot,server/recipe/KnownGeneSpliceRecipe).Gene.chaosTable()->spliceTable(); the specchaosblock ->splice;Gene.hasMagicCarrot()->hasGeneCarrot(),magicCarrotHomozygous()->geneCarrotHomozygous(). Naming logic: "Unknown" = random, "Known" = you pick the gene. Fixtures + parity regenerated (no output change). - Crash fix: the
DistanceManager.runAllUpdatesre-entrancy trap, hit going to the horse dimension.GeneAbilityHandler.onEntityLeave(aglowlight-block cleanup, from the 2026-09-04 magical-utility pass) calledlevel.getBlockStatesynchronously from inside the entity-untracking callback ->IllegalStateException: Entity is already tracked!. Now it queues aPendingClearand aServerTickEvent.Postdrains it next tick. See the 26.1.2 API note.
Built 2026-09-05, NOT yet play-tested: shearing + milking rules + the whole carrot family + the gene database + research papers
(roadmap wiki §§7, 12, 14, 16, 19). Big multi-system pass. :common:test 379 green, :neoforge-26.1.2:build green, parity 3832/48, runServer boots clean (43 segments, still - format 3 is additive). Shape of it:
- Shearing (§12) - right-click an adult horse with shears -> 1-3
horse_hair, once per MC day, gated by a newdata/HorseCooldownsAttachment(a storedMap<String,Long>game-time stamp per key:"shear", and"yield:<geneKey>"- which replaces the static cooldown mapGeneYieldHandlerwas criticised for).+5bond per shear via a newHorseCareHandler.awardBondFor. New itemsbraided_rope+hair_clothwith reversible shapeless recipes, finishing the §12.2 chain. The sheared look (a low-fidelity render-layer overlay) is deferred - a verification item; the mechanic is complete. - Milking rules (§7) - the interaction shipped with the milk gene; the rules did not. Built as
effects-vocabulary additions (buys it for every gene): afull_healthcondition flag (own max, so a frail mare is milkable at her own ceiling);tamedadded to the milk yield'swhen;GeneAbility.YieldgaineddeniedDamage+deniedMessage- the else-branch a bareyieldcan't express, so milking a stallion now kicks you (half a heart) and milking a foal says so.MILK_COOLDOWN_TICKSis24_000(once a day) and lives on the cooldown attachment. Not SpecSchema/parity (effects don't paint).GeneYieldHandler: a matched yield with an emptyproducesis a denial branch; a producing yield on cooldown now says "nothing to give yet" (closes gap #25's feedback complaint). - §19 gene metadata -
common/genetics/GeneRarity(6 tiers) + threeGenedefaults:rarity()(defaultUNCOMMON= the gold-ingot tier),hasGeneCarrot()(false forSexGene, the recessive disorders viaRecessiveDisorderGene, and extension + agouti),spliceTable()(Optional<FounderTable>- the Unknown Gene Splice carrot's per-gene distribution) plusgeneCarrotHomozygous(). SpecFORMAT2 -> 3: optionalblurb,rarity,carrot({enabled, behaviour, flavour}) andspliceblocks (splicereuses the founder-weight parser).SpecGenesurfaces all of them. Every example + shipped gene file and the creator emitformat: 3; the parity fixtures regenerated with no output change (the §19 fields don't touch the paint engine). The strictformat != FORMATcheck stays - dev only. - The gamete hook (§14, the determinism-sensitive core) -
common/genetics/GameteBias: a per-parent breeding modifier (rerollEpigenetics/preferLowerOrder(dominant vs recessive copy) /substitutePairs).Genome.breedWithgained a(bias, bias)overload run in two passes: pass 1 is the allele draw and consumes exactly what the plain breed consumes (soNONE/NONEis bit-for-bit identical - asserted, andcoat-golden.txtis untouched); pass 2 does the epigenetic re-rolls after every allele is locked, so a Unknown Epigenetic Splice carrot never moves a foal's genotype.GeneticCodeCombiner.combinegot the matching overload. NewGameteBiasTest(5). common/genetics/CarrotEffect(sealed:Unknown Epigenetic Splice/Stabilizer/Magnifier/GeneSplice/KnownGeneSplice(geneKey, homozygous)) with a flat stringid()for serialisation,parse/parseList/tokens/isContradictory, andfold(effects, parentGenotype, rng)-> oneGameteBias. Gene splice rolls its target gene + pair at breeding time off the foal RNG (deterministic per foal; never the sex locus). Magic-carrothomozygousfalls back to het when the homozygotecanOccuris false.- NeoForge, the carrot side -
data/CarrotWindowAttachment(List<String>effect tokens +expiresAt; notcopyOnDeath;WINDOW_TICKS= 30 s, vanilla love length).server/BreedingCarrotHandler(EntityInteract): a carrot on a breedable adult -> merge the window,horse.setInLove(player), a per-type coloured particle burst, consume 1; stabilizer/magnifier on an all-homozygous horse messages "does nothing".HorseBreedingHandler.onBabySpawnreads both live windows,CarrotEffect.folds each, threads the biases throughapplyBredFoal(new overload; old arity delegates withNONE), then clears both windows.data/ModDataComponentsgainedCARROT_EFFECTS(an itemList<String>component) andRESEARCH_GENE(a string). - Magic carrot recipe (§14.2) -
server/recipe/KnownGeneSpliceRecipeis one parameterisedCustomRecipe: golden carrot +research_paper+ a hair item + the gene's rarity item (RarityItems: iron/gold/diamond/ emerald/netherite ingot/nether star) + >=1 flavour slot -> aknown_gene_splice_carrotcarryingknown:<geneKey>:het|hom. One recipe, so a drop-in gene gets its carrot free.CarrotCombineRecipemerges 2+ carrots into one and rejects contradictions at craft time (stabilizer+magnifier; two Known Gene Splice carrots that disagree). Both are unit-codec serializers registered viaserver/recipe/ModRecipes+ one datapack JSON each.placeholder_gene_bookand its shapeless recipe are deleted. - Gene database (§16.1) -
data/GeneDatabaseData, server-global per-playerSavedData(seen tokens, carrot-unlock flag, first-met time per gene).server/GeneDiscoveryHandlerfills it on tame (AnimalTameEvent) and on foal birth (fromapplyBredFoal, for the breeder) - one entry per gene the horse carries a non-baseline allele at. It hides nothing: the info panel + genotype code still show every gene; discovery only gates the carrot-recipe display (the recipe itself is gated by needing a paper).network/GeneDatabaseSyncPayload->client/ClientGeneDatabase(browser only; cleared on logout; re-synced on login). - Research papers (§16.2) -
item/ResearchPaperItemwith aresearch_genecomponent. Right-click -> adds the gene to the DB + unlocks its carrot line + consumes; tooltip = name + rarity + blurb. Chest-loot injection:data/loot/AddResearchPaperModifier(anIGlobalLootModifier, gene weighted byGeneRarity.lootWeight()) +data/horsegenetics/loot_modifiers/*.jsontargeting ~8 chest tables. 26.1.2 note: theneoforge:loot_modifiers/global_loot_modifiers.jsonindex file is gone -LootModifierManageris now aSimpleJsonResourceReloadListenerthat reads every file in theloot_modifiers/folder directly. - Horse Browser rework (Phase H) - the Gene Database tab is no longer creative-only: it reads
ClientGeneDatabase, shows a discovered gene in full (blurb, alleles, phenotypes, variants seen, "Magic carrot recipe: unlocked/locked") and an undiscovered one as a dim name + a nudge (creative sees all). A "Write research paper" button spends one book for a paper on the selected discovered gene (WriteResearchPaperPayload->server/ResearchPaperWriter). - Deliberately deferred (owner call): the Horse Master villager (§18), seed-jar gestation + carrot-on-jar effects (§15.1), attached model parts (§19.2), the sheared render-layer look. Also not built: a DB check inside
KnownGeneSpliceRecipe.matches(the paper ingredient is the gate); the §19 header fields as creator form inputs (the creator emitsformat: 3but has no rarity/carrot/splice fields yet); per-genesplicetables on any built-in (all fall back to the uniform draw). - Docs:
wiki/carrots.html(new),wiki/nav.js,wiki/gene-effects.html(yield denial +full_health),wiki/gene-format.html(format 3),wiki/gene-milk.html,wiki/horse-care.html,wiki/genetics-model.html,wiki/api-reference.html,wiki/modding.html,wiki/roadmap.html(§§7/12/14/16/19 trimmed to remainders). Checklist:wiki/verification.html§0.
Built 2026-09-05, NOT yet play-tested: the Horse Browser (H page), first slice, + a per-horse gene popup.
A new common/genetics/GeneDescriptions (a one-paragraph, plain-English summary of every one of the 41 built-in genes) behind a new Gene.description() default - the authoritative gene docs stay in wiki/gene-*.html; this is the glanceable version, and a data-driven gene returns "". :common:test still green (no test touches it). Three new client screens / handlers in neoforge-26.1.2/client/:
HorseBrowserScreen- opened by a newHkeybind (HorseBrowserKeyBindings+HorseBrowserKeyHandler, registered in production, categoryMISC, langkey.horsegenetics.horse_browser). A tabbed reference window; one tab built, Gene Database. In creative: every registered gene alphabetically byname(), a filter box matching gene name / key / allele token / label, and a detail pane with thedescription()summary, every allele (token — label) and everyExpression(name + sentence +wild type/masks/variesflags). Both list and detail scroll independently. Outside creative the tab is a short note. The tab strip is drawn and click-wired (tabAt) so a second tab slots in without input changes.GeneInspectScreen- a full-window popup opened from a new "View Genes" button on the horse inventory panel (above "View Family Tree"). The horse's two allele tokens in column one, the genotype-context phenotype sentence (genotype.expressionOf(gene)) in column two. Lists sex / extension / agouti always, then every gene the horse carries a non-baseline allele at (!pair.homozygousFor(defaultAllele())) - so expressing coat genes and heterozygous carriers, matching the paper dump'snJ/nC/nCrshortform. Expressing rows green, carrier rows muted gold; both print the outcome's real name + sentence (a one-copy MSTN reads "Middle distance", not "no effect"). Closes to the game like the family tree.HorseScreenHooks- the compact genotype short-form line is removed from the grey panel (superseded by the popup);GeneCodeDisplayand the localwrap/shortGeneshelpers went with it. New sharedclient/GuiText(pixel word-wrap + clip).- Nothing talks to the server - all the data is
common/already on the client.:neoforge-26.1.2:buildgreen. Checklist:wiki/verification.html"Still to verify" §0 (gene-browser). - Docs:
wiki/verification.html(§0),wiki/api-reference.html(theGene.name()/description()row),README.md(the panel + the H key),index.html- which was also brought current in the same pass: it was missing cards for dun, silver, mushroom, roan, tobiano and had no section at all for the 13 non-coat genes, so all 41 built-ins now have a card (a new "Non-coat genes" section). Lang:key.horsegenetics.horse_browser. - Not built: any second tab (horse roster, breeding planner, punnett); a gene-level description for data-driven genes (the spec format has no field for it); returning the popup to the inventory screen instead of the world; surfacing
description()on the wiki / a generated gene dictionary (gap #10).
Built 2026-09-05, partly play-tested: breeds + herds + wild aggro; Test gene removed.
A common/breed/ package, 49 real-world breeds + UNKNOWN, biome-weighted wild-herd spawning (traditional harems + bachelor bands), cross/mixed breeding labels, wolf-style herd aggro, and a batch of dev tools. The Test gene was deleted (TestGene + TestCoatPattern gone) - it had served its purpose and looked ugly. Genotype code 44 -> 41 segments (43 in-game with suntouched + waterborn), coat-golden.txt regenerated (the three test=T/t golden cases dropped; CoatTextureComposerTest / GeneCodeDisplayTest / GenotypeTest / GeneticCodeCombinerTest / SpecGeneTest retargeted off Genes.TEST), GenotypeCatalog.size() 462 422 019 -> 462 422 018, totalGenotypes() /3 to 5 881 468 290 421 930 356 326 400 000. Machinery is wiki/breeds.html and wiki/horse-care.html; the shape of it:
- A breed is a constrained founder roll.
Breedcarries per-gene allele pools (as tokens), aBreedStatTargets(per-axis multiplier bands), a biome list, aCommonness-> spawn weight, a magic chance, and a notes list.BreedFounder.roll(breed, rng)starts fromGenotype.randomand overrides: every coat gene the breed doesn't name -> wild (so no stray pattern), every named gene -> the breed's pool, the four magical body-stat loci -> homozygous for the pushing allele on any axis the breed pins, every other magical gene -> wild, then a geometric draw (p=0.20, then 0.10, 0.05, ... halving, cap 10) switches a few back on.Breeds.UNKNOWNskips all of it - the pre-breeds unconstrained roll. Disorder genes keep their global rates unless the breed is.hardy(). - Stat targeting is a band, not the Gaussian. The sheet scores speed/jump/heartiness 1-10;
BreedStatCurvemaps 5 -> x1.0 and 10 -> the sheet's ceiling (speed x2.02, health x2.24, jump x3.43), linear, with a shared ~x0.2 floor at score 1. Height (hh range) -> scale bandmidHh / 15.75.HorseTraits.resolvegained aresolve(genotype, epigenome, BreedStatTargets, healthGenetics)overload;TraitBuildercarries the targets and the four body-stat genes (AbstractMagicStatGene+MagicSizeGene) readout.breedBand(axis)- when a band is set and the horse carries a variant copy, they lerp inside the band from the two copies' epigenetic seeds instead of the bounded Gaussian. So a Thoroughbred reliably resolves near x2 speed (twoSwiftcopies on the Gaussian only reach ~x1.2) and within-breed spread is exactly the band width. A near-baseline band (straddles 1.0) is dropped -> locus left wild -> horse sits on the baseline. New:common/trait/StatAxis,TargetBand,BreedStatTargets. - Spawn side - herds by connected clump (4th revision).
BreedSpawnHandler(FinalizeSpawnEvent) does one thing: on a NATURAL / CHUNK_GENERATION / SPAWNER spawn, sethorse.getPersistentData()horsegenetics:wild_spawn. TheSpawnGroupDataroute is dead -Horse.finalizeSpawnreplaces any custom pack data with its ownHorse.HorseGroupDatabetween every member, so theBandDataapproach silently made every horse "member 0" with its own breed -> "herds are all mixed / groups of 4 solo Unknowns".HerdManagerforms the herd by proximity instead (deferred one tick). Non-natural spawns (egg with a pre-set record;/summon) have no flag -> loneunknown. Twodata/horsegenetics/neoforge/biome_modifier/JSONs:add_horse_herds(37 biomes, weight 10, minCount 3, maxCount 6) andadd_horse_loners(#minecraft:is_overworld, weight 1, 1-1) - a strong bias to packs. Horses now spawn in taiga, jungle, badlands, swamp, snowy biomes and mushroom fields - none vanilla. - Breed labels combine (
BreedLineage.combine, inapplyBredFoal): A x A -> A; A x B -> "A x B cross" (components sorted, order-free); same cross x itself or x one of its own breeds -> that cross; cross x anything else -> Mixed; Mixed x anything -> Mixed. Unknown acts as an ordinary distinct breed (Friesian x Unknown -> a cross; owner may want it absorbing like Mixed - flaggedwiki/roadmap.html#decisions). A cross averages its two components' stat bands per axis (only where both pin it); Mixed/Unknown pin nothing. HorseRecordgainedOptional<String> breed(position 7, afterepigenomeCode); codecbreedoptional field;founder/bredfactories gained abreedTokenparam (old arities kept, delegating tounknown);record.lineage()->BreedLineage,record.traits()is breed-aware.StoredGenome(seed jar) gained abreedfield so a seed-jar foal crosses right.- Surfaces. Info panel shows the breed under the sex/gen line (
PANEL_H164 -> 176). Paper dump (HorsePaperInspectHandler) prints abreed:line. The dev-build[coat]chat line (GeneticCoatTextureFactory.debugLogCoat) appends[<breed>]- threaded fromGeneticHorseRenderervia a newGeneticHorseRenderState.breedLabelandcoatTextureFor(coat, baby, label)/getOrCreate(coat, baby, label)overloads (label is cosmetic only; the bake is genome-keyed). The custom spawn egg gained aBreed:cycle button (CustomHorseSpawnScreen.cycleBreed/applyBreedPreset): landing on a breed rolls a freshBreedFounder.rollof it into the editor (genotype + epigenome + sex) andSpawnCustomHorsePayloadcarries the token so the spawned horse is stamped (ModNetworking.handleSpawnCustomHorse->record.withBreed). - Referenced-but-not-built (noted per-breed +
wiki/roadmap.html#breeds): leopard complex (Lp/PATN1 - already §4.2), Tiger Eye, HYPP (a heart-reducing gene), pangare/mealy, flaxen, the early-lethal foal disorders (SCID/CA/LFS/GBED/NNF/CVM/megaesophagus), gait (DMRT3), mane-shape and feathering render layers. Won't model: metallic sheen, curved ears, head profiles. server/HerdManager.assignFounder- called byserver/HorseFoundingTickHandleronEntityTickEvent.Post, 20 ticks after the horse spawns (WILD_FOUND_DELAY_TICKS), not from the join event. Two reasons it had to move off the join path: (a) the join deferral (server.execute) drains during chunk streaming only a few ms later, still the same tick, so a scan for pack-mates saw an empty clump and every horse founded a herd of one - owner hit this: all wild horses Unknown, no herds; (b)applyTraitsToEntity'sSCALEwrite crashes from that context (see the crash note below). Waiting 20 ticks on the entity tick fixes both - the pack is loaded and query-visible, and nothing re-enters the chunk system. It then forms the herd over the whole connected clump (HERD_RADIUS32 per flood-fill step,BandTypeTRADITIONAL 70% / BACHELOR 30%):- why a clump and not a 32-block look:
NaturalSpawnerwalks each pack member a cumulative ±5 blocks from the last, so a pack of four routinely spans more than 32 blocks end-to-end, and the tick handler processes horses in entity-iteration order, not spatial order. A single-radius rule would let a spread pack found two herds of two breeds. herdedMemberOfClump- flood-fill the connected clump (untamed horses, each within 32 of the next, hopping through fresh pack-mates); if any member is alreadyinWildHerd()-> JOIN it (its breed, band, lead;leadSexif the lead UUID is this horse, elsejoinerSex);- else
freshClump(flood-fill of freshwild_spawnpack-mates, incl. self) has >1 member -> FOUND: elect the clump's lowest-UUID member as the lead, and derive breed + band fromRandomSourceseeded by that lead's UUID + the lead's biome (pickHerdBreed). Every clump member computes the same lead and therefore the same herd, whatever the processing order - the elected lead included, when its own tick runs (it JOINs a mate whoseherd()already points back at it); - else a clump of one -> a lone
UNKNOWN.BandDataand the band NBT keys are gone. Traditional-band foals now spawn at vanilla's ~5% rate (the packAgeableMobGroupDatacan't be overridden any more) - noted as a follow-up. Breed + herd assignment is owner-confirmed working in-game 2026-09-05; the crash fix is not yet play-tested.
- why a clump and not a 32-block look:
server/WildHerdGoal(new, added besideBondFollowGoalat priority 6): an untamed herd member paths to its lead (resolved viaServerLevel.getEntity(uuid)) when >8 blocks away, re-path every ~15 ticks; if the lead is gone for 10s it promotes itself.HorseCareHandler's together-timer skips any horseinWildHerd().HorseCareAttachmentgainedherdBreed+herdBand(herdis now the lead horse's UUID for a natural herd; the lead points at itself).inWildHerd()=herd+herdBreedboth set. Codec fieldsherd_breed/herd_band.server/HorseAggroHandler(new) - wild horses aggro like wolves.EntityAttributeModificationEvent(mod bus) givesEntityType.HORSEanATTACK_DAMAGEof 4; every horse gets aMeleeAttackGoal(priority 3) and aWildHorseForgetTargetGoal(targetSelector, priority 1) that clears the target after 60 ticks with no line of sight ("break eyesight -> neutral").LivingIncomingDamageEvent: an untamed victim + every herd-mate within 24 blocks target the living attacker. Tamed horses and debug-dim horses are untouched.- Debug tools. F8 (
RequestHighlightHorsesPayload->server/DebugHighlightHandler.toggle) is a toggle now: press once to keep every horse within 96 blocks glowing (refreshed every 40 ticks onServerTickEvent.Post), press again to stop. Per-player, dropped on logout. The stick / clock shortcuts now also work anywhere in a dev build (!FMLEnvironment.isProduction()), not just the horse dimension -HorseInteractionHandler. - Paper dump (
HorsePaperInspectHandler) - droppedgeneration:and the ancestors block, added aherd:line: band type, "this horse is the lead" or "led by <name>", and the member count within 64 blocks. - Bigger big horses.
BreedStatCurve.scaleBandnow runs the above-baseline part of a height ratio throughBIG_GAMMA = 3.0(below-baseline untouched - small breeds already read fine). Real horse heights barely differ (a Shire is ~10% over average) but a draught horse should tower: Quarter Horse resolves ~x0.96, Percheron ~x1.0-1.38 (avg ~1.19), Shire/Clydesdale x1.14-1.43. Falabella etc. unchanged. - Surfaces. The custom spawn egg gained a
Breed:cycle button (CustomHorseSpawnScreen.cycleBreed/applyBreedPreset): pick a breed and it rolls a fresh wild individual of it (genotype + epigenome + sex) into the editor;SpawnCustomHorsePayloadcarries the token so the spawned horse is stamped (ModNetworking->record.withBreed). The paper dump prints abreed:line; the dev[coat]chat line appends[<breed>](via a newGeneticHorseRenderState.breedLabel+coatTextureFor(coat, baby, label)/getOrCreate(coat, baby, label)overloads - label is cosmetic only, the bake is genome-keyed). - New tests:
BreedLineageTest(11),BreedStatCurveTest(7),BreedsTest(7),BreedFounderTest(8);TestCoatPatternTestdeleted.:common:test374 green,:neoforge-26.1.2:buildgreen,runServerboots clean (43 segments, biome modifiers load,loaded 2 data-driven gene(s)). Checklist:wiki/verification.html§0e. - The
DistanceManager.runAllUpdatescrash - actually fixed 2026-09-05. History:FinalizeSpawnEvent->onHorseJoindidn't help (onHorseJoinalso fires inside the chunk system'supdateFutures), and deferringonHorseJoin's whole body toserver.executealso crashed inrunClient-server.executetasks drain inMinecraftServer.waitUntilNextTickright next toServerChunkCache.pollTask->runDistanceManagerUpdates, soapplyTraitsToEntity'sAttributes.SCALEwrite there still re-enters the ticket pass and NPEs. Breeds made it reliable: a breed pins the body-size locus homozygous, so every Fjord / Percheron / Falabella gets a real scale change on its first resolve. Real fix: all record / coat / trait / herd work now runs fromHorseFoundingTickHandleronEntityTickEvent.Post- the settled entity-tick loop, after the tick's chunk updates, with the horse's surroundings already loaded so
refreshDimensionstouches nothing re-entrant.onHorseJoin'sserver.executedeferral is gone;HorseGeneticsEventHandlerkeeps only the debug-dimension guards +ensureExistingRecordResolved(called by the tick handler). A wild horse foundsWILD_FOUND_DELAY_TICKS(20) after spawn so its pack is loaded and query-visible first - the earlier "found on join" scan saw an empty clump (pack-mates not indexed yet) and every horse came out a lone Unknown.
- the settled entity-tick loop, after the tick's chunk updates, with the horse's surroundings already loaded so
- Also confirmed in the same session: the frame/tobiano/splash "white over the whole topline" defect (known gap #30) is real in-game; still deferred.
- Docs:
wiki/breeds.html(new),wiki/nav.js,wiki/breeding.html,wiki/horse-body.html,wiki/roadmap.html§22,wiki/verification.html§0e,README.md. - Deliberately not built: per-breed pool-rate balancing (all estimates), the biome-density audit, sex-linked / gaited genes, and a "blended F1 toward the stronger parent" cross-stat rule.
Built 2026-09-05, NOT yet play-tested: magic speed, magic health and magic jump - three more genes on the MagicSizeGene pattern.
MagicSpeedGene (horsegenetics.magic_speed, priority 141, Swift/Sluggish/n), MagicHealthGene (horsegenetics.magic_health, 142, Hardy/Frail/n), MagicJumpGene (horsegenetics.magic_jump, 143, Springy/Leaden/n). 42 built-in genes, 44 in-game. Shape of it:
- One shared base,
AbstractMagicStatGene(the ManeColor/TailColor -> HairColor precedent). It is the magic body size design generalised to the three additive stats: codominant, three alleles,MEAN_DELTA0.10 /SIGMA_DELTA0.07 /MIN_DELTA0.01 per copy, onenextGaussian()off each copy's own epigenetic seed (GeneEpigenetics.copy(slot)), both copies added (uppositive,downnegative), six combinations = six outcomes, allwildType. Founder table written out 40% up/n, 40% down/n, 20% n/n - heterozygotes only, ~80% carrier rate, so most wild horses now carry all four body-stat genes.MagicSizeGenestays its own class - it multiplies scale, which has the two-stage natural+magical clamp; the other three have no natural clamp. - New on
TraitBuilder:multiplySpeedUnclamped/multiplyHealthUnclamped/multiplyJumpUnclamped, the exact counterparts ofmultiplyScaleUnclamped- applied after every addition, clamped only to newHorseTraits.MAGICAL_MIN_FACTOR/MAGICAL_MAX_FACTOR(0.1 - 10, the "10x either way" cap). The bounded Gaussian keeps the real reach near 2.04x at two maximal copies, so the guard never fires - same story as size.MIN_HEALTH(0.5 heart) is still applied last, soFrail/Frail+ a disorder can't zero a horse. - It multiplies the natural loci, it doesn't add. A magic-speed horse that is also
mstn=C/Cis faster than a plain magic-speed horse by the same ratio -MagicBodyStatGenesTest.theMagicScalesTheNaturalStatpins it. A magically fast pony is still slower than a magically fast racehorse. - Magic health is deliberately NOT a
HealthContribution. That marker is whathealth.mode = OFFsuppresses, and it is for disorders; turning those off must not strip a horse's magical vigour.Frail/Frailis a horse with fewer hearts, not a sick one - noCondition, never suppressed. (Same call milk'sWatr/Lavalethal made.) - Paints nothing - all outcomes
wildType, soaffectsCoat()false, out of the texture key,GenotypeCatalogcollapses each to one entry:size()unchanged at 462 422 019.totalGenotypes()went to 17 644 404 871 265 791 068 979 200 000 (x216, three loci x 6 carryable pairs). - New
MagicBodyStatGenesTest(16 tests).:common:test348 green,:neoforge-26.1.2:buildgreen.SpecGeneTest.BUILT_IN_GENES39 -> 42. Two brittle statistical tests were widened where the seed-stream reshuffle tipped them:ShowcaseGenotypesTestmagical-share upper bound 0.60 -> 0.66 (it is a floor, not a rate), andGenotypeCatalogTest's raw-product comparison moved toBigInteger(thelonghad already wrapped - gap #13). coat-golden.txtregenerated, but no coat moved - all 450 rows are byte-identical bar the three new=n/ncode segments per line. Per-gene epigenetic seeds are keyed by gene, not by stream position, so inserting genes at priority 141-143 doesn't shift particle / light / verdant / test.- Old saves will not parse. Genotype code went 41 -> 44 segments. Dev only; start a fresh world.
- Deliberately not built: per-part scaling, a coat marking (they are the magic body size family and it has none either), per-allele founder frequencies, environmental noise on the natural stats (still zero - gap #24).
- Docs:
wiki/gene-magic-speed.html/-health/-jump(new),wiki/nav.js(new "Magical body-stat genes" section,gene-body-size.htmlmoved into it),wiki/genetics-model.html(table + counts + raw total),wiki/horse-body.html,wiki/api-reference.html,wiki/modding.html,wiki/roadmap.html,index.html,README.md. Checklist:wiki/verification.html§0d.
Built 2026-09-05, NOT yet play-tested: PAX3 SW2 is the ordinary horse - 90% of founders carry one copy.
Owner's call, and it is how minimal splash works in life: a mild splash allele is near-ubiquitous, and what one copy buys a horse is roughly what most horses look like. Pax3Gene's founder table went from hardyWeinberg(SW2 2%, SW4 0.5%) to a written-out 90% SW2/N / 1% SW4/N / 9% N/N.
- It had to be written out, for two independent reasons. Hardy-Weinberg's heterozygote share is
2pq, which peaks at 50% - there is no allele frequency anywhere that makes 90% of a randomly-mating population heterozygous, so the table has to say it directly. And left to HWE, one copy on nine horses in ten would put 81% of wild horses atSW2/SW2, the bold outcome - not a pattern any more, the base coat. Heterozygotes only, baseline last, theMagicSizeGenerule. - Measured effect on the wild population (20 000 founder draws, 400 baked coats): horses showing any white-pattern locus 33.3% -> 93.6%; mean white coverage of a wild-caught horse 32.5% -> 49.5%; a foal of two wild-caught parents is
splash-bold0.07% -> 20.5%. Bold splash is now the commonest thing a player breeds by accident, and a horse that is alsoMITFsplash is the usual case rather than a rarity - which is the interaction the two-locus split exists to show. - It makes the splash calibration bug load-bearing. The audit that opened this session found that
WhitePattern.splashmeasures its waterline against the whole-horse height (hoof to ear tip, span 33.75) while the barrel top is at 0.622 of that and the legs only reach 0.326. SoS_SPLASH = 0.34puts the line at frac 0.325-0.386 - i.e. all four legs entirely white - and that is now what 90% of wild horses look like. "Minimal marking" currently means four white legs. Fixing the mapping (s = 1should land at the crest, not the ear tip) is no longer optional tuning; see the known-gaps entry. :common:test333 green.coat-golden.txtuntouched - the golden cases are explicit codes, not founder draws, so nothing about any individual coat moved. No format change; old saves parse.- Deliberately not done: the same treatment for the particle locus. It was asked for on the premise that a heterozygote is silent, and it is not -
ParticleGene.expressionOfreturnssingles[a]when the second copy isn, so one variant copy visibly trails its particle. At 90% het, 90% of wild horses would trail something (today: 7.7%). Owner chose to skip it rather than take that tonal shift.
Built 2026-09-05, NOT yet play-tested: face markings become a family - star, stripe and snip.
All four white loci now draw the head from one shared vocabulary, WhitePattern.faceMarking(epi, skin, strength, jag) + WhitePattern.FaceMarking, which closes the standing "every locus draws the same centreline stripe" gap. Machinery is wiki/pipeline.html#face-markings; the shape of it:
- Three components, not eight named shapes. Horsemen name eight or nine markings, but they are three independent components - a patch on the forehead, a band down the nose, a patch at the nostrils - plus one width, and every named marking is a combination of them.
describe()reads the term back off the components ("star and snip", "blaze to the nostrils", "bald face"); nothing ever chose it, which is the check that three booleans and a width really do span the vocabulary.FaceMarkingTestasserts every name is reachable. - Star and snip are the point - both are detached patches, white with coloured face on every side, which the old painter structurally could not draw: it was one centreline band starting at the nose and running back, so its whole vocabulary was stripe / blaze / bald face. Measured: a star-only marking is 12 texels on
HEAD.TOPand nothing on the muzzle; a snip-only is 7 texels onMUZZLE.TOPand nothing on the head. - Face space is
tfrom poll (0) to nose tip (1), measured along body-spacexover head and muzzle together, so the same numbers mean the same anatomy on the adult (separateMUZZLEbox) and the foal (no muzzle box at all). The eyes sit neart = 0.4on both meshes, which is what anchors the star at 0.30 and the snip at 0.90. - Strength picks the distribution, not the marking. A locus does not decide a horse has a snip; it decides how much white the horse tends toward. At
S_MINIMAL0.12 that is star 37% / nothing 29% / snip 9% / star+snip 12%; at sabino 0.42 a bare face is essentially gone; at 0.62 it is a blaze; at 0.93 a bald face three times in five. This is what finally makesW20/Nmean its own description - "a star and a sock" - which was prose nothing implemented. jagcarries the sabino/splash difference onto the face - 0.42 forKIT, 0.11 for the two splash loci, 0.34 for frame. Same reason their body margins differ, one parameter rather than two painters.- A blaze no longer wraps under the jaw.
Face.BOTTOMon the head and muzzle only whitens at bald-face width. The old painter tested the centreline on every plane of the box, so the underside of the jaw went white on every blaze from every locus - a real bug, found while wiring this up. - The draw is fixed and unconditional: one long and eight floats, every time, including for components that turn out absent. The particle locus's lesson, applied: a draw made only when a flag is set silently repaints every horse in every save the first time that flag's odds move.
FaceMarkingTestruns an empty marking and a bald face through a nine-valueFakeRngand asserts both exhaust it. EdnrbGenestopped hand-rolling its own face. It was a bare|z| <= faceHalfover the whole head with no top and no shape; it is nowFACE_STRENGTH = 0.80/FACE_JAG = 0.34on the shared vocabulary, which is the bald-faced pattern frame is supposed to be.FACE_HALF_MIN/_RANGEandWhitePattern.withinFaceare gone.- New
FaceMarkingTest(12 tests).:common:test333 green,:neoforge-26.1.2:buildgreen, creator parity 3 832 checks / 48 cases (untouched - the creator only portsKIT's dominant-white outcome, notWhitePattern).coat-golden.txtregenerated: 330 of 450 rows byte-identical, and the 120 that moved are exactly the rows carrying aKIT/MITF/PAX3/EDNRBvariant. No format change, so old saves still parse - those horses just repaint. - Deliberately not built: giving tobiano a face marking (real tobianos commonly carry a star or blaze, but the gene's documented behaviour is a coloured head and that is a separate call); medicine hat / war shield, which is not a face marking at all but a retention rule on a near-white horse
- logged in
wiki/roadmap.html#natural-genes, and see gap #29 below; surfacingdescribe()anywhere player-facing (it is a natural feed for the gene dictionary and the info panel - gap #10).
- logged in
- Docs:
wiki/pipeline.html(new "Face markings" section),wiki/api-reference.html,wiki/gene-kit.html,wiki/gene-mitf.html,wiki/gene-pax3.html,wiki/gene-ednrb.html,wiki/roadmap.html§4.2. Checklist:wiki/verification.html.
2026-09-04
Built 2026-09-04, NOT yet play-tested: the particle locus - forty alleles on one gene.
ParticleGene (horsegenetics.particle, priority 150), the largest gene in the mod by a wide margin: 40 variant alleles + n, 861 combinations, 87 outcomes. 39 built-in genes, 41 in-game. A horse trails a particle as it moves - flames, souls, snow, hearts, portal motes. Machinery is wiki/gene-particle.html; the shape of it:
- One locus, not forty genes, and that is the whole design. Forty independent two-allele genes would let a horse carry all forty, so "which of these does this horse trail" stops having an answer and every serious line converges on a horse emitting everything. One locus says the opposite structurally: a horse has two copies of the chromosome, so it shows at most two, ever, and a third is not rare but impossible. The
KitGeneargument at forty times the scale. - Rank, and the copy a horse is hiding. Every allele carries a rank and the alleles are declared in rank order - load-bearing rather than tidy, because
AllelePaircanonicalizes onAllele.order(), so declaring them this way is what puts the shown copy in slot 0 and the hidden one in slot 1. Two non-codominant alleles meet and the lower rank wins; the loser is carried silently and passed on intact, indistinguishable from a wild type. That is what makes a locus this wide breedable rather than merely large. - Codominance is by family - one
groupstring per allele, and two different alleles of one group both show at once. Nine families cover 29 of the 40 alleles and produce the 46 double outcomes; the other 11 never stack. The flames and the smokes are one family of eight, not two of four (any-flmstacks with any-smk), which is 28 of the 46 by itself - a unit test pins it, because getting that grouping wrong would quietly delete most of the locus. Two tokens that look related and are not:Dstrn(enchanting glyphs) is noDst,Lmstr(totem sparks) is no-str. - Everything visible about it is epigenetic. The allele names a particle and nothing else; colour, second colour, body site, count and one spare
datanumber are drawn per allele copy, in that fixed order, every time - including for particles that use none of them, because the draw order is the contract and a conditional draw silently rewrites every horse in every save. So twoRflm/nhorses are not the same horse, and each half of a codominant pair is independent: red flames off the front hooves and blue smoke off the tail is one horse nobody designed. A foal that inherits the copy inherits the exact number. - New machinery:
common/genetics/EpigeneticAbilityContribution- the exact twin ofEpigeneticTraitContribution, one layer along.AbilityContributionis a pure function of the genotype, which is right wherever the alleles fix the behaviour (twoHlr/Hlrhorses heal identically, and should); this locus is the case it cannot express.GeneEpigeneticsmoved fromcommon/trait/tocommon/genetics/so both sides can use it without the two packages depending on each other, and gained theforGene(gene, genotype, epigenome)factoryHorseTraitsused to keep private.HorseAbilities.activeForgained anEpigenomeoverload; the translator's per-horse cache is now keyed on both code strings, or it would hand one horse another horse's colours. - The
emitterverb grew, and all of it is usable from a gene file today - which is the property the shared vocabulary was chosen for. New:color2,count(1-16),data(a normalised[0,1)standing in for a shriek's delay or a sculk charge's roll), and five body sites beside the four single-point anchors -spine,hooves,front_hooves,back_hooves,tail. A multi-point site is re-picked per particle, so a firing of four offhoovesreally does come off different hooves. Positions come from the live bounding box and yaw, which already hasAttributes.SCALEapplied, so a magically enormous horse trails from its own hooves. particleForis a registry lookup now, not a name table. Only the ~8 parameterised particles are written out; everything else isBuiltInRegistries.PARTICLE_TYPE+instanceof SimpleParticleType. Forty case labels would have been forty chances to mistype a field name that does not match its own id (TRIAL_SPAWNER_DETECTED_PLAYER_OMINOUSis registered astrial_spawner_detection_ominous), and it would go stale the next time the game adds a particle. The colour-carrying options want ARGB and read the alpha (ColorParticleOption.getAlpha), so a bare0xRRGGBBis fully transparent -DustParticleOptionsis the exception and takes plain RGB, which is why the two are written differently.- The source list was Bedrock particle names. Ten have no Java equivalent and carry a substitute (
magnesium_salts->white_ash,small_soul_fire_flame->copper_fire_flame,sculk_sensor_redstone->vibration,oozing_emitter->item_slime,warden_dig->block(sculk),crop_growth_emitter->happy_villager,evoker_spell->instant_effect, plus four straight renames). Two were cut rather than substituted -BigflmandCndlflmboth land on a particle another allele already has, and Java flames areSimpleParticleTypes carrying neither colour nor scale, so the pairs would have been indistinguishable. Forty alleles, not forty-two. - It paints nothing. All 87 outcomes are wild types, so
affectsCoat()is false, the locus is out of the texture key, andGenotypeCatalogcollapses 861 combinations to one entry -size()is unchanged at 462 422 019. Forty alleles for no catalogue growth at all. GenotypeCatalog.totalGenotypes()is aBigIntegernow, and the old number was already wrong. The particle locus pushed the product past 2^63 - and checking that revealed thelonghad already wrapped before this session: the documented 3 028 898 126 035 238 912 was nonsense, and the true pre-particle figure was 94 874 633 669 214 259 200 000. It is now 81 687 059 589 193 477 171 200 000.size()still saturates atInteger.MAX_VALUE, which is right for an index bound callers loop over; this is a statistic nothing indexes, so the honest answer costs nothing. No production caller - the entrance sign that used it is long gone.- Founder frequency is 0.1% per variant, the same for all forty, because no particle is the ordinary one. Forty of them puts the variant share at 4%, so about 7.7% of wild horses trail something while any named particle is roughly 1 in 500 - and a codominant double is about 1 in 10 000, i.e. you cannot catch one. Same rule as the health loci, pointed at something worth having.
- New
ParticleGeneTest(21 tests).:common:test321 green,:neoforge-26.1.2:buildgreen, creator parity 3 832 checks / 48 cases,runServerboots clean (41 segments,loaded 2 data-driven gene(s), zero errors).coat-golden.txtregenerated - every row moved, because the gene set moved and every derived epigenetic seed moved with its position. - Old saves will not parse. The genotype code went 40 -> 41 segments. Dev only; start a fresh world.
- Deliberately not built: any coat marking for the locus (it does not need one
- the gene is the visible thing, which is the answer to the complaint milk and verdant attract); per-allele founder frequencies (no particle is more ordinary than another); an epigenetic emission rate (density is already the
countdraw, and a second density knob would fight it).
- the gene is the visible thing, which is the answer to the complaint milk and verdant attract); per-allele founder frequencies (no particle is more ordinary than another); an epigenetic emission rate (density is already the
- Docs:
wiki/gene-particle.html(new),wiki/nav.js,wiki/gene-effects.html(the emitter's new fields, the anchor table, which particles read what),wiki/genetics-model.html,wiki/api-reference.html,wiki/modding.html,index.html,README.md. Checklist:wiki/verification.html§0c.
Built 2026-09-04, NOT yet play-tested: seven magical utility genes, designed as a set.
The point of them is combination - broad epigenetic ranges and independent loci, so that a ten-times healer with a striped mane that spreads moss is a horse nobody wrote a line of code for. 38 built-in genes, 40 in-game at the time (39 / 41 now, with the particle locus). Machinery is wiki/gene-milk.html and its six siblings; the shape of it:
MilkGene(horsegenetics.milk, priority 130,Watr/Lava/n) - a bucket gets milk from a grown mare, water from anyWatr/Watrhorse, lava from anyLava/Lavaone. Both variants are recessive to the wild type and to each other, so it is the cleanest double-carrier locus in the mod: you cannot catch a lava horse, only breed one.Watr/Lavais an embryonic lethal - the same path asMET(canOccurfalse, the breeding handler reads the drawn genotype and cancels), but deliberately not aHealthContribution:health.modegoverns disorders, and turning them off must not quietly make a water/lava horse possible.MagicSizeGene(horsegenetics.body_size, 140,Big/Small/n) - codominant, and revised 2026-09-04 after the first draft. Every allele copy carries a percentage and both copies contribute: the size is one plus their sum,Bigpositive andSmallnegative. Six combinations, six outcomes - the one locus in the mod where every combination genuinely differs, which is what codominance is.Big/Smalllands near 1.0 rather than exactly on it, because the two percentages are independent draws; forcing an exact zero would need a special case contradicting "the percentages add", and the residual usefully says "this horse carries both extremes".- The distribution is normal, mean 10% and sigma 7% per copy, floored at 1% so a
Bigallele can never come out making a horse smaller. That shape is load-bearing: 80% of wild horses carry a copy, so if one copy were dramatic then dramatic would be the baseline and nothing would read as unusual. Instead the population has a quiet continuous spread you notice across a paddock rather than on any one horse. - Only heterozygotes are born wild - the founder table is written out (
Big/n40%,Small/n40%,n/n20%) rather than derived from Hardy-Weinberg, because random mating is exactly what it is not. Every doubled horse in the world is one somebody bred: the health loci's rule, pointed at something worth having. - It is the mod's first
EpigeneticTraitContribution, and the first caller of the newGeneEpigenetics.copy(slot)- a codominant gene cannot ask for "the copy that expresses", which would count one allele twice and the other not at all. Rng.nextGaussian()is a new default method: Irwin-Hall (twelve uniforms minus six) rather than Box-Muller, for two reasons that both matter here. Its tails are bounded at +/-6 sigma, so a trait deciding how big a horse is has a hard bound on absurdity and the guard clamps never fire; and all-0.5 inputs give exactly 0, soMidpointRnglands on the mean (Box-Muller would hand it 1.18 sigma below it).- The practical ceiling is now about 2.04x (two copies at +6 sigma), not the first draft's 10x - and it takes two good copies to pass even the natural
MAX_SCALE, which is the point of making it codominant. The magical bounds stay as guards rather than as the design.
- The distribution is normal, mean 10% and sigma 7% per copy, floored at 1% so a
ManeColorGene/TailColorGene(112 / 114,Mnsld/Mnstrp/nandTlsld/Tlstrp/n, sharingHairColorGene) - solid or banded hair in any colour, and the heterozygote is both at once in two different colours. That is the first thing in the mod that needs both allele copies' seeds rather than the expressing one, hence the newCoatBuildContext.epigeneticsForCopy(key, slot); asking for the expressed copy would paint the stripes in the base colour. Two loci and not one, so a red mane and a blue tail is breedable.LightGene(160,Lthf/Ltmn/Lteye/n) - gold hooves / mane / eyes, each glowing, plus torch-strength world light. Ten combinations, seven outcomes, genuinely codominant: three alleles each dominant to the wild type and to none of each other, which no dominance ranking can express and the combination table says in one row each. Any variant copy lights the horse; the alleles decide only where it shows.HealerGene(116,Hlr/n, recessive) - players within 3 blocks mend, and a red stripe runs down the centre of the mane so a player can see it. The stripe's opacity is the gene's one epigenetic value; it says nothing about the healing, deliberately - a mark that encoded a stat would make a horse's value legible from a screenshot.VerdantGene(180,mush/moss/grass/n) - spreads mycelium, moss or grass from the hooves. Every variant needs two of itself;mush/mossis not half of each, it is nothing. A different shape from milk's mutually recessive pair, whose clash is lethal where this one is merely inert.- The three that paint nothing (milk, size, verdant) reuse the non-coat trick: every outcome is a
wildType, soaffectsCoat()is false, they are out of the texture key, and the gallery collapses each to one entry. Seven genes for four genes' worth of catalogue growth.GenotypeCatalog.size()is now 462 422 019 andtotalGenotypes()was 94 874 633 669 214 259 200 000 at this point. (The figure originally recorded here, 3 028 898 126 035 238 912, was a wrappedlong- the overflow was found and fixed when the particle locus landed. See that entry.) - New machinery, four pieces, all in
common/:trait/EpigeneticTraitContribution+HorseTraits.resolve(genotype, epigenome, healthGenetics)- a trait whose magnitude is on the allele copy. Determinism is untouched: theRngis aSeededRngon the expressing copy's stored, heritable seed, exactly as the coat's is.resolve(genotype)with no epigenome now answers with the midpoint (MidpointRng, new), which is the honest answer to a question about a genotype rather than a horse.TraitBuilder.multiplyScaleUnclamped- applied after the naturalMIN_SCALE/MAX_SCALEclamp and bounded only by the newMAGICAL_MIN_SCALE/MAGICAL_MAX_SCALE(0.1-10). The exact counterpart of the coat's uncapped phase-3 accumulator, and it composes right: a magically enormous pony is still smaller than a magically enormous draught horse.coat/pattern/CoatOverlay+CoatOverlayContribution- a fourth coat phase, after the composite and afterredrawEyes. It exists for exactly two things the earlier phases structurally cannot do: colouring the eyes (phase 5 restores them from the template, so a gene wanting them has to run later) and carrying an emissive texel mask ("this glows" is not a colour, so neither accumulator has a channel for it).CoatTextureComposer.bakereturnsBaked(argb, emissive);composeis the pixels alone, so the golden test and every existing caller were untouched.genetics/AbilityContribution- a built-in gene can now grant game behaviour, using the sameGeneAbilityvocabulary a data-driven gene'seffectsblock parses into.SpecAbilitieswas renamedHorseAbilitiesbecause it stopped being about the spec path. The alternative - a second vocabulary for built-ins - would have meant writing the translator twice and would have let a behaviour exist for Java genes and not for gene files.
- Two new effect verbs, both written for a built-in gene and both usable from JSON today, which is the property the shared vocabulary was chosen for:
healing(target / radius / amount / interval /max_targets- the capwiki/gene-effects.htmlrequires of any radius effect) andspread(cover/ radius / chance / interval).spread'scoveris a vocabulary word, not a block id: what "spreading moss" eats is a family of conversions plus a rule, and that judgement needs the block registry, so it lives in the translator. - The emissive path is now texel-level.
getOrCreateEmissivefolds the bake's own mask together with a specGlow's part list, and the renderer asks for it every frame (aNO_GLOWsentinel is cached, becausecomputeIfAbsentwill not store anulland would recompose the coat per frame for every ordinary horse). That is what lets light glow four hooves and two eyes, neither of which is aPart. - Gold eyes keep a pupil.
CoatOverlay.shadeTowardscales the target colour by the texel's own luma before blending, so the sclera goes gold and the pupil stays black; a flat lerp turned the whole eye into one gold rectangle. - The size gene broke the walking animation, and the fix is one line. Vanilla advances the leg-swing phase (
walkAnimationPos) from the world distance the entity moved and nothing else; the only size compensation anywhere in it is a hard-codedisBaby() ? 3.0F : 1.0F. Nothing consultsAttributes.SCALE, because before this mod nothing changed it - so a scaled horse walked with its feet sliding, worse the bigger it got, and mirrored on a small one.GeneticHorseRenderer.stretchGaitToSizedivides the phase byrenderState.scaleaftersuper.extractRenderState, which makes a bigger horse take proportionally longer, slower strides. Amplitude (walkAnimationSpeed) is deliberately untouched - it is a 0-1 multiplier on an angle, and an angle already scales with the model. No-op at scale 1. - The spawn egg previews size.
CustomHorseSpawnScreenresolves the genome'sTraitsand scales the preview model by it, so Reroll epi. visibly resizes the horse, with asize 1.14xreadout under the panel whenever it is not ordinary size. Framing is deliberately not refitted to the result - fitting a big horse back into the panel would cancel exactly the thing being previewed - so the model is capped at 2.5x and the readout says(preview capped)past that. The number never caps. - Founder tables use a
LinkedHashMap, baseline last -Map.ofiteration order is salted per JVM start, so a multi-allele table built from one would have made a world's founders unreproducible. Caught byGenotypeTest.randomDrawsOneFloatPerGeneInGeneOrder. coat-golden.txtregenerated (75 cases now, up from 57): every row moved, because the gene set moved and every derived epigenetic seed moved with it. NewMagicalUtilityGenesTest(32 tests).:common:test300 green,:neoforge-26.1.2:buildgreen, creator parity 3 832 checks / 48 cases,runServerboots clean (40 segments,loaded 2 data-driven gene(s)).- Old saves will not parse. The genotype code went 33 -> 40 segments. Dev only; start a fresh world.
- Deliberately not built: a coat marking for milk or verdant (both are invisible until you put a bucket under the horse or watch the floor - logged as a gap, and it sits awkwardly beside healer, which draws a stripe precisely so you can see what it does); an epigenetic colour for light (vanilla light has no hue, so a blue-glowing horse would still cast white light); any stat change from the size locus (a ten-times horse is a spectacle, not a better horse); milking's §7 rules - tamed, full health, once a day, the stallion kick - four of which are limits of the effect vocabulary rather than of milk, so they are logged in
wiki/roadmap.htmlrather than special-cased. - Docs: seven new
wiki/gene-*.html,wiki/nav.js,wiki/gene-effects.html(the two verbs + the "both kinds of gene" note),wiki/pipeline.html(phase 6),wiki/horse-body.html(the epigenetic twin + the two-stage scale),wiki/genetics-model.html,wiki/api-reference.html,wiki/modding.html(walkthrough 4 + both-copies + epigenetic traits),wiki/horse-traits.html,wiki/roadmap.html§7,index.html. Checklist:wiki/verification.html§0a.
Built 2026-09-04, NOT yet play-tested: the horse dimension goes back to random pens
(roadmap §8, done). The genotype gallery is retired before anyone ever walked it.
common/genetics/ShowcaseGenotypes(new, pure, 6 tests) is the draw: an ordinaryGenotype.randomfounder roll with a floor under it. Always at least one natural coat gene expressing beyond extension and agouti; withMAGICAL_CHANCE= 0.5, a magical one too. A draw that already clears the floor is left exactly as it fell.- Two rules keep it honest. Forcing only ever keeps a combination that actually expresses in this genotype (
Genotype.shows), so a chestnut is never handed an agouti it will not paint. And a combination thatmasksneither counts toward the floor nor is ever forced - a quarter of all founders carry the diagnostic test gene, which paints flat over everything, so counting it would quietly exempt a quarter of the corridor. - The one honest gap: the floor guarantees a gene expresses, not that it is perceptible. Mushroom on a black horse is a real expression that looks like nothing, and nothing in the model can answer "would a player see this". Accepted rather than special-cased; flagged in
wiki/verification.html. Genotype.with(AllelePair)is the founder-only setter it needed - one locus replaced, the shape ofwithSex, which now delegates to it.- The corridor is a fixed 2 000 pens (
DebugPenManager.PEN_COUNT, ~7 000 blocks), an arbitrary number rather than one derived from the genotype space - which is the whole point.MAX_GALLERY_PENS,galleryPens()and the catalogue tally sign are gone; the entrance sign now readsHorse Pens / 2,000 pens / random genome / mare + stallion.PLOT_SPACING_Xdrops from 2 526 to 8 007. - Two correctness fixes the randomness forced.
buildPennow clears untamed horses in the pen before stocking (a pen is built once per plot, so anything standing there belongs to a previous occupant of that recycled X slot and has nothing to do with the sign just written); andplotBoxcovers the whole X slot rather than stopping athighestIndex, or a slot where a previous visitor walked further would accumulate horses forever. The "leaving clears entities, not blocks" claim still holds, for a new reason - see the horse-dimension section. GenotypeCatalogis untouched and still used, by the tests and by whatever punnett display gets built - it just stopped driving the dimension.size()is still 2 064 387. Known gap #12 is closed by this rather than by a cap.- The catalogue's per-entry sign-fits test stays;
ShowcaseGenotypesTestadds the property-test twin over random draws the roadmap asked for (a sign may overflow its last line, it may never drop a gene). :common:test268 green,:neoforge-26.1.2:buildgreen.coat-golden.txtuntouched - nothing about the pipeline or any gene moved.
Built 2026-09-04, NOT yet play-tested: the custom horse spawn egg's editor was rebuilt
(roadmap §9, most of what was left). client/ CustomHorseSpawnScreen is now gene list left / live 3D horse centre / controls right, and the horse that spawns is the horse you were looking at.
- Every registered gene is a row, alphabetically by
Gene.name()- "ACAN, Agouti, B4GALT7, Champagne, EDNRB (frame overo), KIT (white spotting)..." rather thanhorsegenetics.ednrbin registry order. The sex locus is excluded - the Mare/Stallion button owns it. The old modal+ Add genepicker is gone; the list is the picker. - The list is a catalogue you add from. A gene starts off the horse and draws as a plain name; clicking the row adds it, and only then does it grow two allele buttons and an
x. Every gene carrying two baseline buttons from the start was a wall ofN/Nthat said nothing - a horse carries every locus whether or not you have touched it, so what the list is for is picking the handful you want to see.Row.addedis held explicitly, not inferred from the alleles, so a gene you added and cycled back to baseline stays on the horse. - A gene is added homozygous for the allele that does something - the first allele that is not
defaultAllele(), doubled. If a horse cannot carry that pair (KIT's four nonviableWhomozygotes,MET'smet/met) the next one it can, and failing all of them one copy against the baseline. Either copy can then be cycled anywhere, baseline included. - A row that expresses says so - the name turns green and the
Expression.name()prints under it (no effectotherwise). That is one more consumer for the expression prose (known gap #10). - A live 3D preview in the middle, reusing
FamilyTreeScreen's technique exactly: a throwaway client-onlyHorse, theCoatDatainjected straight ontoGeneticHorseRenderState, never throughClientCoatCache. The coat is rebuilt only when the genotype-plus-epigenome key moves, and the texture factory caches bytextureKey()on top of that, so editing is cheap. Reroll epi.is the point of the change. The screen holds a realEpigenomeand previews with it, so re-rolling flips through the bay leg heights / grey dapples / splash edges one genotype can produce - the first surface anywhere that makes per-allele epigenetics visible. It is then sent with the genotype (SpawnCustomHorsePayloadgainedepigenomeCode) and written straight in via the newHorseRecords.newFounder(horse, rng, Genome). The server used to roll its own, which made the preview a suggestion rather than a preview.- Copy code / Paste code round-trip the genotype code through the clipboard (roadmap §9's "turns every bug report into something reproducible"). Genotype only - the epigenome has its own button, and a code you can paste into chat wants to stay one line. A pasted locus sitting at its baseline is not marked added, or a paste would come back as 30-odd rows of
N/N. Plus a Clear genes. - Creative-only, re-checked on the server.
handleSpawnCustomHorsenow requiresgetAbilities().instabuildand that the sender is holding the egg; the payload spawns an arbitrary entity with an arbitrary genome, so the client-side gate was worth nothing on its own. The survival egg-consumption path is gone with it - it can't be reached any more. - Still not built (the remainder of roadmap §9): the per-allele epigenetics editor proper - each copy's priority and seed shown individually with a type-it-in field. At 30-odd genes that is 60 fields, so it wants a per-gene expander rather than a flat list.
Built 2026-09-04, NOT yet play-tested: the trait / size / health system, and the death of the random stat roll (roadmap Tier 2 §6.1, Tier 3 §7, §4.3, §4.4, §6.2-6.4).
Thirteen new genes, a new common/trait/ package, and the removal of the last non-genetic randomness on a horse. 31 built-in genes, 33 in-game. Machinery is wiki/horse-body.html; the shape of it:
HorseStatsis deleted. A foal's speed and health used to be a uniform draw from[0.75*min(parents), 1.5*max(parents)]- an uncapped random walk with no genetics in it, where two full siblings could differ by a factor of two and "breeding for speed" was breeding for luck.HorseRecordlost itsspeedandhealthfields (andhasStats/withStats/ceilSpeed/ceilHealth), exactly as it lostsex: a stored derived value can only go stale against the alleles beside it.HorseRecord.traits()resolves on demand.StoredGenomeandHorseRecordCodecslost the two fields too.common/trait/-HorseTraits.resolve(genotype[, healthGenetics])walksGenes.codeOrder()once, hands everyTraitContributionaTraitBuilder, and returns aTraits(speed, health, jump, scale, conditions). Pure: noRng, no epigenetics, no entity. Baselines0.1875 / 22.0 / 0.5 / 1.0, a little under vanilla's midpoints so the variant alleles are what push a horse up.MIN_HEALTH = 1.0- a genetic health value must never resolve to zero.- One capability interface, not the roadmap's four.
TraitContribution(+ aHealthContributionmarker) replacedStatContribution/BodyContribution/ViabilityRule/ConditionRule: they all run in the same walk, and the genes that need any of them mostly need several at once. Viability is derived from the worstCondition.severity(), so a gene cannot declare a horse lethal without saying what killed it. - Additions are applied before multipliers, so trait resolution is order-independent by construction and gene priority buys nothing here. Multipliers exist only for scale, because dwarfism is proportional - a dwarf pony is smaller than either alone.
- All thirteen genes paint nothing: every expression is
wildType, which now explicitly means "changes nothing about the coat". SoaffectsCoat()is false for all of them, they are out of the texture key, andGenotypeCatalogcollapses each locus to one entry -size()is unchanged at 2 064 387, whiletotalGenotypes()went to 292 822 943 423 500 800. Thirteen genes for zero gallery growth; the sex locus's trick, reused. - Priority sub-band 80-99 is now "non-coat": mstn 80, pdk4 81, ckm 82, ryr2 83, lcorl 84, hmga2 85, acan 86, b4galt7 87, plod1 88, rapgef5 89, st14 90, shox 91, met 92. They sort after every painting gene and before the magical band.
- Performance / size (6):
MstnGene(codominant; eachCbuys 0.020 speed and costs 2 health - with no stamina resource, endurance is paid in hearts),Pdk4Gene,CkmGene(speed),Ryr2Gene(jump strength is tracked for the first time),LcorlGene+Hmga2Gene(size). Attributes.SCALEanswers the roadmap's flagged-unverified question. It exists in 26.1.2, is onLivingEntity.createLivingAttributes(so every living entity has it), and vanilla scales the model and the hitbox from it. No renderer work, no hitbox work - the whole size system is one attribute write.JUMP_STRENGTHis there too.- Health (7):
AcanGene(5 alleles, 15 combinations; affected is "no working copy left", soD1/D4is affected - the check is a predicate on the pair, nothomozygous(), which is the clearest argument yet for the combination table),B4galt7Gene(the one survivable disorder),Plod1Gene,Rapgef5Gene,St14Gene,ShoxGene(lethal at birth) andMetGene(lethal at conception,canOccur = false). Six of them extend a newRecessiveDisorderGenebase - two alleles, three combinations, only the double-variant does anything. - Four colour genes gained a disorder:
SilverGeneZ/Z-> MCOA (impairing, -2 health),EdnrbGeneO/O-> overo lethal white, which now actually kills (closing the "the death is not modelled" gap), andMitfGene/Pax3Genehomozygotes -> deafness, shared as one condition with two causes (TraitBuilderde-dups on id) andINFORMATIONAL, because the mod has no hearing to take away. - No founder is ever affected - verified over 200 000 draws. Every health founder table lists only the clear horse and the carrier: a wild-caught horse is an adult that survived, so the only way to see a disorder is to breed two carriers. Carrier rates 1.4%-4%.
server/LethalFoalHandler- born, then dies. Stores nothing: being lethal is a property of the genotype, so it re-reads it each second and a foal that logs out mid-death still dies on the way back in. Damage is 28% of the foal's own max health, floor 2, once a second, which out-damagesHorseCareHandler's regen at any health value (roadmap §7's open item, closed). Guards: babies only (an adult spawned by the egg is a debug tool for looking at a coat), not in the horse dimension, and only onhealth.mode = FULL. New datapack damage typehorsegenetics:genetic_defect+ a lang key.- The conception lethal is one branch:
applyBredFoalnow returnsboolean,onBabySpawncancels the event onfalse, andStallionSeedJarHandlerdiscards the foal (spending the jar and the mare's love, as a real pairing would).Genotype.breedWithis untouched, so the odds stay the ordinary one-in-four. ServerConfig(the mod's first) -health.mode=FULL(default) /NO_DEATHS/OFF. The genes are registered and inherited identically in all three; the setting only governs whether what a horse carries is allowed to affect it.OFFisresolve(g, false), which skips everyHealthContribution.- Surfaces read it. The info panel shows speed / health / jump / size (as a word) plus a condition list, reading speed and health off the live entity attributes (authoritative and synced) and the conditions off the genotype; the paper dump prints all four plus every condition with its description. That partly closes old gap #10 - the expression/condition prose is finally read by something.
coat-golden.txtregenerated: 312 of 342 rows byte-identical. Only the 30 rows involving pink hair / magic zebra / test moved, because those three genes shifted position incodeOrder()and their derived epigenetic seeds moved with them. Nothing else about any coat changed - the pipeline was not touched. NewHorseTraitsTest(10) +HealthGenesTest(8);:common:test262 green,:neoforge-26.1.2:buildgreen,runServerboots clean (33 segments,loaded 2 data-driven gene(s), the server config file generates correctly).- Old saves will not parse. The genotype code went 20 -> 33 segments. Dev only; start a fresh world.
- Deliberately not built: DMRT3 (gait - needs animation work); per-part scaling for the two dwarfisms (
SCALEis one number for the whole entity); ST14's near-hairless coat (phase 1 only removes pigment, and a de-pigmented mane reads as a white mane); environmental noise on the stats (deliberately zero - see the gaps list). (CSNB shipped 2026-09-06 with the leopard complex.) - Docs:
wiki/horse-body.html(new, the machinery), thirteen newwiki/gene-*.html,wiki/nav.js,wiki/breeding.html,wiki/genetics-model.html,wiki/api-reference.html,wiki/modding.html(walkthrough 3),wiki/roadmap.html§§4.3/4.4/6/7,wiki/gene-silver.html,gene-ednrb.html,gene-mitf.html,gene-pax3.html,index.html,README.md. Checklist:wiki/verification.html§0b.
Built and owner-verified in-game 2026-09-04: the white-pattern rewrite - four real loci replace four made-up genes.
WhiteGene, SplashGene, SabinoGene and FrameGene are deleted. In their place, named for the genes they model: KitGene (horsegenetics.kit, priority 76, eight alleles), MitfGene (horsegenetics.mitf, 78, four), Pax3Gene (horsegenetics.pax3, 79, three) and EdnrbGene (horsegenetics.ednrb, 74, two). Still 18 built-in genes, 20 in-game. The rule the owner set: only alleles at exactly the same locus share a gene - so tobiano (an inversion near KIT, not a KIT variant) and roan (region-mapped, causal change unresolved) stay their own genes and compose freely with everything. Landing with it:
KITabsorbs dominant white and sabino, which were two alleles of one real gene modelled as two independent genes - so a horse could be homozygous sabino and dominant white, a genotype that cannot exist because a horse has two copies of chromosome 3 and no more. AllelesW22/W13/W10/W5/W23/SB1/W20/N; 36 combinations, 32 carryable, 8 outcomes.W20is the booster the source describes (subtle alone, adds white beside another variant);SB1is the one documented viable dose series; the rest are "dominant with variable expression". Only a table says all three at once.- Splash splits in two, because it really is two genes (
MITFandPAX3). That is not bookkeeping: a horse carrying one copy at each is markedly whiter than either alone - a genotype one gene cannot express at all, since one gene has two slots.SW6-SW8are deliberately folded intoSW5: the source describes all four in word-for-word identical terms, so four alleles would be four indistinguishable rows. EDNRBgainslethal-white.O/Ois Overo Lethal White Syndrome, and the model now distinguishes two kinds of lethal. An embryonic lethal (KIT's four nonviableWhomozygotes,SW3/SW3,SW4/SW4) getscanOccur = false- no pen, not counted, not a founder.O/Ois born, so it occurs, has its own masking all-white outcome and gets a pen; it is simply absent from the founder table, because a founder is an adult horse. The death is not modelled (no health system) - the foal lives. Deliberate;wiki/roadmap.html#health-genes.- Two shared painters,
coat/pattern/WhitePattern-sabino(theKITshape: ragged margins growing inward from legs, belly, face, then torn body patches) andsplash(theMITF/PAX3shape: a hard, wobbled waterline rising up the horse). Each takes onestrengthin[0,1]; each outcome picks a number on that ramp. One painter per family, not per allele - the difference between two alleles at one locus is overwhelmingly a difference of degree, and eight bespoke painters would be the same painter eight times pretending the differences were principled. - "White finds white": both painters read the coat they are handed and raise their own strength by how much of it is already de-pigmented (splash
0.55, sabino0.35). This is load-bearing, not a flourish. Painted blindly,SW1/N + SW2/Nmeasured 44% white against 45% and 42% alone - two waterlines at the same height are one waterline, and the whole point of the split would have been invisible. With it: 70%. It is also what makesW20a booster and what makes frame-plus-splash louder than either. One line per painter, no interaction table anywhere. - Two rendering bugs caught in the sample bakes and fixed before landing.
KITbody white thresholded a high-frequency fractal and came out salt-and-pepper - a perfectly good roan and completely wrong for sabino; it now uses a low-frequencyPatchNoise.fieldplus a fine jag on the threshold, the same recipe frame uses. And the splash waterline's one-pixel fade painted a gold fringe along the whole horse (a half-scaled black texel samples the LUT's warm diagonal), so the cut is now hard and the irregularity comes from wobbling where the line falls. FounderTable.hardyWeinberg(Map<Allele,Double>, Predicate<AllelePair>)- the multi-allele generalisation.KIThas 36 combinations and four homozygous lethals; hand-tabulating that is not transparency, it is an invitation to a typo nobody would ever see. Excluded combinations are dropped and the rest rescaled - which is the biology: a lethal is absent from the adult population you observe.GenotypeCatalogis now computed on demand, not materialised.size()is arithmetic;get(i)reads an odometer over each gene's non-masking distinct pairs, with the one entry per masking combination appended after them (so masked pens moved from mid-corridor to the tail). It had to change: the catalogue is 2 064 387 pens and an eagerList<Genotype>that size is hundreds of megabytes.totalGenotypes()is 55 099 802 880.- The gallery was capped. 2M pens is a ~7.2M-block corridor - a quarter of the way to the world border, leaving room for four plots in the dimension.
MAX_GALLERY_PENS= 20 000 held it for one session. Superseded 2026-09-04: the corridor is random pens now and there is no cap, no catalogue tally sign and noMAX_GALLERY_PENS. CoatRegions.whitenLowerLeg/whitenBlazenow have no callers. Both cut hard, which is why every splash sock used to end in a perfect ring - that old known gap is gone with the gene rather than fixed. Kept as helpers, with the caveat written into their javadoc.- Measured coverage through the real pipeline (three seeds):
KIT5.7 / 11 / 15 / 25 / 25 / 73 / 94 / 100 %;MITF45 / 72 / 91;PAX342 / 70;EDNRB42 / 100. NewWhitePatternGenesTest(12 tests) pins that each table is total, that every declared outcome is reachable, that the ladder is monotone at every step, that the viability rules hold, and that the two splash loci andW20actually stack. Also closes old known gap #12:CoatPipelineGoldenTest.overridenow throws on an unknown gene instead of silently leaving the segment alone, and the five stalecream/pearlcases were re-pointed atmatp. coat-golden.txtregenerated (57 cases now, up from 42); every coat changed - the gene set moved, so every founder draw and every derived epigenetic seed moved with it.:common:test248 green,:neoforge-26.1.2:buildgreen, creator parity 3 832 checks / 48 cases,runServerboots clean (20 segments,loaded 2 data-driven gene(s)).- Old saves will not parse their white-pattern loci. Dev only; start a fresh world. Summon tokens moved:
kit=SB1/N,mitf=SW1/N,pax3=SW2/N,ednrb=O/N. - Deliberately not built: the leopard complex (
LP/TRPM1) andPATN1- the appaloosa family. It is a new pattern family (leopard, blanket, snowcap, varnish roan, plus white sclera / striped hooves / mottled skin), not an overhaul of the white markings that exist: it needs a spot field and a blanket mask, neither of whichWhitePattern's two shapes cover, andPATN1would be the model's first modifier gene. Logged inwiki/roadmap.html#natural-genes. - Owner-verified in-game 2026-09-04, as a general confirmation that it all renders correctly rather than an item-by-item walk of the checklist. The one thing left in
wiki/verification.htmlis the lethal-white breeding ratio - that twoO/Ncarriers throw an all-white foal about one time in four - because that is a statistic over many foals, not something a play session can see, and recording it as confirmed would be overstating what was checked. - Docs:
wiki/gene-kit.html,wiki/gene-mitf.html,wiki/gene-pax3.html,wiki/gene-ednrb.html(all new;gene-white/gene-splash/gene-sabino/gene-framedeleted),wiki/nav.js,wiki/genetics-model.html,wiki/pipeline.html(the "white finds white" section),wiki/api-reference.html,wiki/modding.html,wiki/roadmap.html§4.2 / §5.2,index.html,README.md. Checklist:wiki/verification.html§0.
2026-09-03
Built 2026-09-03, NOT yet play-tested: dun becomes a three-allele locus (roadmap §4.1).
DunGene now carries D / d1 / d2 - six combinations, three outcomes - and d1 is the allele that draws the dorsal stripe without diluting anything, so a horse can carry primitive markings and not be a dun. Landing with it:
- The locus with two dominance orders. Dilution reads
D > d1 = d2; marking readsD = d1 > d2. No single label covers both, which makes this the clearest argument yet for the combination table - clearer than MATP, which at least had one order. d2, not the old catch-alld, isdefaultAllele()(the only allele that draws nothing). So the allele tokens moved: summon withdun=D/d2.CoatSampleTool,CoatPipelineGoldenTestand the threeLegacyCodetest strings were updated with it.- How an undiluted horse shows a darker stripe. Phase 1 is downward-only, so
d1cannot paint a dark line - and does not need to: a primitive marking is countershading, so both marked outcomes run one painter and differ only in constants, the marking being the region the dilution is lerped off.d1never touches black (the gradient's wholeblack = 1row is pure black andPURE_BLACK_ALPHAgives such a texel 80% opacity, so nudging it off that row makes it fully opaque and therefore darker than its own stripe) and takes red only where there is red -keepRedramps up to 1 as the texel's black rises, the mirror ofD's ramp down to 0. Consequence, and the right one:d1on a solid black composes byte-identically to a plain black horse. A real non-dun black shows no markings either. - Leg bars stay
D-only (primitive(..., legBars));d1is the dorsal stripe alone. - Founder table written out as six weights, Hardy-Weinberg at
p(D) = 1/24andp(d1) = 1/10. The threeDrows still sum to 8.16%, exactly the old two-allele number -d1split the non-dun population rather than making duns rarer. About 18% of wild horses now carry a stripe and no dilution. - The gallery grew by half: dun contributes 3 distinct pens instead of 2, so
GenotypeCatalog.size()is 147 458 andtotalGenotypes()is 1 033 121 304 (dun'sallPairsOfdoubled, 3 → 6). coat-golden.txtregenerated: all 240 pre-existing rows are byte-identical, including everyDone - the only change is 12 newd1cases. Nothing about any other coat moved. NewDunGeneTest(8 tests) pins the table, the founder shares, the spine-vs-flank contrast on both marked outcomes, the black-coat no-op and theD-only leg bars.:common:test235 green,:neoforge-26.1.2:buildgreen.- Also fixed in passing:
:common:bakeCoatSampleshad been broken since the MATP merge - itsbuild()helper still wrote positional code segments into what is now a gene-keyed string, and three samples named the retiredcream/pearlgenes. Both fixed; twodun_marked_*samples added. - Old saves will not parse their dun locus. Dev only; start a fresh world.
- Docs:
wiki/gene-dun.html(rewritten),wiki/genetics-model.html,wiki/roadmap.html§4.1. Checklist:wiki/verification.html§0.
Built 2026-09-03, NOT yet play-tested: sex is a gene (roadmap §5.3, first half).
SexGene (horsegenetics.sex, priority 1 - the first gene in codeOrder()), alleles X/Y: X/X is a mare, X/Y a stallion, Y/Y cannot occur. 18 built-in genes, 20 in-game. Landing with it:
- Both outcomes are wild types - the first gene in the model that paints nothing, ever. So the gallery does not widen (
GenotypeCatalogcollapses every wild type into one group): still 98 306 pens, whiletotalGenotypes()doubles to 516 560 652 (every genotype now genuinely comes in two sexes). Gene.affectsCoat()(derived: "is any of my outcomes not a wild type?") plusGenotype.coatCode()-toCode()minus the genes that can never paint.CoatData.textureKey()runs on that, so a mare and a stallion of the same colour still share one baked texture instead of doubling the cache. Same reasoning as the epigenetics already excluded from the key.Gene.canOccur(AllelePair)(defaulttrue) -GenotypeCatalog. allPairsOffilters on it, soY/Ygets no pen and is not counted.expressionOfstill answers for it (a hand-written code reads as a stallion) because parsing is tolerant.HorseRecord.sexis gone as a field -sex()reads the locus out ofgeneticCodeviaGenotype.sexOf(String)(a segment scan, not a full parse: the info panel asks per frame). The"sex"codec key andStoredGenome'ssexcomponent went with it, andHorseRecords.randomSexis deleted.HorseRecord.founder/bredlost theirSexparameter.- A foal's sex is inherited, not rolled - the dam is
X/Xand gives anX, the sire gives hisXorY50/50, all through the samebreedWithdraw as every other gene. No special case anywhere.Genotype.withSex(Sex)/Genome.withSex(Sex)is the founder-only way to choose one (the custom spawn egg); the egg's gene list leaves the sex locus out, since its Mare/Stallion button owns it. coat-golden.txtregenerated - 90 of 240 rows are byte-identical (the deterministic coats; the pipeline didn't move), the other 150 shifted because the code gained a segment and every gene's derived epigenetic seed moved with its position.:common:test227 green,:neoforge-26.1.2:buildgreen, parity 3 832 checks / 48 cases,runServerboots clean (20 segments,loaded 2 data-driven gene(s)).- Old saves will not reproduce their horses. Dev only; start a fresh world.
- Docs:
wiki/gene-sex.html(new),wiki/genetics-model.html,wiki/breeding.html,wiki/pipeline.html,wiki/api-reference.html,wiki/roadmap.html§5.3. Checklist:wiki/verification.html§0.
Built 2026-09-03, NOT yet play-tested: the combination-table rewrite (roadmap Tier 1 §2, Tier 2 §5.1/§5.2).
DominancePattern is deleted. A gene no longer declares a dominance label; it declares an Expression per distinct outcome - id, display name, a human-readable description, a wildType flag ("this combination changes nothing"), a masks flag, a deterministic flag and its own paint function - plus one function expressionOf(AllelePair) mapping any combination to one of them. Several pairs sharing an expression is what "dominant" meant; only the double-variant landing on a non-wild-type outcome is what "recessive" meant; two variant alleles each with an outcome plus a third for the pair of them is codominance. Works for any number of alleles with no special case. Landing with it:
Gene.randomPair(rng)→FounderTable- a weight per allele combination as percentages, sparse (an unlisted combination never occurs), normalised-with-a-warning, onenextFloat()per gene per founder, with aFounderContextfor a genome-aware distribution that throws if asked about a gene not yet rolled.FounderTable.hardyWeinberg(variant, baseline, p)is the convenience that reproduces the old "1 in N per allele" numbers. The test gene is why this matters: 25% of founders areT/tand none areT/T, which no per-allele frequency can express.- Cream + pearl →
MatpGene(horsegenetics.matp, priority 40) - the proving case and the mod's first three-allele locus:Cr/prl/N, six combinations, four outcomes. Kills the impossibleCr/Cr-and-prl/prlgenotype;coat/pattern/CreamPearlDilutionis gone (the dose table is a six-rowswitchon the gene). 17 built-in genes at the time (18 now, with sex); 19 in-game, 20 now. Allelelostvisible/deterministic(both are properties of a combination) and gained anorder- its index inalleles(), which is a slot order forAllelePair's canonical form and not dominance.Gene.precedenceandAllelePair.dominant()are gone.Gene.wildType()→defaultAllele()- what a code segment-less gene reads as, a parsing default. The word "wild type" now means an expression.- The JSON gene format went to
"format": 2-dominanceandwildOddsreplaced by anexpressionstable (each entry withwhen,wildType,masks,description, its ownlayersand its owneffects) and afounderstable. The parser proves the table is total and unambiguous over everyn(n+1)/2combination: a gap, an overlap, or an unreachable catch-all is a load error naming the offending combination. Both shipped genes and all six examples rewritten. - The epigenome moved onto
HorseRecord- see the separate bullet below. GenotypeCatalog.distinctPairsOfnow groups pairs by the expression they land on, with every wild type counting as one group ("changes nothing" is one look). Exact rather than an approximation, and it shrank the gallery from 331 778 pens to 98 306 (splash 3→2, MATP 9→4);totalGenotypes()was 258 280 326 (516 560 652 since the sex gene).coat-golden.txtregenerated - every coat changed, because founder draws and the gene set both moved.:common:test213 green at the time,:neoforge-26.1.2:buildgreen, parity 3 832 checks / 48 cases (up 48: the fixtures now pin which expression each combination resolves to),runServerboots clean (19 segmentsat the time, 20 since the sex gene;loaded 2 data-driven gene(s)).- Old saves will not reproduce their horses - the code lost a segment and the founder draw changed. Dev only; start a fresh world.
- Docs:
wiki/genetics-model.html#combinations,wiki/gene-format.html,wiki/gene-matp.html,wiki/modding.html,wiki/api-reference.html. Checklist:wiki/verification.html§0.
Built 2026-09-03, NOT yet play-tested: the epigenome lives on HorseRecord.
HorseRecord gained epigenomeCode beside geneticCode (plus genotype() / epigenome() / genome() / hasGenome() / withGenome()), and data/HorseCoatAttachment + the horsegenetics:horse_coat attachment are deleted. Both are heritable facts assigned once at birth, so storing the genotype in two places was one fact twice - and keeping the epigenome off the record is why FamilyTreeScreen had to invent an ancestor's coat from its UUID. It now draws the real coat (closing old known gap #9's second half). HorseRecords.newFounder rolls the whole genome; HorseBreedingHandler / StallionSeedJarHandler read the parent genome off the record; the record attachment gained copyOnDeath. HorseRecordCodecs serialises epigenome_code as an optional field defaulting to "", which is the hasGenome() sentinel.
Built 2026-09-03, owner-verified: the earlier data-model rewrite (roadmap Tier 1, §2).
Every Gene now declares int priority(); codeOrder() / naturalOrder() / magicalOrder() are all derived by one sort on (priority, key) over built-ins + SpecGenes together (no hand-written lists, loaded genes interleave by priority). The genotype / epigenome code strings became gene-keyed and tolerant (<geneKey>=<a>/<b>; missing gene = wild type, unknown gene = dropped, "" = wild type) - see "The genetics model" below. coat-golden.txt regenerated: every deterministic coat is byte-identical (the pipeline is untouched); non-deterministic rows shifted because Epigenome.random / fromSeed now draw per-gene seeds in the new codeOrder() - a stored epigenome code round-trips unchanged, only the seed-derived stand-in moved. :common:test 195 green, :neoforge:build green, runServer boots clean (20 segments with the two shipped spec genes). NeoForge needed no source changes - it delegates all code parsing to common/. Closes old known-gap #18 (GeneCodeDisplay now derives its gene list, so spec genes show). Owner-verified in-game 2026-09-03: wild horses spawn and render correctly, right-click paper genome dump works. Still unconfirmed: bred foal, seed-jar round-trip, a spec gene actually showing in the display (needs a horse carrying Suntouched/Waterborn) - wiki/verification.html §0.
Standing build state
common/
- compiles; the JUnit suite passes (
./gradlew :common:test— it reports the count, which is why one is not written here). Coversbreed/(the breed system:Breed/Breeds(49) /BreedFounder/BreedLineage/BreedStatCurve/Commonness->wiki/breeds.html),trait/(the non-coat body:HorseTraits/Traits/Condition/TraitBuilder/EpigeneticTraitContribution/ the breedStatAxis+TargetBand+BreedStatTargets->wiki/horse-body.html) andgenetics/(allele/gene model - 48 genes, 24 that paint and 24 that never do: sex, the 18 natural ones (extension, agouti, champagne, grey, MATP (cream + pearl, three alleles), dun (three alleles), silver, mushroom, brindle (the one X-linked gene), tiger eye (eyes only), roan, tobiano, the four white-pattern lociKIT(eight alleles - sabino + theWseries + dominant white),MITFandPAX3(splash, which really is two genes) andEDNRB(frame + lethal white), and the leopard complex -LPplus the two silent modifiersPATN1/PATN2it reads viaexpressionIn, eight outcomes, the model's only cross-locus gene), magic zebra + pink hair, the magical utility + body-stat genes - mane colour + tail colour (three alleles each, a per-copy hue), healer, light (four alleles, codominant), milk (three alleles, one lethal pair), magic body size (codominant, epigenetic, on most horses), particle (forty alleles, epigenetic, paints nothing), verdant (four alleles), LUT (swaps the phase-2 gradient for a variant homozygote - the only gene that can) and cutie mark (recessive, epigenetic; a client layer stamps 1-3 item icons on both flanks) - and the thirteen non-coat genes - performance (MSTN, PDK4, CKM), jump (RYR2), size (LCORL, HMGA2) and health (ACAN with five alleles, B4GALT7, PLOD1, RAPGEF5, ST14, SHOX, MET);Genotypecode round-trip, breeding, theEpigenome/Genomeper-allele epigenetics + priority tie-break,GenomeSample- a genome detached from a horse, for the stallion seed jar -Expression+FounderTable+ theGenotypeCatalogreduction of 42 875 903 837 175 872 297 619 456 000 000 genotypes to 11 098 128 386 distinct coats -size()/get()arelongnow, the leopard complex doubled it pastInteger.MAX_VALUE),Inheritance+ the sex-linked machinery,EyeColor+EyeColorContribution(the ranked iris channel),CutieMarkContribution(the mark's modifier hook) andSpliceSafety(the derived random-splice blacklist),coat/+coat/pattern/(the pipeline -CoatTextureComposer,PigmentField,ColorField,CoatOverlay,GradientLut,BayCoat,GreyCoat,WhitePattern,BodyStripes,HairPattern,CoatRegions, the pure gene hooks, thecoat-golden.txtbyte-identity net,CoatTextureIdtexture-id injectivity),coat/skin/(HorseSkinGeometry),name/(breedNth),horse/(pedigree ->wiki/breeding.html),trait/(the non-coat body ->wiki/horse-body.html), andgenetics/spec/(the data-driven gene format:GeneSpec,Json,GeneSpecParser,SpecSchema,SpecValues,SpecGene,GeneSpecLoader, pluscoat/pattern/SpecPainter; and the geneeffectspath -GeneAbility,AbilityType,HorseAbilities- the Minecraft-specific things a data-driven gene does beyond the coat - see below).
neoforge-26.1.2/
- compiles and assembles (
./gradlew :neoforge-26.1.2:buildpasses; only twogetGuiLeft/getGuiTopdeprecation warnings) against the real NeoForge26.1.2.100SDK.
runServer
- boots clean to
Done (...)! For help; all dimensions, attachments, SavedData, payloads (incl.spawn_custom_horse), thehay_portalblock + block entity, thecustom_horse_spawn_eggitem, and theClientConfigall register with no errors. Re-verified 2026-09-02 with the shipped Waterborn + Suntouched genes loaded ([genes] loaded 2 data-driven gene(s): example.suntouched, example.waterborn ... 13 segments) and again after the gameplay-layer items + thestored_genomedata component were added (Loaded 1531 recipes, 16 new shapeless recipes among them; the component registers with no error - the 26.1.2 path the roadmap flagged as unverified now works). Re-verified again 2026-09-02 after the horse-care attachment (horse_care), thecare_syncpayload and thehorse_water/horse_foodblock tags were added - still boots clean toDone. Re-verified 2026-09-05 after the LUT gene + thehorsegenetics:horse_browserMenuType+open_horse_browser/select_browser_genepayloads were added: boots toDone, 44 segments (42 built-in + the two shipped spec genes), no errors.
runClient
- actively play-tested over the 2026-08-30, 2026-09-01 and 2026-09-02 sessions; see below.
2026-09-02
Built 2026-09-02, NOT yet play-tested
the seven remaining visual natural genes (roadmap §§4.1-4.2), all hand-written Genes in common/genetics/genes/:
- dilutions -
DunGene(thenD/d, now the three-alleleD/d1/d2: mild body dilution + primitive markings - a dorsal stripe + leg barring that skip the dilution, via newCoatRegions.dorsalStripe/legBar),SilverGene(Z/z, DOMINANT: eumelanin-only, chocolate body + flaxen mane/tail, chestnut unaffected - runs right after agouti innaturalOrder()),MushroomGene(Mu/mu, RECESSIVE: pheomelanin-only, chestnut -> sepia). - white patterns (all non-deterministic, epigenetic seed on the variant copy) -
RoanGene(Rn/rn, DOMINANT: near-binary white-hair dither, density tapering back-to-front so it feathers into the solid face),TobianoGene(To/to, DOMINANT: big crisp patches from a topline-biasedPatchNoise.field, white legs),FrameGene(Ov/ov: bold jagged-edged patches in an absolute-Y flank band on BODY/NECK + a bald face) andSabinoGene(SB1/sb1, dose 1 = jagged stockings + belly + blaze, dose 2 = "sabino-white"). The last two are gone - the white-pattern rewrite (2026-09-04) moved frame toEdnrbGeneand sabino intoKitGene, and both painters became strengths on the sharedWhitePattern. - New helper
coat/pattern/PatchNoise(warped 3-octave fractalfield+ 2-octavefbm2) - the spotting genes need patch fields that cross seams smoothly and aren't one lattice cell wide; single-octaveBodyNoisegridded up into visible squares.CoatRegions.dorsalStripe/legBarare the dun primitive-marking helpers. - Reworked 2026-09-02 after owner feedback on the first bakes: grullo was warm-brown not mouse-grey (dun's
keepRednow scales to ~0 by the texel's black content, so a black base lands on the LUT's neutral column); mushroom read as no-op (keepRed0.44 -> 0.12); silver's mane came out chestnut not flaxen (mane now pulls red down too); roan made hard 100% white pixels and had a hard edge at the face (now near-binary flecks + a smooth rear->front density falloff); tobiano was ~80% white with gold fringing (higher threshold, hard-binary decision - a half-scaled black texel reads gold on the LUT); frame produced almost nothing (per-partsideWeightleft the barrel untouched -> absolute-Y band); sabino showed axis-aligned squares (PatchNoiseinstead of rawBodyNoise.value). - Registry was 18 genes then; it is 18 now - cream and pearl merged, then sex was added (20 in-game with the two shipped spec genes).
GeneCodeDisplay's trailing order gained all seven.coat-golden.txtregenerated (10 new cases).GenotypeCatalogblew up to 331 778 distinct coats; the combination-table rewrite brought that back to 98 306, and the white-pattern rewrite took it to 2 064 387 (totalGenotypes()55 099 802 880) - which is where the catalogue stopped being materialised at all and the gallery gained a cap - both since superseded by the revert to random pens. The exhaustive2^genes/3^genestests (CoatTextureIdTest,CoatTextureComposerTest's combo sweep) were converted to seeded sampling - enumerating them is no longer tractable. Sample bakes (:common:bakeCoatSamples, 12 new) look right after the rework; nothing seen in-game - checklist inwiki/verification.html.
Built 2026-09-02, NOT yet play-tested
the gameplay-layer items (roadmap wiki §§11-19, first slice). 19 new Items in item/ModItems: horse_hair + horse_hair_bundle (4 hair ↔ 1 bundle, roadmap §12.2's first two rungs), four breeding carrots (epigenetic_splice/gene_splice/stabilizer/magnifier), one generic known_gene_splice_carrot (per-gene parameterisation needs a data component - deferred), placeholder_gene_book (literal name "PLACEHOLDER GENE BOOK", stands in for the research paper), empty_seed_jar + stallion_seed_jar (both SeedJarItem, tooltip from the stored_genome component), four tickets, three whistles, and stall_sign + bound_stall_sign. The 17 non-sign items have owner-supplied textures (tickets share one, whistles share one); the two stall signs borrow minecraft:item/oak_sign - per-tier / real art is a follow-up in wiki/verification.html §15. New item/ModCreativeTabs registers one Horse Genetics tab holding all of them (20 with the custom spawn egg, which also still shows in vanilla Spawn Eggs). The dev test-world hotbar (server/DebugTestWorldHandler) gives the spawn egg on slot 0 (tools shifted to 1-6) and one of every new item in the main inventory. Recipes are datapack JSON under data/horsegenetics/recipe/ (singular). Tickets are inert (stall-teleport needs stall blocks that don't exist); the carrots do nothing yet. The whistles work - see below.
Built 2026-09-02, NOT yet play-tested
the whistles (item/WhistleItem). Right-click anywhere to teleport every tamed horse you own within range (basic 16 / golden 32 / echo 64 blocks), same dimension, not ridden, to a grid of spots beside you; ~3 s use cooldown, a chime, a chat count. Leashed horses are unleashed and come; a horse already within 3 blocks or one you are riding is left alone. This is the owner's "area recall" reading of roadmap §11; the bond-gated version waits on bond, and "what echo adds" beyond range is still open. Checklist wiki/verification.html §17.
Built 2026-09-02, NOT yet play-tested
the stall system (roadmap §11). A new bound_horse data component (data/BoundHorse), a stall_sign / bound_stall_sign item (item/StallSignItem, texture borrowed from minecraft:item/oak_sign), a server-global data/StallData SavedData of data/StallRecords (one per bound horse), a flood-fill server/StallDetector, and server/StallDebug (the "debug overlay" - a particle wireframe + chat summary). Flow: right-click a horse with a blank stall_sign (server/StallSignHandler) -> it becomes a bound_stall_sign carrying that horse's UUID + name; right-click the outside face of a wall with the bound sign -> StallSignItem.useOn drops a real oak_wall_sign with the horse's name and flood-fills the block behind that wall (this layer ± 1, air only, ≤ MAX_BLOCKS 512) - an enclosed area becomes that horse's stall, its outline flashed with HAPPY_VILLAGER particles. Breaking the sign (BreakBlockEvent) releases the stall. Dev keybind F7 (key.horsegenetics.show_stalls -> RequestStallHighlightPayload, dev-gated in ModNetworking) re-flashes every stall's outline near the player + prints a summary. No teleport-to-stall yet (that's the tickets, still inert); no client-side persistent wireframe (particles only). Checklist wiki/verification.html §18.
Built 2026-09-02, NOT yet play-tested
the stallion seed jar first slice (roadmap §15.1). New common/genetics/GenomeSample (a Genotype + Epigenome detached from a horse as code strings, with breedInto(mareGenome, rng)); new data/StoredGenome + data/ModDataComponents registering the horsegenetics:stored_genome data component (persistent Codec + networked StreamCodec); server/HorseBreedingHandler refactored so its foal-building body is a reusable applyBredFoal(...); new server/StallionSeedJarHandler - right-click a tamed adult stallion with an empty_seed_jar → a stallion_seed_jar stamped with his genome, sex, UUID, name, speed/health; right-click a tamed adult mare with a filled jar → a foal bred immediately through applyBredFoal from her live genome + the jar's stored one, jar consumed, mare put on the vanilla breeding cooldown. Both ends require the horse to be in breeding mode (isInLove() - fed a carrot/apple, works in creative); the op consumes that love state. The jar transforms in the player's hand (creative included). No real breeding-carrot gate (vanilla love is the stand-in), no gestation state (foal is immediate). Partly owner-confirmed 2026-09-02 (collection + tooltip); :common:test 194, :neoforge-26.1.2:build, runServer all green.
Built 2026-09-02, NOT yet play-tested
the horse-care systems - gated healing (roadmap §7.2) plus bond + herds (§13), the first slice of both, sharing one slow tick as the roadmap demands. All in neoforge-26.1.2/, nothing in common/:
data/HorseCareAttachment- acopyOnDeathattachment (horsegenetics:horse_care) holdingbond(0-100), anOptional<UUID>herd id, thebondToday/dayStampdaily-cap pair, abondTicksfractional accumulator and atogetherTicksherd-formation counter;behaviourTier()(0 vanilla / 1 face / 2 approach / 3 follow).server/HorseCareHandler-EntityTickEvent.Post, every 30 ticks, staggered bytickCount + entityId. Gated healing: only for a hurt horse, a 3-block scan for a block in#horsegenetics:horse_water(or any#minecraft:waterfluid) and one in#horsegenetics:horse_food;heal(1)+HEARTparticles per hit (2if in a herd). Bond: proximity (~+0.5/min), riding (~+1/min), feeding (+2, onEntityInteractwith anyisFoodstack from the owner); cap +15 per 24 000-tick day; foal-in-herd doubles the accrual. Herds:togetherTicks+30 with company / -30 alone; ≥ 12 000 → join a neighbour's herd or mint aUUIDand pull in herdless neighbours; decays to 0 → leave. Attachment written back only on change; sync packet only when bond / in-herd changed.server/BondFollowGoal- oneGoaladded at priority 4 to everyHorseon join (dedup viagoalSelector.getAvailableGoals()), inert below tier 1 / while leashed / while ridden; tier 1 look-only, tier 2 paths only if a route exists, tier 3 follows persistently. Real pathfinding, never a teleport.network/HorseCareSyncPayload→client/ClientHorseCareCache(cleared onLoggingOut);client/HorseScreenHooksshows abond N <tier> • herdline. Care sync also sent onStartTracking.- Two block tags:
data/horsegenetics/tags/block/horse_water.json,horse_food.json. :common:test(195),:neoforge-26.1.2:build,runServer(boots clean, tags load with no error) all green. Checklist:wiki/verification.html§0; machinery:wiki/horse-care.html.- Not in this slice: milking (§7.1), shearing/sleeping bond (shearing unbuilt), a stored herd alpha, any stamina resource.
2026-09-06
Owner-verified in the browser (2026-09-06): the gene creator opens and renders correctly on the live wiki page.
Owner's words: "that looks correct". Taken as a general confirmation that the tool loads and the horse reads right, not an item-by-item walk of the checklist - the same standing the white-pattern rewrite got on 2026-09-04. So it closes: it opens at all (the expresses ReferenceError is gone), the parity line reads green in the page, and the 3D preview reads as a horse rather than the pile of bounding boxes it was. What that cannot cover, and what stays open in wiki/verification.html §0-H, is everything you have to interact with - typing into a field, the Gameplay forms, adding the eight effect verbs, and loading Waterborn to confirm its effects survive the round-trip.
Owner-verified in-game (2026-09-06)
- The topline recalibration's single-copy splash reads as socks - white confined to the legs, no belly/back bleed. Closes the
wiki/roadmap.html#defectsdefect-1 /PAX3-SW2-on-90%-of-founders verification question; the ladder-step, stacking and sabino-underline questions the same recalibration raised are still open. - Tiger eye reads well on the Puerto Rican Paso Fino. Closes the two-texel-amber-iris verification question for that case; the sclera, blue-beats-amber and foal-eye questions the eye-colour channel raised are still open.
2026-09-05
Owner-verified in-game (2026-09-05)
- The LUT gene -
Blupnk/Blupnkresolves against the blue/pink gradient correctly, a carrier is indistinguishable from a plain horse, the alternate texture loads fromcommonresources at runtime. Working perfectly (owner's word). The verification checklist item is retired.
2026-09-04
Owner-verified in-game (2026-09-04)
- The white-pattern rewrite -
KIT,MITF,PAX3andEDNRBall render correctly: the eight-stepKITladder reads as distinct steps,KITbody white is patches rather than confetti, the splash waterline is crisp with no gold fringe, the two splash loci visibly stack,W20boosts rather than acts alone,O/Orenders pure white, and eyes survive the widest patterns. Confirmed as a whole, not item by item - see the note above for what that leaves open.
2026-09-03
Owner-verified in-game (2026-09-03)
- The data-model rewrite (gene priority + derived orderings + gene-keyed tolerant code strings): wild horses spawn and render their correct coats - no regression from the new
codeOrder()/ code format - and the right-click paper genome dump works. Not yet checked: a bred foal, the stallion-seed-jar round-trip, and a spec gene (Suntouched/Waterborn) appearing in the short genome display.
2026-09-02
Owner-verified in-game (2026-09-02)
- Custom horse spawn egg end to end - the egg, the age/sex/genome editor, and the spawn (see the status bullet below for the two follow-up fixes).
- Waterborn's coat: the neon-blue streaks in the mane and tail render.
- Waterborn's particle trail: the blue dust at the hooves on the move works.
walk_on_waterand the tamed-mare milking are not yet confirmed -wiki/verification.html§13. - Suntouched's
glow: the emissive gold mane + tail render (bright in the dark), the gold-dustemittershows, and the horse lights the area around it (the trailingminecraft:lightblock). Light cleanup on death/unload, the foal case, and hard base coats (cremello, dominant white) are not yet confirmed -wiki/verification.html§13. - Stallion seed jar - collecting from a stallion and impregnating a mare produces a real bred foal, and the jar carries the correct genome. Two bugs found and fixed the same day: the jar didn't visibly change in creative (now transforms in hand), and it worked with the horse not in breeding mode (now both ends require
isInLove()). The short genome display string (info panel / tooltip) is missing the magical genes - aGeneCodeDisplaybug, data is fine (open issue inwiki/verification.html). The breeding-mode gate itself is built-but-unretested.
2026-09-01
Owner-verified in-game (2026-09-01)
- The coat pipeline end to end. Wild spawns show a wide variety of genotypes and every one renders its correct coat - no more flat-white horses. That was the
CoatTextureIdfix (see the coat-pipeline section): the oldsanitize()lower-cased the texture key, folding all 19 683 genotypes onto 27Identifiers. - Agouti / bay: renders correctly, and seal is properly gone as a gene
- a high roll of bay's epigenetics gives the seal look. (Verified against the old two-number roll; the generator was rewidened afterwards - see the coat-pipeline section - so the spread of leg heights is unverified, the mechanism isn't.)
- Splash: renders correctly (leg white + centreline blaze). (That was the old single splash gene, retired 2026-09-04 for
MITF+PAX3; the replacement is unverified.) - Eyes: survive the coat on every horse seen, adult and foal.
FamilyTreeScreen: correct in full - nodes, coats, layout.- Horse dimension: correct in full, including the sunk pen amenities (water cauldron + hay bale flush with the grass) keeping horses penned.
- "Spawn Test Horse World" title-screen button (dev only) works.
- Clock on a tamed foal ages it to adult without also seating the player on it.
- Roped-horse portal shortcut: right-clicking a
hay_portalwhile leading a horse sends the horse through and drops the lead. - Diluted bay points (fixed and re-verified the same day): a bay carrying champagne / cream / pearl now shows real diluted points - amber champagne chocolate over gold, buckskin dark brown over gold, perlino rusty, pearl bay sepia - instead of the jet black they all rendered before. See the coat-pipeline section for the cause (
PigmentField.dilute). - Three rendering issues found in the same session are logged in
wiki/verification.html- see "Known gaps" below. The bay/dilution one closed the same day; grey closed later (theGreyCoatrework, built but not yet play-tested); the two splash ones were retired with the gene.
2026-08-30
Owner-verified in-game (2026-08-30)
- Hay-bale portal: golden-carrot lighting; the animated
hay_portal.pngtexture renders, faces the player (thin half-block slab), is opaque (no sky/clouds/water through it), and its fps ramps 12 -> 48 as you dwell; gold dwell-swirl particles + the chat countdown; the End-portal starfield effect (before it was replaced by the custom texture); return teleport drops your tamed horses in the air beside the overworld portal, unharmed (10 s invuln), not sucked back. - Horse dimension: the reworked E/W pen-back walls (gravel strip flush to the pen, glowstone above, one wood wall, bedrock); the layered wall behind the portal (gravel floor strip + floating glowstone + wood wall + bedrock, no dark band / missing blocks) with the E/W walls carried to
originX-1so the corner has no gap; no block break / place anywhere in the dimension (survival + creative). - Info panel (mounted horse GUI): readable shadow-free text, the "View Family Tree" / Set buttons + barn box visible and clickable, and E no longer closes the screen while the barn box is focused (Enter submits, Esc closes).
FamilyTreeScreen: per-node 3D horse models in the right coat/pose that turn to face the cursor; the chart shrinks to fit the window (boxW/boxH/uiScalefromrebuildNodes, full names viadrawFitted's posescale) - no column overlap, nothing cut off, names legible, no scroll bar by default.- Breeding: a foal rolls speed/health between its parents, a correctly combined genetic code, and (for the pairing's first foal) a name combining both parents.
2026-09-02
Built 2026-09-02, NOT yet play-tested
data-driven genes and the rebuilt gene creator. A gene that fits the format is now a JSON file dropped in config/horsegenetics/genes/ - no Java, no rebuild - and wiki/gene-creator/ is the tool that writes it, previewing the gene on a 3D horse over any of 15 base coats before you export. Nothing about the eleven built-in genes changed: no gene file ships by default, so the registry, the genotype code, the gallery numbers and coat-golden.txt are all untouched. See "Data-driven genes" below; in-game checklist in wiki/verification.html.
Built 2026-09-02, partly play-tested
gene effects - a data-driven gene can carry Minecraft-specific behaviour alongside its coat layers. Six verbs at the time, eight since 2026-09-04 (traversal, attribute, emitter, mob_effect, yield, glow, healing, spread), each with an optional boolean when and a minDose. emitter grew a color2, a count, a data number and five body-site anchors when the particle locus landed. common/ parses and validates all of them (GeneAbility records / one AbilityType per-verb declaration / a generic GeneSpecParser.readAbility / HorseAbilities, unit-tested); the NeoForge translator (server/GeneAbilityHandler, server/GeneYieldHandler) executes traversal + emitter + mob_effect + yield + glow. attribute is the one verb parsed but not executed yet (logged once). mob_effect resolves the id against the registry and keeps a hidden/ambient effect topped up on the self / rider target on its refresh beat (duration refresh + 20, so a when going false lets it decay - no explicit removal). glow has two independent halves: light (0-15) maintains a trailing minecraft:light block server-side (moved on block change, cleared on EntityLeaveLevelEvent / when false, skipped in the horse dimension, air-only placement), and parts (a body-region list - a new AbilityType Kind.PARTS parsed through PartGroups.expand) drives client/EmissiveCoatLayer, which redraws those coat parts full-bright over the base coat via a second baked texture. walk_on_water is an approximation (surface buoyancy, not a solid plane). Two shipped genes exercise it: Waterborn (traversal + emitter + yield) - neon-blue mane/tail streaks + blue particle trail owner-confirmed in-game (2026-09-02), walk_on_water and the tamed-mare milking not yet - and Suntouched (glow + emitter) - light 12 + emissive gold mane + a gold-dust aura, the emissive mane and the area lighting both owner-confirmed in-game (2026-09-02). Reference: wiki/gene-effects.html (verbs + the "add an effect" contract), wiki/gene-waterborn.html + wiki/gene-suntouched.html (the genes), wiki/horse-traits.html (the wider architecture); checklist wiki/verification.html §13.
Play-tested 2026-09-02, works
the custom horse spawn egg (item/ModItems -> CUSTOM_HORSE_SPAWN_EGG). A plain Item reusing the vanilla minecraft:item/horse_spawn_egg texture (identical icon), in the Spawn Eggs creative tab. Right-clicking it (client/CustomHorseSpawnEggClient cancels the interaction) opens client/CustomHorseSpawnScreen. The egg, the editor and the spawn were all confirmed working, and two follow-ups were fixed the same day: a full-screen dim in extractRenderState was drawing over the buttons (widgets render during super.extractRenderState), and the gene list could run off the bottom of a short screen. The editor itself was rebuilt 2026-09-04 - see the entry at the top of this list; what carries over is the egg, the interaction cancel, and the two drawing lessons. All custom horses get their body from their genotype like any other horse.
Behaviour change 2026-09-02: Waterborn + Suntouched ship loaded.
To make the data-driven-effects work testable in-game, example.waterborn and example.suntouched are registered via a classpath gene index - neoforge-26.1.2/src/main/resources/horsegenetics/genes/index.json (["suntouched.json", "waterborn.json"]) + the two files beside it, which GeneSpecLoader.fromClasspath() picks up in the mod constructor. These are the first (and so far only) gene files to ship, breaking the "no gene ships by default" invariant on purpose (the owner OK'd it): the in-game genotype code is now 20 segments (13 at the time), GenotypeCatalog/the gallery are ~4x (each shipped DOMINANT two-allele gene doubles them), and shorter saved horses won't parse. :common:test is unaffected - the index lives in the neoforge module's resources, not on the common test classpath, so Genes stays at 11 built-ins there (17 now) and coat-golden.txt + SpecGeneTest's BUILT_IN_GENES still hold. The horse dimension will be overhauled later regardless.
Built 2026-09-02, glow owner-confirmed in-game
Suntouched (example.suntouched, allele Sntch/n, DOMINANT magical, wildOdds 128, priority 210), plus the mob_effect and glow verb translators (mob_effect has no shipped user and is unverified). Suntouched is a spec gene shipped as its own file: one deterministic coat layer (PARTS on HAIR x TOWARD gold #ffcf47 at 88%) plus an effects block of glow (light: 12, parts: ["HAIR"]) and an emitter (gold #ffcf47 dust, interval 6, body anchor). It is the worked example for glow: server/GeneAbilityHandler.reconcileGlow maintains the light block and client/EmissiveCoatLayer + GeneticCoatTextureFactory.getOrCreateEmissive draw the full-bright mane. mob_effect was wired in the same pass (applyMobEffect) but nothing shipped uses it now. See wiki/gene-suntouched.html; checklist wiki/verification.html §13.
Docs 2026-09-02, no behaviour change
the Docs/*.md -> wiki conversion. All five markdown docs are gone; their content lives in wiki/*.html (see the Docs-split section above), the four Javadoc comments that named them were repointed, and Gene Dict.md was split into a page per gene - all 11 now documented, where only 4 were before (and two of those pointed at a CreamPearlGene.java that has never existed). Two pages are new rather than converted: wiki/modding.html (how to write a gene) and wiki/api-reference.html (class abstractions), which is the modder-facing documentation the roadmap assumed would exist. wiki/nav.js builds every sidebar from one array, so a new page is one line. No Java behaviour changed - only Javadoc text - and :common:test (153) and :neoforge-26.1.2:build are both green. Nothing to play-test.
Built 2026-09-02, NOT yet play-tested
the first two magical genes - magic zebra (Mzeb, dominant, 1/100 per allele) and pink hair (Pihr, recessive, 1/12 per allele). Both are phase-3 genes; details in wiki/gene-*.html, in-game checklist in wiki/verification.html. They take the registry to 11 genes, which moves a lot of derived numbers: the code string is 11 segments, breedWith draws 22 booleans, and the gallery goes from 434 pens / 1 519 blocks to 1 730 pens / 6 055 blocks of corridor (long since overtaken - see the white-pattern entry). Sample bakes look right (stripes read black over cremello and over dominant white; pink manes keep their strand shading on black, chestnut and perlino alike); nothing seen in-game.
Built 2026-09-02, behaviour-neutral by construction
the three-phase pigment pipeline (wiki/roadmap.html §1). Phase 3 is now a signed, uncapped ColorField that magical genes add into, both gene hooks are pure (read-only views in, a contribution out), and CoatBuildContext no longer carries scratch space. No coat changed: CoatPipelineGoldenTest hashes 20 genotypes × 3 seeds × adult/foal and every one is byte-identical to the pre-refactor bake, so there is nothing new to play-test - it's groundwork for the magical genes. Nothing in neoforge-26.1.2/ needed touching, which was the test of whether the refactor stayed inside common/.
2026-09-01
Built 2026-09-01, NOT yet play-tested
per-allele epigenetics and the dapple-grey rework. Epigenetics moved off the horse and onto the allele copy (Epigenome / Genome / AlleleEpigenetics, each copy carrying a priority + epigeneticSeed, inherited unchanged by a foal); GreyGene now renders a real dapple grey through the new GreyCoat + BodyNoise; and bay's leg black is a uniform per-horse extent with per-leg jitter instead of the old low-biased single number. Compiles, 138 common tests pass, sample bakes look right, nothing seen in-game yet - checklist in wiki/verification.html.
Built 2026-09-01, never play-tested, and superseded 2026-09-04
the genotype gallery rework of the horse dimension - one pen per visually distinct genotype, the entrance tally sign, and the per-gene distinctness metadata. Reverted to random pens before anyone walked it. What survives from it: the per-pen genotype sign, the pair per pen, and the entity-only teardown that leaves blocks standing.
Built 2026-09-01, NOT yet play-tested
the dev test-world auto-delete
client/DebugTestWorldCleanupwipes everytest_horse_*save on client
shutdown (and sweeps leftovers on the next start), so the button stops filling run/saves. See "Running the game".
Standing build state
Open issues + NOT verified in-game
see wiki/verification.html. The two newest items are the top of the list, and they are the same play session: PAX3 SW2 on 90% of founders (does a herd still read as a population rather than one horse repeated - and does it read as socks or as four white legs, which would be gap #30 rather than the frequency) and the face-marking family (does a three-to-five-texel star read as a star at 128px, does a snip land on the nostrils, is the star above the eyes given the head's approximate rest-pose projection). After those: the particle locus (§0c - forty particle ids, six body sites, none of it seen; the emitter-style ones and the ten Bedrock substitutions are the likeliest to read badly), the seven magical utility genes (§0a - and inside that, the walking animation of a scaled horse and whether the wild size spread reads right across a herd), the trait / health layer (§0b), the random pens and the rebuilt spawn egg, then foals (only spot-checked). Update it after each runClient.
Machine caveat (this dev laptop)
hybrid graphics (NVIDIA RTX 3050 Ti + AMD integrated). java.exe/javaw.exe are pinned to the NVIDIA GPU and the FML splash is disabled, or the JVM hard-crashes in the AMD GL driver. See "Running the game".