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

FieldShapeLeft 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_commonvery_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 malformed file is thrown; a missing gene is warned and skipped

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.

A breed does not change the genotype code

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

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.