Reference / data-driven breeds
The breed file format
A breed is a JSON file, not a Java class. Every breed the mod ships
is one, in common/src/main/resources/horsegenetics/breeds/; a breed you
write goes in .minecraft/phc/breeds/ and needs no rebuild — just the
jar. The breed designer walks through a breed
one step at a time, validates the file with the game’s own parser and shows the
horses it makes as you go; this page is what it is writing.
A breed that doesn’t fit — one that needs behaviour rather than a
table of numbers — can still be written in Java against
Breed.Builder and registered with Breeds.registerJava.
That escape hatch is deliberately narrow, and the only thing shipping through it is
Feral Mixed, which is the absence of a breed rather than one. A breed
that is a set of genes, biomes, bands and numbers must be a file: a Java one cannot be
edited by a player, shared between installs, or opened in the designer.
The shipped files are inside the jar, so a world changes them another way:
.minecraft/phc/breed-spawning.toml overrides each one’s biomes,
weight and hours, or switches it off (breeds).
It never touches a drop-in breed — that is already a file you can edit.
The shape
{
"id": "friesian",
"name": "Friesian",
"description": "Jet black, heavily feathered, and built to carry a knight.",
"kind": "natural",
"commonness": "uncommon",
"spawn": ["wild", "cowboy", "spawn_egg", "stable"],
"spawn_time": "any",
"biomes": ["minecraft:forest", "minecraft:plains"],
"price": [8, 14],
"stats": { "speed": 5, "jump": 5, "health": 7, "size": [0.95, 1.24] },
"genes": {
"horsegenetics.extension": [
{ "pair": "E/E", "weight": 62 },
{ "pair": "E/e", "weight": 33 }
],
"horsegenetics.agouti": [ { "pair": "a/a", "weight": 100 } ]
},
"bands": {
"horsegenetics.ednrb": { "cover": [0.55, 0.80] },
"horsegenetics.agate": { "hue": 210, "cellSeed": "-4127755610044201371" }
},
"notes": ["Lower-leg feathering - not modelled."]
}
Only id and name are required. Every other
field may be left out, and leaving one out is not the same as writing a zero into it
— the table below is what the game does with an absent field. The designer never
pre-fills a default into a box for exactly that reason: a filled box says somebody
chose the value, which would be a lie.
Every field, and what a blank one means
| Field | Shape | Left out… |
|---|---|---|
id |
lower-case token, unique | required. splice and feral_mixed are
reserved by the breed-label system and refused. |
name |
display string | required. |
description |
a sentence or two | none. What the in-game breed book (the Breeds tab of the H menu) shows about the breed, under “About”. |
kind |
"natural" or "magical" |
natural. A label, not a mechanic — what makes a breed magical in
play is the genes in its pool. |
commonness |
one of the seven tiers, extremely_common…very_rare |
moderate. Each step is a doubling; see Commonness. |
spawn_weight |
a raw number, instead of a tier | the tier above. Naming both is an error. |
spawn |
a checklist of wild, cowboy, spawn_egg,
stable |
all four. [] is a real answer — it means the
breed comes from nowhere at all — and is not the same as leaving the field
out. |
spawn_time |
"any", "day" or "night" |
any. When its wild herds may be founded - see
where a breed may come from. |
biomes |
namespaced biome ids - any biome, modded ones included | none, so the breed never heads a wild herd wherever spawn says. |
price |
[min, max] emeralds |
HorsePrices.DEFAULT_PRICE, which owns the number. What a signed
transfer paper for one costs at the cowboy’s, rolled per horse inside the
range. |
stats |
speed, jump, health as 1–10 scores
(a number or [lo, hi]); size as a multiple of an ordinary
horse, a number or [lo, hi] — see size |
every axis wild, which is not the same as a score of 5 —
see BreedStatCurve’s near-baseline rule. |
genes |
gene key → weighted { "pair": "E/e", "weight": 44 } entries |
the locus is rolled wild if it paints, and left at its global founder rate if it does not. |
bands |
gene key → value name → [lo, hi], a single number (a lock), or
for a seed a string of digits |
no number pinned; founders roll from the value’s own design range. |
notes |
strings | none. They only surface on the breeds page. |
Two kinds of wrongness, treated differently
A gene file is parsed strictly — an unknown key is an error — because the creator writes those and an unknown key means the tool and the game disagree. A breed file is different, because a breed is mostly a list of references to other people’s genes.
So: a file that is wrong (no id, an unknown key, a
weight that is not a number) is thrown, reported by file name, and the other files
still load. A file that names something this install has not got
— a gene from a pack you are not running, an allele token that gene no longer
declares, an epigenetic value that was renamed — is warned about in the log
and that locus alone is dropped. The breed still loads.
That is the whole point of the distinction: a breed written for a bigger modpack has to degrade to the horses this install can actually make, rather than vanishing.
Gene pools
A pool is a weighted table of allele pairs. Weights are relative, so
62 / 33 / 5 and 0.62 / 0.33 / 0.05 are the same breed; the
designer shows the share each one works out to, because a weight of 44 means nothing
until you know what the others add up to. (Breed.founderTable scales them
to the percentages a FounderTable takes, so any total is fine.)
A pool is exactly which pairs the breed may have. Name only heterozygotes and every founder is a carrier; include the wild pair and its weight is the share of founders that do not show the gene at all. The designer’s gene card is this table with a tick box per pair.
Size
stats.size is a multiple of an ordinary horse:
1.0 is a vanilla-sized horse (15.3 hands on the info screen), a Falabella
runs about [0.38, 0.51] and a Clydesdale [1.14, 1.43]. The range
is the body-scale band; nothing is converted. It was written in hands until
2026-09-10, and every shipped file was converted through the old curve
(BreedStatCurve.sizeForHands), so no breed changed size.
The genes follow from the size. Each founder's size is drawn inside the
range first, and then: between 0.7x and 1.3x it carries one copy of the
size allele and the wild type, with the whole distance on that copy; outside it,
two, sharing it. So the giants and the miniatures breed true, and a breed of
ordinary-ish horses throws some variety among its foals. A range across 1.3 produces both
kinds of founder. See BreedStatCurve.heterozygousSize; the rule applies to
every breed, shipped or dropped in.
Naming a locus replaces what BreedFounder would otherwise do with it. It
does not mean the horse shows that gene: a Suffolk Punch fixed e/e
at extension still carries and transmits agouti, it just cannot show it. Pools are about
what a line passes on, which is a different question from what it looks like.
Every gene the breed does not name is handled for it. Coat genes and the modifiers a
coat gene reads (Gene.coatDependsOn()) are forced wild, so
a breed cannot leak a pattern it never asked for. So is every magical
gene and every disorder (HealthContribution): a breed is
exactly its sheet, with no stray magic and no background disorder rate. A breed that
should sometimes carry a disorder names it, with the clear pair at the larger weight.
The four body-stat genes come from stats; the natural performance genes
keep their wild roll. Only Feral Mixed, the unbred population, rolls
everything at random.
Epigenetic bands
A band is the difference between “Friesians are black” (a pool) and “Friesians are deeply black” (a number). Each gene declares named numbers on every allele copy — how far a splash covers, how wide a dorsal stripe is — and a breed may pin any of them to a closed range. Each founder is given a point inside it on both copies, drawn separately so its two gametes are not interchangeable.
From there it is ordinary epigenetics. It inherits, it drifts, and nothing pulls it back: a breed shapes the horses it starts with and then lets go. Keeping a line to a standard is the player’s job, not the game’s.
A single number is a lock: every founder is given exactly that value on
both copies. That is what a seed takes - a seed is the long behind a noise
field, and two neighbouring seeds draw unrelated fields, so there is no range to give;
locking one means every founder of the breed carries the same pattern. Write a
seed as a string of digits ("-4127755610044201371"), because a JSON number
is a double and cannot hold most seeds exactly; a small one may be a plain number. A
category - an index into a list the gene owns - is banded over whole
options, both ends included.
Two things a band will not do, each warned about rather than silently ignored.
The four body-stat genes are owned by stats, which knows
about scores, sizes and which allele is the pushing one, so a band naming one of them
is refused. And a value the gene does not declare is dropped the same
way an unknown gene is.
Where a breed may come from
spawn is a checklist rather than a mode, because the four are genuinely
independent. “A breed the cowboy has heard of but that no longer runs wild”
is ["cowboy", "spawn_egg"]; “a collector’s breed that only
exists as an egg” is ["spawn_egg"]; “a landrace nobody
farms” is ["wild"]. None of those is a special case in any of the
four consumers — each just asks Breed.allows.
Dropping spawn_egg is the “opt out of having a spawn egg”
switch: no breed spawn egg exists for that breed, it cannot
turn up in dungeon loot, and the horseman never stocks one.
Wild herds turn up in every biome a wild breed names, modded ones
included. The mod's herd spawn is a biome modifier of its own
(BreedHerdsBiomeModifier) that adds the spawn to its listed vanilla biomes
and to every biome a registered breed lists - so a breed dropped into the folder
that lives in a dark forest or a modded biome gets herds there on the next start. A horse
still needs grass and light to spawn, so a desert or Nether breed will rarely or never be
seen wild.
spawn_time is checked when a herd is founded: outside its hours the
breed is not a candidate and the biome's other breeds share the draw. It only speaks for
wild. Vanilla will not spawn an animal in the dark on a chunk near a player,
so a night breed's herds mostly come from chunks generated at night.
Loading, and the order it happens in
The mod reads the files it ships (listed in index.json, because a jar has
no directory to walk) and then everything in .minecraft/phc/breeds/ (the
game directory - an instance folder, for any launcher but the vanilla one), in filename
order. The folder and a README in it are made the first time the game starts with the
mod installed, and the Breeds tab of the H menu has an
Open breeds folder button. An id that collides with one already loaded
is ignored, and said so.
It happens after the drop-in genes load, and that ordering is why it is an explicit call rather than a static initialiser: a breed file is mostly references to genes, and loading breeds first would report every drop-in gene as missing.
Adding or removing a gene lengthens the code and invalidates every horse already saved. Adding or removing a breed costs nothing: it is a label and a set of founder rules, and no horse stores it beyond the token on its record. That is what makes breeds the safe thing for a player to fiddle with.
Regenerating the shipped files
The files under common/src/main/resources/horsegenetics/breeds/ and the
browser’s single-array copy at
wiki/horse-designer/assets/breeds.json are one generated artefact with two
spellings, written by ./gradlew :common:bakeBreedFiles. The wiki tools read
the second because a WebAssembly build cannot walk a classpath.
BreedFilesTest is what stops them going stale quietly: it checks every
shipped file is exactly what a fresh bake would write, that the index lists all of them,
and that the browser bundle holds the same breeds in the same order.
Changing the format lands in four places
BreedSpecParser— reading it, including which blank means what.BreedSpecWriter— writing it back, omitting the field when it is at its default. The two must agree or the round-trip test fails.wiki/breed-designer/js/—bd-core.js(the state, which is the file, andtoJson/fromJson) andbd-steps.js(the step that sets the field).- this page.
Then re-run ./gradlew :common:bakeBreedFiles, because a field that has
changed shape changes every shipped file. Unlike the gene creator there is no parity
check to run: the designer validates with the game’s own parser compiled to
WebAssembly, so there is no second implementation to drift.