For modders
Class abstractions
The reference half of the modder docs: every type a gene author touches, what it abstracts, and the signatures that matter. For the narrative version — how to actually write one — see Writing a gene.
The two modules
| Module | Contains | Rule |
|---|---|---|
common/ | Pure Java: genetics, coat pipeline, skin geometry, names, the pedigree domain model. | Zero Minecraft / NeoForge imports — not even DFU Codecs. This is the part that survives a version port unchanged. |
neoforge-26.1.2/ | Renderer, attachments, payloads, screens, event handlers, the horse dimension, blocks. | Its job is to translate: build and read common types and shuttle them in and out of Minecraft’s systems. The logic stays in common/. |
If you want to import anything Minecraft-related into common/,
stop. A gene never needs to — the whole gene API is expressed in plain
Java, and that is what makes a future forge-1.12.2/ module cheap.
Package map
Rng.java the randomness seam - the only source of chance
SeededRng.java a deterministic Rng from one long
genetics/
Gene.java THE interface a gene implements
Allele.java one allele of one gene
AllelePair.java the two copies at one gene (canonicalized)
Expression.java ONE outcome a gene can produce, and its paint function
AbilityContribution.java capability: this gene makes the horse DO something
EpigeneticAbilityContribution.java ...and what it looks like is on the allele copy
LutContribution.java capability: swap the phase-2 gradient (the LUT locus, and ONLY it)
WhiteLockContribution.java capability: make a white texel FINAL - applies at every
step of phase 3 at once, so it is not a priority
CutieMarkContribution.java capability: modify the cutie mark another gene drew
EyeColorContribution.java capability: claim the iris colour (ranked; blue beats amber)
EyeColor.java one iris-colour claim - id, name, rgb, rank, strength
EyePatchContribution.java capability: paint PART of an iris (composes; not ranked)
EyePatch.java one coloured patch - a 4-bit mask over the 2x2 iris
EyePatches.java a gene's patches, per eye
EyeSpread.java how much of each iris a depigmenting claim reached
Inheritance.java AUTOSOMAL / X_LINKED / Y_LINKED, and the reserved-slot scheme
SpliceSafety.java which loci the Unknown Gene Splice carrot may roll (derived)
GeneEpigenetics.java one gene's per-horse numbers, by copy or by expression
epi/EpiValue.java one declared number: name, kind, range, founder distribution
epi/EpiSchema.java a gene's whole declaration - Gene.epiSchema()
epi/EpiValues.java the literal numbers on one allele copy, read by name
epi/EpiRoll.java rolls a founder's, honouring each value's distribution
epi/EpiDrift.java the nudge every value takes on its way to a foal
epi/EpiCodec.java reads and writes them as human-editable text
FounderTable.java the wild-population weight per allele combination
FounderContext.java the genotype so far, for a context-aware founder table
Genotype.java one AllelePair per gene; parse / toCode / breedWith
AlleleEpigenetics.java (priority, seed) on ONE allele copy
Epigenome.java one AlleleEpigenetics per copy, aligned to the genotype
Genome.java Genotype + Epigenome; the thing a foal inherits
GenomeSample.java a Genome frozen to code strings, off the horse (seed jar)
Genes.java the registry + the three orderings
GenotypeCatalog.java every visually distinct genotype
GeneCodeDisplay.java shortForm / wrap - human-readable codes
GeneticCodeCombiner.java the string-level and Genome-level breeding seam
CoatPhenotype.java coarse CHESTNUT / BLACK / BAY / WHITE
genes/ the forty-three shipped Gene implementations
spec/ DATA-DRIVEN genes - a gene as a JSON file
GeneSpec.java the format, as records (header + layers + effects)
SpecSchema.java what each mask / op accepts, and its default
GeneSpecParser.java JSON tree -> GeneSpec, strictly, with real errors
Json.java a hand-rolled parser (common/ takes no dependencies)
SpecValues.java one horse's draw of the spec's knobs
SpecGene.java a Gene that answers everything from a GeneSpec
GeneSpecLoader.java find + parse + register, from a folder or the classpath
GeneAbility.java the closed set of Minecraft "effects" a gene can carry
AbilityType.java one self-contained declaration per effect verb (name,
params, defaults, validation, record builder) + the
shared condition-flag / trigger vocab
HorseAbilities.java which abilities a Genotype expresses (NeoForge reads this)
PartGroups.java LEGS / HAIR / POINTS ... aliases for a parts list
SpecFixtureTool.java the bakeSpecFixtures task (creator parity)
CreatorAssetTool.java the bakeCreatorAssets task (creator's inlined assets)
coat/
CoatData.java a Genome ready to render; textureKey()
CoatGenerator.java the FOUNDER path - the only place epigenetics are invented
CoatTextureId.java injective code -> Identifier-legal path
pattern/
CoatTextureComposer.java the bake: 3 gene phases + composite + eyes + overlay
LutSet.java base GradientLut + keyed alternates (the LUT locus)
PigmentField.java phase 1 accumulator (writable)
PigmentView.java phase 1 accumulator (read-only) - what a gene gets
ColorField.java phase 3 accumulator (writable)
ColorView.java phase 3 accumulator (read-only) - what a gene gets
CoatBuildContext.java the horse: genotype, epigenome, skin, adult, rng
CoatOverlay.java phase 6 sink: final pixels + the emissive level per texel
CoatOverlayContribution.java capability: this gene writes finished pixels
GradientLut.java the red/black gradient lookup
CoatRegions.java reusable "paint / restrict this region" helpers
BodyNoise.java seamless body-space noise
BodyStripes.java seamless body-space stripe field (the data-driven STRIPES mask)
ZebraStripes.java the zebra body map - vertical, arcing, ringing, per region
BlaschkoStripes.java brindle's streaks - broken, tapered, asymmetric side to side
HairPattern.java the mane / tail painter: bands, centre stripe, hues
BayCoat.java the bay range, blood bay to seal brown
GreyCoat.java dapple grey generator
CoatPattern.java (x,y,z) -> ARGB, for the sample baker
CoatSheetRasterizer.java bake a CoatPattern to a sheet / PNG
CoatSampleTool.java the bakeCoatSamples gradle task
SpecPainter.java executes a GeneSpec's layers - masks x ops
skin/
HorseSkinGeometry.java body space <-> texel, for ADULT and BABY
breed/
Breed.java one breed, parsed: pools, scores, bands, sources, price
Breeds.java the registry - shipped files, drop-ins, FERAL_MIXED
BreedSource.java WILD / COWBOY / SPAWN_EGG / STABLE - where a breed may come from
BreedBands.java the epigenetic numbers a breed pins or locks, seeds included
SpawnTime.java ANY / DAY / NIGHT - when a breed's wild herds may be founded
BreedFounder.java roll a founder Genome for a breed
BreedLineage.java the label: PURE / CROSS / SPLICED / MIXED / FERAL, + combine
BreedStatCurve.java a 1-10 score, or a size multiple, -> a TargetBand; one size copy in 0.7-1.3x
Commonness.java the rarity -> herd-weight ladder
spec/BreedSpecParser.java a breed JSON file -> Breed (warns and skips what is missing)
spec/BreedSpecWriter.java ...and back, omitting every field at its default
spec/BreedSpecLoader.java find + parse, from a folder or the classpath index
spec/BreedFileTool.java the bakeBreedFiles task
horse/ Sex, HorseRecord, ParentStats, HorseDatabase, InMemoryHorseDatabase
trait/ Traits, Condition, Severity, Viability, TraitBuilder, TraitContribution,
EpigeneticTraitContribution, HealthContribution, HorseTraits
(GeneEpigenetics moved to genetics/ - the ability side
needs it too, and genetics/ must not depend on trait/)
name/ HorseNameGenerator, HorseNames
Core abstractions
Rng — the randomness seam
nextGaussian() is Irwin-Hall, not Box-Muller
A default method: twelve uniform draws minus six, giving an approximately
standard normal with bounded ±6σ tails and, crucially,
exactly 0 for all-0.5 inputs - so
MidpointRng lands on the distribution's mean. Box-Muller
would hand it 1.18σ below the mean. GAUSSIAN_SAMPLES is
part of a gene's draw-order contract.
public interface Rng {
float nextFloat(); // [0.0, 1.0)
boolean nextBoolean();
int nextInt(int bound); // [0, bound)
long nextLong(); // full 64-bit range - noise-field seeds
}
common/ depends on this instead of java.util.Random or
Minecraft’s RandomSource, so the version adapter can plug
either in. Implementations: NeoRng (wraps
RandomSource), SeededRng (a long seed;
what a coat rebuild uses), and FakeRng in tests.
SeededRng also has a namespaced constructor
SeededRng(long baseSeed, String namespace), which is how
epigeneticsFor derives an independent stream per gene key from one
copy’s seed.
Gene
The full interface is listed on Writing a gene. In summary:
| Member | Abstracts |
|---|---|
key() | Identity: <modauthor>.<gene>. Also the namespace for the gene’s epigenetic RNG stream. |
name() / description() | Cosmetic defaults for the in-game gene browser (press H), tooltips and the wiki. name() is the display name (defaults to key()); description() is a one-to-three sentence summary of the whole locus, defaulting through GeneDescriptions — a central table of the built-ins — and "" for a gene it does not cover (every data-driven gene, today). Neither affects the model; the authoritative per-gene write-up is still wiki/gene-*.html, and each outcome has its own sentence on Expression.description(). |
alleles() | Everything this locus can carry — any number. The list order is not a dominance ranking; it is the slot order AllelePair canonicalizes to, and must agree with each Allele.order(). |
defaultAllele() | What a genotype code with no segment for this gene reads as — the population baseline. A parsing default, not a claim about expression. |
expressions() | Every distinct outcome this gene can produce, wild types included. The gene dictionary and the wiki read it; the coat pipeline does not. |
expressionOf(pair) | The function. Which outcome this allele combination produces, on its own. Several pairs may return the same Expression — that is what a “dominant” gene is. |
expressionIn(pair, genotype) | The same in the context of the whole genotype, for a gene that depends on another (agouti does nothing on a chestnut). Defaults to expressionOf; this is what the pipeline calls. |
priority() | Fixed int processing priority, no default. Genes derives every ordering by sorting on (priority, key). 0–99 natural band, 100+ magical (convention; out-of-band only warns). Not AlleleEpigenetics.priority. |
founderTable(ctx) | The wild-population distribution over this gene’s allele combinations. One nextFloat() per founder. ctx carries the genes already rolled. |
isNatural() | Phase declaration. true (default) = phase 1; false = phase 3. |
isNatural() phase | Where the expression paints: Expression.restrict in phase 1, Expression.tint in phase 3. The paint function lives on the outcome, not on the gene. |
isVisible(pair, genotype) | Derived: !expressionIn(...).wildType(). |
isDeterministic(pair, genotype) | Derived: expressionIn(...).deterministic(). Cache correctness depends on this. |
canOccur(pair) | Can a horse actually carry this combination? true by default. GenotypeCatalog.allPairsOf filters on it, so a combination that cannot happen gets no pen and is not counted, and an author should leave it out of the founder table too. Overridden by the sex locus (no Y/Y), KIT (four nonviable W homozygotes), MITF (SW3/SW3) and PAX3 (SW4/SW4). It means embryonic lethal: a foal that is born and then dies, like O/O, stays true and keeps its outcome. expressionOf must still answer for a combination this rules out — parsing is tolerant. |
inheritance() | Which chromosome the gene sits on — AUTOSOMAL (default), X_LINKED or Y_LINKED. Only brindle overrides it. Declaring one of the sex-linked modes changes three things: breedWith routes the sire's copy by the foal's sex instead of drawing it, GenotypeCatalog stops enumerating combinations no horse of either sex could have, and a hemizygous copy is displayed with an X- / Y- prefix. A sex-linked gene must also declare the reserved placeholder allele at the end of alleles() its slot sorts to — Genes.register refuses one that does not, because the failure otherwise is a silently mis-sorted pair. |
hemizygousPlaceholder()isPlaceholder(allele)realAlleles(pair) | Derived from inheritance(). realAlleles is the one a sex-linked gene's expressionOf counts — two for a diploid horse, one for a hemizygous one, none for a mare at a Y-linked locus — so “every real copy is Brn” covers recessive in mares and always shows in stallions with no special case. |
sexConsistent(pair) | Could a horse of some sex carry this combination? Two placeholders at an X-linked locus could not (every horse has an X). GenotypeCatalog.allPairsOf ANDs it with canOccur. |
spliceable() | May the Unknown Gene Splice carrot roll this locus? true by default, and almost nothing should override it — the real rule is derived in SpliceSafety: every combination the carrot could roll is resolved through HorseTraits, and the locus is dropped if any is lethal, impairing, or short of baseline health. A hand-written blacklist would be wrong the first time someone added a health gene and forgot it, and wrong silently. This is the override for harm resolution cannot see. |
affectsCoat() | Derived: does any outcome fail to be a wild type? false means the gene never paints, and Genotype.coatCode() leaves it out of the texture key — the sex locus, the non-coat genes, and the two leopard-complex modifiers. |
coatDependsOn() | Gene keys whose alleles change this gene’s coat output although those genes paint nothing themselves. Default empty. Genotype.coatCode() folds them into the texture key when this gene actually paints on the horse; BreedFounder forces a named modifier wild unless the breed lists it. The leopard complex was the first to use it (reading PATN1/PATN2), and SpecGene now derives it from any needs block in a gene file — which is how accretion reads accretion field. The roadmap still discourages it for third-party genes. |
fromToken(token) | Code-string token → allele. Defaults to a scan of alleles(). |
rarity() | §19 metadata. A GeneRarity tier (default UNCOMMON, the gold-ingot tier) — drives the gene-carrot recipe cost and research-paper loot weight. Tier→item table is on the recipe side, not here. A spec gene reads it from an optional rarity field. |
hasGeneCarrot() / geneCarrotHomozygous() | Whether a Known Gene Splice carrot exists for this gene (default true; false for sex, the recessive disorders, extension, agouti) and whether feeding it treats the parent as homozygous rather than the default heterozygous. |
spliceTable() | Optional<FounderTable> — what the Unknown Gene Splice carrot rolls on this gene (a random gene each time). Empty (the default) means a uniform draw over the gene’s viable pairs. |
The gene base classes (common/genetics/genes/)
abstract class TwoAlleleGene implements Gene // one variant, one wild type, one outcome
static Setup gene(String key, int priority, String displayName);
boolean expresses(AllelePair pair);
boolean isCarrier(AllelePair pair); // a recessive's single copy
public final Allele variant, wild;
abstract class AbstractNaturalGene extends TwoAlleleGene // isNatural() == true
protected abstract PigmentField restrict(CoatBuildContext ctx, PigmentView coat);
abstract class AbstractMagicalGene extends TwoAlleleGene // isNatural() == false
protected abstract ColorField tint(CoatBuildContext ctx, PigmentView coat, ColorView colour);
abstract class AbstractAbilityGene extends TwoAlleleGene // paints nothing
protected abstract List<GeneAbility> abilitiesWhenExpressed(EpiValues epi);
The Setup is fluent and validated at construction:
variant(token, label), wildAllele(token, label),
dominant() / recessive(),
hardyWeinberg(f) / founders(policy, percent) /
founderTable(t), wild(text),
carrier(id, name, text), outcome(id, name, text), then
varies(), masking(), rarity(),
feralOnly(), noGeneCarrot(). Between them they answer
seven of Gene's eight methods; the subclass writes the eighth. Worked
example and the limits of the shape:
the short path.
Other shapes that recur have their own informal bases —
RecessiveDisorderGene, DominantDisorderGene,
AbstractMagicStatGene, AbstractMagicFactorGene,
HairColorGene, AbstractEyeGene,
AbstractWeatherGene, AbstractMatchedPairGene. None of
them is required: implements Gene is always available and is what a
multi-allele locus uses.
GameteBias and CarrotEffect (common/genetics/)
A breeding carrot (a breeding carrot) is data:
GameteBias is a per-parent modifier passed into
Genome.breedWith(other, rng, mineBias, theirsBias) —
rerollEpigenetics (epigenetic-splice), preferLowerOrder
(stabilizer / magnifier), substitutePairs (gene splice, Known Gene Splice carrot).
GameteBias.NONE on both sides is bit-for-bit the plain
breedWith(other, rng). CarrotEffect is the sealed
set (EpigeneticSplice / Stabilizer / Magnifier
/ GeneSplice / KnownGeneSplice(geneKey, homozygous) - the
Unknown Epigenetic Splice, Unknown Gene Splice and
Known Gene Splice carrots) with a flat
id() string for serialisation and
fold(effects, parentGenotype, rng) → one bias.
Allele and AllelePair
// order = the allele's index in Gene.alleles(). A slot order, NOT dominance:
// it only lets AllelePair canonicalize without a registry lookup.
public Allele(String geneKey, int order, String token, String label);
String key(); // "<geneKey>.<token>" - the primary key
String geneKey();
Gene gene(); // resolves through Genes
String token(); // the text in a code string
String label(); // human-readable
int order(); // index in Gene.alleles() - a slot order, NOT dominance
public record AllelePair(Allele first, Allele second) {
// An UNORDERED combination. The ctor throws if the two alleles belong to
// different genes, and puts them in Allele.order() order so a pair equals
// its reverse. Bookkeeping, not biology - see Expression.
String geneKey();
Gene gene();
Expression expression(); // what this combination does
boolean has(Allele a);
int count(Allele a); // 0, 1 or 2 - the dose
boolean homozygous();
boolean homozygousFor(Allele a);
String toTokens(); // "Cr/prl"
}
Expression — one outcome
public final class Expression {
// Replaces DominancePattern. A gene declares one of these per DISTINCT
// outcome and maps any AllelePair to one of them; several pairs sharing an
// expression is what "dominant" used to mean.
static Expression wildType(String description); // id "wild"
static Expression wildType(String id, String name, String description);
static Builder of(String id, String name); // .describe().varies()
// .masking()
// then .restrict(..) or .tint(..)
String id(); // identity - the catalogue dedups on it
String name();
String description(); // one sentence, for the gene dictionary and the wiki
boolean wildType(); // changes nothing: no painter, skipped by the composer
boolean masks(); // while this shows, no other gene is visible
boolean deterministic(); // every horse with this combination is painted alike
boolean isNatural(); // does it paint pigment (phase 1) or colour (phase 3)?
PigmentField restrict(CoatBuildContext ctx, PigmentView coat);
ColorField tint(CoatBuildContext ctx, PigmentView coat, ColorView accumulated);
}
FounderTable — the wild population
public final class FounderTable {
static Builder builder(); // .weight(a, b, percent)
static FounderTable always(Allele a, Allele b);
// p*p / 2pq / q*q from a variant's frequency - a convenience, not a second
// model. What the old "1 in N per allele" meant.
static FounderTable hardyWeinberg(Allele variant, Allele baseline, double p);
// ...and its multi-allele generalisation: 2*p_i*p_j per combination (p_i^2
// when i == j), with anything `occurs` rejects dropped and the rest
// RESCALED to 100. Written for KIT, which has thirty-six combinations and
// four homozygous lethals - hand-tabulating that is not transparency, it is
// an invitation to a typo nobody would ever see. The rescale is the biology:
// a lethal combination is absent from the adult population you observe.
// Pass a LinkedHashMap - iteration order fixes the table's row order.
static FounderTable hardyWeinberg(Map<Allele, Double> frequencies,
Predicate<AllelePair> occurs);
AllelePair draw(Rng rng); // exactly one nextFloat()
List<AllelePair> pairs(); // the combinations that occur at all
double share(AllelePair p);// normalised, 0 for one not listed
}
// Handed to Gene.founderTable so a gene's frequency can depend on the genes
// already rolled. Asking about a gene that has NOT been rolled throws.
public final class FounderContext {
Gene gene();
AllelePair pair(Gene gene);
Expression expressionOf(Gene gene);
boolean isRolled(Gene gene);
}
Canonicalization is why a pair equals its reverse and why the genotype code is
stable: E/e and e/E are the same object and print the
same way.
Genotype
static Genotype of(List<AllelePair> pairs);
static Genotype of(AllelePair... pairs);
static Genotype wildType();
static Genotype parse(String code); // exactly one segment per registered gene
static Genotype random(Rng rng); // each gene rolls its own pair
String toCode();
String coatCode(); // toCode() minus the genes that never
// paint - what textureKey() runs on
static Sex sexOf(String code); // the sex segment, without a full parse
Sex sex(); // read off the sex locus
Genotype withSex(Sex sex); // founders only - a foal inherits its own
Genotype with(AllelePair pair); // founders only - one locus replaced
Genotype breedWith(Genotype other, Rng rng); // 2 nextBoolean() per gene, ALWAYS
// (a sex-linked locus decides the sire's copy
// from the foal's sex and throws the 2nd coin away)
AllelePair pair(Gene gene);
AllelePair pair(String geneKey);
Collection<AllelePair> pairs();
boolean has(Allele allele);
boolean isDeterministic();
boolean hasVisibleNonDeterministic(); // -> bake a texture per horse
// the generic question - "does this gene do anything on this horse?"
boolean shows(Gene gene);
Expression expressionOf(Gene gene);
List<Gene> visibleGenes();
// the three coarse predicates the coat code still reads by name.
// isWhite() is true for KIT dominant white *or* EDNRB lethal white - the two
// combinations that remove every pigment everywhere.
boolean isWhite(); boolean hasBlackPigment(); boolean isAgouti();
CoatPhenotype phenotype(); // coarse; family-tree fallback only
AlleleEpigenetics, Epigenome, Genome
public record AlleleEpigenetics(int priority, EpiValues values) {
static final int MIN_PRIORITY = 1, MAX_PRIORITY = Integer.MAX_VALUE;
static AlleleEpigenetics founder(EpiSchema schema, Rng rng); // roll a fresh copy
AlleleEpigenetics drifted(Rng rng); // as passed to a foal
AlleleEpigenetics bumped(boolean up); // same values, priority +/-1
static AlleleEpigenetics deconflict(AlleleEpigenetics first,
AlleleEpigenetics second, Rng rng);
}
// What a gene declares it writes on a copy. Most genes declare nothing.
public record EpiValue(String name, Kind kind, double min, double max,
Dist dist, int arity, double clampLo, double clampHi) {
enum Kind { SCALAR, SEED, CATEGORY }
sealed interface Dist { Uniform | Gaussian(mean, sigma, floor) | Power(gamma)
| Bright(component, satMin, satMax, valMin, valMax) }
static EpiValue uniform(String name, double min, double max);
static EpiValue power(String name, double min, double max, double gamma);
static EpiValue gaussian(String name, double mean, double sigma, double floor);
static EpiValue perLeg(String name, double min, double max); // arity 4
static EpiValue seed(String name);
static EpiValue category(String name, int count);
static EpiValue[] colour(String prefix, double satMin, double satMax,
double valMin, double valMax); // _r / _g / _b
}
// The numbers themselves. Immutable; quantised to what the code string can write.
public final class EpiValues {
double get(String name); // a magnitude
double get(String name, int leg); // one leg of a per-leg value
long seed(String name);
int category(String name);
int rgb(String prefix); // folds _r/_g/_b back to 0xRRGGBB
EpiValues with(String name, double v); // editing - designer, BreedFounder
EpiValues withSeed(String name, long v);
long hashInto(long h); // what the texture key folds in
}
public final class Epigenome {
public record Copies(AlleleEpigenetics first, AlleleEpigenetics second) {
Copies swapped();
}
static Epigenome random(Rng rng); // FOUNDERS ONLY
static Epigenome fromSeed(long seed); // replay from one long (stand-in)
static Epigenome of(Map<String, Copies> byGene);
static Epigenome parse(String code);
String toCode(); // "<prio>:<seed hex>/<prio>:<hex>-..."
Copies copies(Gene gene);
AlleleEpigenetics expressed(Gene gene, Genotype genotype);
long expressedSeed(Gene gene, Genotype genotype);
long visibleFingerprint(Genotype genotype); // for the texture key
}
public record Genome(Genotype genotype, Epigenome epigenome) {
static Genome random(Rng rng);
static Genome of(Genotype genotype, Rng rng); // founder epigenetics
static Genome parse(String genotypeCode, String epigenomeCode);
Genome breedWith(Genome other, Rng rng); // alleles AND their epigenetics
String genotypeCode();
String epigenomeCode();
long expressedSeed(Gene gene);
}
Genome exists because only a breeding pass that draws both halves at
once can keep the epigenome aligned slot-for-slot with the genotype after
AllelePair re-sorts a pair.
Genes — the registry
public static final String NS = "horsegenetics";
static List<Gene> codeOrder(); // segment order in a genotype code string
static List<Gene> naturalOrder(); // phase-1 effect order
static List<Gene> magicalOrder(); // phase-3 effect order
static List<Gene> all();
static Gene byKey(String geneKey); // throws if unregistered
static Allele allele(String alleleKey); // "<gene>.<token>"
static void register(Gene gene); // ANY gene - built-in, SpecGene or another mod's
static void registerAll(Collection<? extends Gene>);
static void freeze(); // no more genes; late ones throw with a reason
static boolean isFrozen();
static List<SpecGene> loaded(); // the data-driven genes, in gene order
static void clearLoaded(); // back to the built-ins; also thaws (tests only)
static Gene byKeyOrNull(String geneKey); // null (not throw) if unregistered - for tolerant parse
register refuses a key that is not
<modid>.<gene> in [a-z0-9_], and refuses a
duplicate. Both are unrecoverable and both are silent if allowed through — a
colliding gene is simply missing from every horse. Another mod reaches this through
neoforge.api.RegisterHorseGenesEvent; see
registering from another mod.
freeze() buys no speed — the orderings below are already computed
once per registration and cached, and they are on the hot path for every code parse.
It buys a named failure: a gene registered after a code has been read
silently changes what every code already parsed means, and the symptom is horses
whose loci have all shifted by one segment.
All three orderings are one derived result: every registered
gene — built-in and SpecGene alike — sorted on
(priority(), key).
codeOrder() / all() is the whole list;
naturalOrder() and magicalOrder() are it filtered by
isNatural(). A data-driven natural gene at priority 45 lands between
the built-in pearl (42) and champagne (50) — loaded genes are not appended.
Registration order is ignored; ties break alphabetically by key.
Register during startup, before anything parses a genotype: each registration can
move where a gene sits in the code. GenotypeCatalog is lazy and
invalidated on every registration, since the catalogue is a product over
codeOrder().
Coat-pipeline abstractions
CoatTextureComposer.bake(…) returns a
Baked(int[] argb, float[] emissive) — the second is a glow level per texel, 0 for the dark ones;
compose(…) is the pixels alone and is unchanged, which is
why the golden test and every existing caller were untouched. The extra
phase runs after the eyes are restored and lets a gene write finished pixels
and mark texels emissive — see
the pipeline.
CoatBuildContext — the horse, immutable
public CoatBuildContext(Genotype genotype, Epigenome epigenome, Skin skin, boolean adult);
Genotype genotype();
Epigenome epigenome();
Skin skin(); // ADULT or BABY geometry
boolean isAdult(); // grey only greys adults
int size(); // sheet edge, 128
/** This horse's randomness for one gene - a SeededRng on the EXPRESSING copy's seed. */
Rng epigeneticsFor(String geneKey);
/** ...and for ONE copy: slot 0 is pair.first(), 1 is pair.second(). For a gene whose
* two alleles paint at the same time - see HairColorGene's heterozygote. */
Rng epigeneticsForCopy(String geneKey, int slot);
It carries no scratch space. The pigment and colour fields are the composer’s, handed to a gene as read-only views on the call.
PigmentView / PigmentField
public interface PigmentView {
int size();
float red(int px, int py); // surviving pheomelanin, [0,1]
float black(int px, int py); // surviving eumelanin, [0,1]
PigmentField mutableCopy(); // your private working field
}
public final class PigmentField implements PigmentView {
void setRed / setBlack(int px, int py, float value);
void restrictRed / restrictBlack(int px, int py, float amount); // *= (1 - amount)
void dilute(int px, int py, float keepRed, float keepBlack, float blackTint);
void whiten(int px, int py, float amount); // 1 = bald white; every white marking
void forEach(PixelOp op);
}
ColorView / ColorField
public interface ColorView {
int size();
int red / green / blue(int px, int py); // SIGNED, UNCAPPED accumulators
int opacity(int px, int py);
boolean isAbsolute(int px, int py); // meaningful on a delta only
int argb(int px, int py); // the capped pixel
default int visible(int px, int py, int channel); // what a VIEWER would see
ColorField mutableCopy();
}
public final class ColorField implements ColorView {
static ColorField deltaLike(ColorView like); // an all-zero delta
void add(int px, int py, int dr, int dg, int db); // saturating, order-independent
void addOpacity(int px, int py, int da);
void set(int px, int py, int a, int r, int g, int b); // FLAT PAINT - replaces
void setArgb(int px, int py, int argb);
void apply(ColorView delta); // fold one gene's delta in
int argb(int px, int py); // the only place anything caps
}
They are not the same and the difference matters to any gene painting over
something. A texel the natural phase left transparent reads
as white from visible() — because that is
what a viewer sees, the bald template — not as the black
argb() reports. A texel that resolved to near black reads as 20%
grey, for the same reason.
GradientLut
public GradientLut(int[] argb, int width, int height);
int width(); int height();
/** x = (1-redLevel)*(w-1), y = blackLevel*(h-1) */
int sample(float redLevel, float blackLevel);
HorseSkinGeometry
Covered in full on Body space & regions. The surface:
enum Skin { ADULT, BABY }
enum Axis { X, Y, Z }
enum Face { NOSE, TAIL, TOP, BOTTOM, RIGHT, LEFT } // normal(), spanA(), spanB()
enum Part { BODY, NECK, HEAD, MUZZLE, MANE, TAIL,
LEFT_EAR, RIGHT_EAR, LEFT_/RIGHT_ FRONT_/HIND_ LEG } // mirror()
record Bounds(double xMin, xMax, yMin, yMax, zMin, zMax) { min/max/span(Axis) }
record BodyPoint(double x, double y, double z) { along(Axis) }
record Texel(double u, double v, int x, int y, boolean clamped)
record Sample(Part part, Face face, BodyPoint point)
interface TexelVisitor { void visit(int px, int py, Part part, Face face, BodyPoint point); }
static final int TEXELS_PER_UNIT = 2, SHEET_SIZE = 128;
static boolean hasPart(Skin, Part);
static Bounds bounds(Skin, Part); static Bounds bodyBounds(Skin);
static void forEachTexel(Skin[, Part], TexelVisitor);
static Optional<Sample> sample(Skin, int px, int py);
static Texel project(Skin, Part, Face, BodyPoint);
// The posed mesh - for drawing, never for painting. Everything above works in
// the part's rest-pose AABB; these walk the real rotated cuboid instead.
static BodyPoint posed(Skin, Part, Face, double fa, double fb);
static BodyPoint posed(Skin, Part, Face, BodyPoint onBounds);
static BodyPoint posedNormal(Skin, Part, Face); // outward unit direction
// The part's own frame: a body point as three 0..1 fractions along the edges of
// the part's actual box, pitch taken out, named and oriented for the body axis
// each pairs with. Equals part-space normalisation exactly on an unpitched part;
// on the neck, head, muzzle, mane, ears and tail it is the only way to say
// "along this part" rather than "along the world". Reads as space: "local".
static BodyPoint local(Skin, Part, BodyPoint);
CoatData and CoatGenerator
public final class CoatData {
static final CoatData DEFAULT;
CoatData(Genome genome);
CoatData(Genotype genotype, Epigenome epigenome);
Genome genome(); Genotype genotype(); Epigenome epigenome();
CoatPhenotype phenotype();
boolean isDeterministic();
/** the genotype code, + "@<fingerprint hex>" only when non-deterministic */
String textureKey();
}
public final class CoatGenerator {
/** THE FOUNDER PATH - rolls a fresh Epigenome. A foal must never come here. */
static CoatData generate(Genotype genotype, Rng rng);
static CoatData of(Genome genome);
}
CoatPattern and the sample baker
/** A pure (x,y,z) -> ARGB function in body space. Used by the offline baker. */
public interface CoatPattern {
int argb(double x, double y, double z);
}
int[] CoatSheetRasterizer.bake(CoatPattern pattern);
void CoatSheetRasterizer.writePng(int[] argb, Path out) throws IOException;
Data-driven genes
The types a gene file goes through. Full format reference: The gene file format.
// The format, as records.
public record GeneSpec(String key, String name, boolean natural, int priority,
List<AlleleSpec> alleles, List<Knob> knobs,
List<ExpressionSpec> expressions,
List<FounderWeight> founders) {
static final int FORMAT = 2;
AlleleSpec variant(); // alleles[0] - what perDose counts
AlleleSpec baseline(); // alleles[last] - the population default
boolean isDeterministic(); // no knobs = one bake serves every carrier
boolean hasAbilities(); // any Minecraft "effects" on any expression?
ExpressionSpec expression(String id);
}
// One outcome. `combinations` is total over the gene's n(n+1)/2 pairs, checked
// at load: a gap, an overlap or an unreachable catch-all is a load error.
public record ExpressionSpec(String id, String name, String description,
boolean wildType, boolean masks, boolean deterministic,
List<String> combinations, List<Layer> layers,
List<GeneAbility> abilities) {}
public record FounderWeight(String combination, double percent) {}
// The "effects" block - Minecraft behaviour, inert in common/, run by NeoForge.
// See wiki/making-a-gene.html. Closed set; every entry has when()/minDose().
public sealed interface GeneAbility {
record Traversal(String flag, Condition when, int minDose) implements GeneAbility {}
record AttributeMod(String attribute, String op, double amount, Condition when, int minDose) ..
record Emitter(String kind, String shape, String anchor, Trigger trigger, int color,
String particle, double chance, Condition when, int minDose) implements GeneAbility {}
record SelfEffect(String effect, String target, int amplifier, int refreshTicks,
Condition when, int minDose) implements GeneAbility {}
record Yield(Trigger.OnInteract trigger, String consumes, String produces,
int cooldownTicks, Condition when, int minDose) implements GeneAbility {}
record Glow(int light, List<HorseSkinGeometry.Part> emissiveParts,
Condition when, int minDose) implements GeneAbility {}
record Healing(String target, double radius, double amount, int intervalTicks,
int maxTargets, Condition when, int minDose) implements GeneAbility {}
record Spread(String cover, double radius, double chance, int intervalTicks,
Condition when, int minDose) implements GeneAbility {}
sealed interface Trigger { /* Continuous, OnMove, Interval(ticks), OnInteract(item) */ }
sealed interface Condition { /* Always, Flag(name,negate), All, Any, Not */ }
}
// One effect verb as a module. GeneSpecParser reads any effect off this table -
// no per-verb parser code. Adding a verb = a record above + a register(...) here
// + a translator branch + a wiki/making-a-gene.html section.
public final class AbilityType {
static final List<String> CONDITION_FLAGS; // what a "when" flag may name
static final List<String> TRIGGERS;
static AbilityType byName(String type); // error lists all
static Collection<AbilityType> all();
List<Param> params(); List<String> allowedKeys();
// TRAVERSAL, ATTRIBUTE, EMITTER, MOB_EFFECT, YIELD, GLOW, HEALING, SPREAD -
// the eight, one field each.
// Param.Kind adds PARTS for GLOW's body-region list (via PartGroups.expand).
}
// Which of a genotype's abilities are in play. BOTH kinds of gene contribute:
// a SpecGene's expression carries an effects block, and a built-in Gene may
// implement AbilityContribution and return the same records.
public final class HorseAbilities {
record Active(String geneKey, GeneAbility ability) {}
static List<Active> activeFor(Genotype genotype); // midpoint
static List<Active> activeFor(Genotype genotype, Epigenome epigenome); // this HORSE
static boolean anyLoaded();
}
// A caller that CACHES activeFor must key on the epigenome too - an ability's
// magnitude can live on the allele copy, so two horses with identical alleles
// are not interchangeable.
/** The capability a built-in Gene implements to grant game behaviour.
* Same vocabulary as a gene file's effects block - one translator, not two. */
public interface AbilityContribution {
List<GeneAbility> abilitiesFor(AllelePair pair, Genotype genotype);
}
/** The capability a Gene implements to swap the phase-2 colour LUT. EXACTLY ONE
* gene implements this (the LUT locus) and always will - a horse has two copies
* of the chromosome, so it shifts its palette at most one way; a new palette is
* a new ALLELE on that gene, never a new gene. */
public interface LutContribution {
Optional<String> alternateLut(AllelePair pair, Genotype genotype); // key, or empty
Map<String,String> lutResources(); // key -> "textures/coat/....png" (plain strings; common names no Identifier)
}
// CoatTextureComposer.bake/compose gained a (LutSet luts) overload; the old
// (GradientLut lut) one delegates with LutSet.of(lut). The game module loads
// redblackgradient.png + every lutResources() texture into the LutSet.
// Expression.Builder.marker() finishes an outcome that is NOT a wild type but
// has no painter of its own - the LUT swap, the white lock and tiger eye's iris
// colour are all applied out of band by the composer.
/** The capability a Gene implements to make WHITE FINAL: once a texel is white,
* nothing painted afterwards may change it. EXACTLY ONE gene implements it
* (extreme white dominant).
*
* IT IS NOT A PRIORITY, and that is the whole of the design. A priority is a
* SLOT - "after these, before those" - and a marking above the slot paints over
* the white exactly as before; there is no number high enough, because "highest"
* is a position and this rule is not positional. The composer asks once, builds
* a mask, and applies it at EVERY step of phase 3 and again over the overlay.
* The gene's own priority() is therefore a code-order slot and nothing more. */
public interface WhiteLockContribution {
boolean locksWhite(AllelePair pair, Genotype genotype);
}
// The mask is seeded from the PIGMENT FIELD (texels phase 1 left unpigmented =
// the natural white markings) and grown after each magical gene paints, so white
// a magical gene lays down is locked too. Eye texels are cut out and never
// re-added. ColorField.apply(delta, locked) and CoatOverlay.lock(mask) are the
// two readers; a null mask means no gene asked, which is the ordinary case.
/** The capability a Gene implements to CLAIM THE IRIS COLOUR. Several genes have
* a claim on it, so claims are RANKED rather than blended: highest rank wins,
* ties go to the earlier gene in codeOrder(). The three ranks are the three ways
* a real horse's eye changes colour, and they nest: a whole-body dilution that
* catches the iris (cream, pearl, champagne) loses to a gene aimed at the iris
* and nothing else (tiger eye), which loses to an iris that never got melanocytes
* (the four white loci) - an absence cannot be overpainted.
* MAY vary per horse: declare every number in epiSchema(), read it by name, and declare the
* outcome varies(), or two visibly different horses share one cached texture. */
public interface EyeColorContribution {
Optional<EyeColor> eyeColor(AllelePair pair, Genotype genotype,
Epigenome epigenome, // may be null - a genotype-only question
double whiteCoverage);
}
public record EyeColor(String id, String name, int rgb, int rank, double strength) {
static final int RANK_DILUTION = 10, RANK_PIGMENT = 20, RANK_DEPIGMENTED = 30;
static final EyeColor BLUE = ...;
static EyeColor dilution(String id, String name, int rgb);
static EyeColor pigment(String id, String name, int rgb);
boolean depigmented();
boolean losesTo(EyeColor other);
}
// whiteCoverage is the fraction of the RESOLVED coat left with no pigment -
// passed, not recomputed, because a horse broadly white from TWO mild alleles
// stacking has the same claim as one white from a single bold allele, and no
// per-locus test can see that.
// CoatOverlay.tintIris(rgb, strength) is what writes it: weights each eye texel
// by 1 - luma, so the BLACK iris takes the colour and the white sclera does not.
// (shadeToward weights by luma and does the opposite - right for a glowing eye,
// wrong for an iris.)
/** The SECOND eye hook, and the companion to the first. Where a colour claim is
* RANKED (a horse has one eye colour and several genes want it), patches
* COMPOSE: absolute writes over the finished iris, applied in codeOrder(), last
* writer to a quadrant wins. It is how a gene puts two colours in one iris.
* One implementor: magic sectoral heterochromia. */
public interface EyePatchContribution {
Optional<EyePatches> eyePatches(AllelePair pair, Genotype genotype,
Epigenome epigenome, double whiteCoverage);
}
public record EyePatches(List<EyePatch> right, List<EyePatch> left) { } // eyeRects order
public record EyePatch(int quadrants, EyeColor color) {
static final int NONE = 0b0000, WHOLE = 0b1111;
static final int[] WEDGES = { ... }; // the 12 masks that read as a sector
static int complement(int quadrants);
static int randomWedge(Rng rng);
static int differentWedge(Rng rng, int other); // guaranteed != other
}
// An iris is a 2x2 block, so a "shape" is a four-bit mask over it. Bits are in
// reading order (TL, TR, BL, BR), in TEXTURE space - the two eyes' faces are
// mirrored on the sheet, so "left half" is not the same side of the horse on
// both. CoatOverlay.tintIrisSector(eye, quadrants, rgb, strength) paints one.
/** NOT a claim and NOT a gene: how much of each iris a DEPIGMENTING claim
* actually reached. One MITF copy gives anything from one blue eye to extensive
* splash, so complete heterochromia (one iris) and sectoral heterochromia (part
* of one) are the same failed melanocyte colonisation stopped earlier - not a
* separate trait. Read ONCE by the composer, off the stored values of the
* locus that WON the claim; one roll, not one per locus, or a horse carrying two
* white loci could never have a single blue eye. */
public record EyeSpread(int right, int left) {
static final EyeSpread BOTH = ...; // 62% of blue-eyed horses
static final float BOTH_EYES = ..., ONE_EYE = ..., SECTORAL_IN_BOTH = ...;
static EyeSpread roll(Rng rng);
boolean heterochromatic(); boolean sectoral(); boolean empty();
}
// The composer therefore paints the PIGMENT claim over both whole irises FIRST
// and lays the blue over as much of each as the spread reached: a depigmented
// iris is pigment that never arrived, so the horse's own eye has to still be
// there underneath for the blue to stop short of.
/** The capability a Gene implements to MODIFY THE CUTIE MARK. CutieMarkGene owns
* the channel and draws the base emblem; every implementor then folds over it in
* codeOrder(). It can afford an open hook 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 or move a
* texture key. An implementor is called for EVERY combination it can have,
* including its own wild type, and decides for itself. */
public interface CutieMarkContribution {
CutieMarkGene.Mark modifyCutieMark(AllelePair pair, Genotype genotype,
Epigenome epigenome, CutieMarkGene.Mark mark);
}
// Mark gained `emissive` - the light locus is the first user, because a horse with
// four burning gold hooves wearing the one dull thing on its body reads as a bug.
// Every Mark field is something CutieMarkLayer honours; adding one means adding it
// there in the same change.
/** ...and its per-horse twin, for a gene whose effect VARIES between horses
* carrying the same alleles. Exactly EpigeneticTraitContribution one layer
* along. A gene implements one or the other, never both. */
public interface EpigeneticAbilityContribution {
List<GeneAbility> abilitiesFor(AllelePair pair, Genotype genotype,
GeneEpigenetics epigenetics);
}
// (HorseAbilities was SpecAbilities until 2026-09-04; renamed when built-in genes
// started contributing, since it was no longer about the spec path.)
public record Layer(String name, List<Mask> masks, Op op) {}
public record Mask(MaskType type, Params params, Combine combine, boolean invert) {}
public record Op(OpType type, Params params) {}
public record Knob(String name, double min, double max, boolean perLeg, double spread, boolean seed) {}
sealed interface Value { record Const(double v); record FromKnob(int index); record PerDose(...); }
// One horse's draw. Every knob, once, in declaration order, off the expressing copy.
public final class SpecValues {
static SpecValues draw(GeneSpec spec, Rng rng, int dose);
double get(Value value, int legIndex); // legIndex -1 anywhere that is not a leg
long seed(Value value, long fallback);
}
// A Gene that answers everything from a spec - indistinguishable downstream.
public final class SpecGene implements Gene {
SpecGene(GeneSpec spec);
GeneSpec spec();
int dose(AllelePair pair); // copies of the variant allele: 0, 1 or 2
}
// Loading. A bad file costs you that gene and no other.
public final class GeneSpecLoader {
record Result(List<GeneSpec> specs, List<String> errors) { boolean ok(); Result merge(Result); }
static Result fromDirectory(Path dir); // filename order; a missing folder is fine
static Result fromClasspath(); // horsegenetics/genes/index.json
static List<String> register(Result result);
static List<String> loadAndRegister(Path dropInDirectory);
}
// The interpreter. Same shape as the two Gene hooks.
public final class SpecPainter {
static PigmentField restrict(GeneSpec, SpecValues, CoatBuildContext, PigmentView);
static ColorField tint(GeneSpec, SpecValues, CoatBuildContext, PigmentView, ColorView);
}
wiki/gene-creator/js/spec-engine.js is the same interpreter in
JavaScript, so the creator's preview is the game's output. Changing what a mask
or an op means here without changing it there makes the tool lie.
./gradlew :common:bakeSpecFixtures then
node wiki/gene-creator/tools/check-parity.mjs is the tripwire —
it compares the schema tables and the painted result, and it is not optional
after touching either side.
Domain abstractions (non-coat)
enum Sex { MALE, FEMALE; String label(boolean adult); } // stallion/mare, colt/filly
record ParentStats(double speedMin, speedMax, healthMin, healthMax) {
static ParentStats of(Traits dam, Traits sire); // the ordinary way
static ParentStats of(double damSpeed, sireSpeed, damHealth, sireHealth);
int rankSpeed(double child); // 1 above both, 0 between, -1 below both
int rankHealth(double child);
}
// HorseStats.rollFoalStat is DELETED. A horse's speed and health are resolved
// from its genotype (see "The trait system" below); HorseRecord has no speed or
// health field at all, the same way it has no sex field.
interface HorseDatabase {
void record(HorseRecord r);
Optional<HorseRecord> lookup(UUID id);
boolean forget(UUID id);
List<HorseRecord> ancestorsOf(UUID id, int depth);
int offspringCount(UUID a, UUID b);
}
/** The string-level and Genome-level breeding seams. */
String GeneticCodeCombiner.combine(String motherCode, String fatherCode, Rng rng);
Genome GeneticCodeCombiner.combine(Genome mother, Genome father, Rng rng); // use this
/** A Genome detached from a horse, as code strings - what the stallion seed jar carries. */
record GenomeSample(String genotypeCode, String epigenomeCode) {
static GenomeSample of(Genome genome);
Genome genome();
Genome breedInto(Genome mare, Rng rng); // == mare.breedWith(genome(), rng)
}
Full detail on Breeding & pedigree, which is the single source of truth for records, pedigree and stat inheritance.
Breeds (common/breed/)
// A breed is a JSON file. This is the parsed form; the Builder is the escape
// hatch for one that needs behaviour, and Feral Mixed is its only user.
record Breed(String id, String name, boolean magical, List<String> biomes,
double spawnWeight, Set<BreedSource> sources,
Map<String, List<Combo>> genePools, StatScores scores, BreedBands bands,
double magicChance, Set<String> magicWhitelist, Set<String> magicBlacklist,
List<String> notes, Optional<PriceRange> price) {
boolean constrains(String geneKey);
boolean allows(BreedSource source); // the four-way checklist
boolean hasSpawnEgg(); // == allows(SPAWN_EGG)
BreedStatTargets statTargets(); // DERIVED from scores(), never stored
FounderTable founderTable(String geneKey);
}
// The registry. Lazy, because a breed file names genes and the gene registry is
// not complete until the host has loaded its own drop-ins.
Breeds.loadBuiltins(); // the classpath index
Breeds.loadFrom(Path directory); // .minecraft/phc/breeds/
Breeds.registerBundle(String json, String source); // the browser's one-array copy
Breeds.registerJava(Breed b); // the escape hatch
List<Breed> Breeds.forBiome(String biomeId, BreedSource source);
List<Breed> Breeds.from(BreedSource source);
// Reading and writing the file format. Warnings are for what this install has
// not got; only a malformed file throws.
Breed BreedSpecParser.parse(String json, String source, Consumer<String> warnings);
List<Breed> BreedSpecParser.parseAll(String jsonArray, String source, Consumer<String>);
String BreedSpecWriter.write(Breed breed);
// Did a gene splice carrot's allele reach the foal? Asked of the FOAL: does it
// carry an allele NEITHER parent had?
boolean SpliceOutcome.spliceReached(Genotype foal, Genotype dam, Genotype sire,
GameteBias damBias, GameteBias sireBias);
Full detail on the breed file format and Breeds.
The trait system (common/trait/)
Genotype in, body out. The full argument is on the horse’s body; this is the surface.
/** Everything a genotype says about the horse's body. Pure data, resolved not stored. */
record Traits(double speed, double health, double jump, double scale,
List<Condition> conditions) {
Viability viability(); // derived: the worst severity present
Optional<Condition> lethalCondition(); // what kills it, for the chat line
boolean lethal();
boolean hasConditions();
}
record Condition(String id, String name, String description, Severity severity) {
static Condition informational(id, name, description); // no cost - deafness
static Condition impairing(id, name, description); // fewer hearts
static Condition lethalAtBirth(id, name, description); // born, then dies
static Condition lethalAtConception(id, name, description); // no foal at all
}
enum Severity { INFORMATIONAL, IMPAIRING, LETHAL_AT_BIRTH, LETHAL_AT_CONCEPTION }
enum Viability { VIABLE, LETHAL_AT_BIRTH, LETHAL_AT_CONCEPTION }
/** The capability a Gene may ALSO implement to contribute something non-coat. */
interface TraitContribution {
void contribute(AllelePair pair, Genotype genotype, TraitBuilder out);
}
/** Marker: this contribution is a disorder, so the server's health.mode can skip it. */
interface HealthContribution extends TraitContribution { }
/** The per-horse twin: how MUCH is written on the allele copy, not on the allele.
* Every Rng here is a SeededRng on a stored, heritable copy seed, so this stays
* inside the determinism contract. */
interface EpigeneticTraitContribution {
void contribute(AllelePair pair, Genotype genotype, GeneEpigenetics epigenetics, TraitBuilder out);
}
/** Twin of CoatBuildContext's two accessors. Lives in genetics/, NOT trait/ -
* the ability side needs it as well, and genetics/ must not depend on trait/. */
interface GeneEpigenetics { // genetics/
EpiValues expressed(); // the copy this horse shows - one locus, one result
EpiValues copy(int slot); // one particular copy - for a CODOMINANT gene
// epigenome may be null -> every value reports its schema's midpoint.
static GeneEpigenetics forGene(Gene g, Genotype genotype, Epigenome epigenome);
}
/** The sink. Additions are applied before multipliers, so gene order buys nothing. */
final class TraitBuilder {
TraitBuilder addSpeed(double d);
TraitBuilder addHealth(double d); // health points - two per heart
TraitBuilder addJump(double d);
TraitBuilder addScale(double d); // the height loci
TraitBuilder multiplyScale(double f); // dwarfism, which is proportional
TraitBuilder multiplyScaleUnclamped(double f); // magical size: applied AFTER the
// natural clamp, bounded only by
// MAGICAL_MIN_SCALE / MAGICAL_MAX_SCALE
TraitBuilder multiplySpeedUnclamped(double f); // magical speed }
TraitBuilder multiplyHealthUnclamped(double f); // magical health } bounded by MAGICAL_MIN_FACTOR
TraitBuilder multiplyJumpUnclamped(double f); // magical jump } / MAGICAL_MAX_FACTOR (0.1 - 10)
TraitBuilder condition(Condition c); // de-duplicated on the condition id
}
/** One walk of Genes.codeOrder(). Pure: no Rng, no epigenetics, no entity. */
final class HorseTraits {
static Traits resolve(Genotype g); // no epigenome: MIDPOINTS
static Traits resolve(Genome g); // the real horse
static Traits resolve(Genotype g, boolean healthGenetics); // false = config "off"
static Traits resolve(Genotype g, Epigenome e, boolean healthGenetics);
static Traits baseline();
static final double BASE_SPEED = 0.1875;
static final double BASE_HEALTH = 22.0;
static final double BASE_JUMP = 0.5;
static final double BASE_SCALE = 1.0;
static final double MIN_HEALTH = 1.0; // never zero: that is a crash, not a sick horse
}
On the NeoForge side, HorseRecords.traitsOf(...) resolves honouring
the server config and HorseRecords.applyTraitsToEntity(...) writes
MOVEMENT_SPEED, MAX_HEALTH, JUMP_STRENGTH
and SCALE.
Writing a non-coat gene is an allele list, a combination table of
wildType expressions, a founder table and one
contribute method — there is no painter, because it never
paints. RecessiveDisorderGene is an abstract base covering the whole
of the “two alleles, only the double-variant does anything” shape; six
of the seven health genes extend it.
Display helpers
/** "eeaa nSpl" rather than the full slash-and-dash code - drops wild-type slots. */
String GeneCodeDisplay.shortForm(String code);
String GeneCodeDisplay.shortForm(Genotype genotype);
/** Greedy wrap for a sign / panel. Overflows the LAST line rather than dropping a gene. */
List<String> GeneCodeDisplay.wrap(Genotype genotype, int lines, int maxChars);
Build & test commands
./gradlew :common:test | Pure-Java logic, no Minecraft. The fastest loop — run this first when iterating on genetics or coats. |
|---|---|
./gradlew :common:bakeCoatSamples | Writes build/coat-samples/*.png and *_foal.png. No game launch. |
./gradlew :neoforge-26.1.2:build | Full compile + jar. Slow on the first run (downloads the SDK). |
./gradlew :neoforge-26.1.2:runClient | Launch the game with the mod. |
./gradlew :neoforge-26.1.2:runServer | Headless dedicated server. Does not auto-stop — kill it once it prints Done (…)! For help. |
Requires JDK 25; foojay-resolver-convention auto-provisions it.
GenotypeCatalog — every distinct genotype, computed on demand
public final class GenotypeCatalog {
static List<AllelePair> allPairsOf(Gene gene); // minus what canOccur AND sexConsistent reject
static List<AllelePair> distinctPairsOf(Gene gene); // one per Expression; all wild types are one group
static long size(); // the reduced catalogue - a long since the
// leopard complex doubled it past Integer.MAX_VALUE (2026-09-06)
static BigInteger totalGenotypes(); // the raw product, before the reduction.
// A BigInteger since 2026-09-04: the particle locus pushed the product past
// 2^63 and the long WRAPPED - to a smaller, entirely plausible-looking number.
static Genotype get(long index); // built on the spot, not fetched from a list
static List<Genotype> entries(); // a LAZY view - iterate it, don't collect it
}
Nothing is materialised. size() is arithmetic and
get(i) reads an odometer over each gene’s non-masking distinct
pairs; the one entry each masking combination owns is appended after all of them, in
gene order. It used to be an eagerly built List<Genotype>, which was
fine at a few thousand entries and stopped being fine the moment the white-pattern loci
took the catalogue past two million (it is now 1.85 billion) took the catalogue past two million — that list is hundreds of megabytes ofmdash; that list is hundreds of megabytes of
genotype nobody reads more than a few hundred of.
The consequence for callers: entries() is a view, so
entries().stream().filter(...) still walks every entry and building each
one. Sample it, or ask the arithmetic instead.
The catalogue no longer drives the horse dimension — that corridor is
random pens now, a fixed
DebugPenManager.PEN_COUNT of them. What still reads the catalogue is
the test suite, and whatever punnett display gets built.
ShowcaseGenotypes — a founder worth looking at
public final class ShowcaseGenotypes {
static final float MAGICAL_CHANCE = 0.5f;
static Genotype random(Rng rng); // the horse dimension’s stock draw
static List<Gene> naturalShowcaseGenes(); // paints, and not extension/agouti
static List<Gene> magicalShowcaseGenes();
static List<AllelePair> showcasePairs(Gene gene); // occurs, expresses, does not mask
}
An ordinary Genotype.random founder is the honest wild distribution,
and the honest wild distribution is mostly plain horses — correct for a wild
spawn, useless for a corridor whose whole job is to show what the genes do. So a
showcase draw is a wild draw with a floor: at least one natural coat gene
beyond extension and agouti is expressing, and with probability
MAGICAL_CHANCE a magical one is too. A draw that already clears the
floor is left exactly as it fell.
Two rules keep it honest. Forcing only ever picks 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 that masks neither counts toward the floor
nor gets forced — a masking combination (KIT dominant white, EDNRB lethal white),
which paints flat over everything, and letting that satisfy the floor would exempt
a quarter of the corridor from it.
Pure, and a founder path: the randomness is legitimate because these horses have no parents. Nothing else should call it — a wild spawn is a wild spawn.
WhitePattern — the two shapes white spotting comes in, and the face
public final class WhitePattern {
// the KIT shape: ragged margins growing inward from legs, belly and face
static PigmentField sabino(CoatBuildContext ctx, PigmentView coat,
String geneKey, double strength);
// the MITF / PAX3 shape: a sharply bounded waterline rising up the horse
static PigmentField splash(CoatBuildContext ctx, PigmentView coat,
String geneKey, double strength);
// the face marking every white locus draws from - one long and eight
// floats off epi, always, whatever marking falls out
static FaceMarking faceMarking(Rng epi, Skin skin, double strength, double jag);
// THE reference height for every vertical constant in every white locus:
// the top of the barrel, in body units above the hoof. On it 0.05 is the
// coronet, 0.10 the fetlock, 0.26 the knee and hock, 0.52 the underline
// and 1.0 the spine - so a number in a painter says something anatomical.
// Measuring against HorseSkinGeometry.bodyBounds instead (the whole-horse
// box, hoof to EAR TIP) is a defect, not a tuning miss: it has bitten
// splash, sabino and frame, each time by letting white over the spine.
public static double toplineHeight(Skin skin);
final class FaceMarking {
boolean covers(Part part, Face face, BodyPoint point);
boolean hasStar();
boolean hasStripe();
boolean hasSnip();
double halfWidth(); // of the stripe, in body units
boolean isBald();
boolean marksAnything();
String describe(); // "star and snip", "blaze", "bald face"
}
}
faceMarking is the one call a white locus makes for the head: three
independent components — a patch on the forehead, a band down the nose, a patch at
the nostrils — plus a width, from which every named marking falls out.
describe() reads the horseman’s term back off them; nothing ever chose
it. strength picks the distribution and jag is how far
the margin wanders in body units, which is how the same vocabulary gives
KIT a torn star and splash a clean blaze. Unlike the two body painters this
one is handed the Rng directly, because the caller is already inside its own
draw sequence. Full reference:
face markings.
One painter per family, not per gene and certainly not per allele: the
difference between two alleles at one locus is overwhelmingly a difference of degree,
so each outcome picks a
strength in [0, 1] and the painter does the rest. Both raise
that strength by how much of the coat they are handed is already de-pigmented —
see white finds white. Both read every
number by name from ctx.epigeneticsFor(geneKey), over the schema
fragments WhitePattern.sabinoSchema() and
splashSchema(), which the calling gene composes into its own
epiSchema().