From 4cad82b4a9cc6e25cce236866c389a5a45594933 Mon Sep 17 00:00:00 2001 From: Kacpherek Date: Mon, 26 Oct 2026 23:14:31 +0100 Subject: [PATCH 1/2] Draw the variant of rabbits, parrots and axolotls, salmon sizes and the baby goat, polar bear, ocelot, turtle and armadillo RabbitRenderer, ParrotRenderer, AxolotlRenderer and SalmonRenderer pick their texture or model from the variant the mob already carries, and the baby models and textures of the other AgeableMobRenderer species are used. A baby zombie villager uses its own texture. --- crates/horizon-client/src/mobs/render.rs | 69 ++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/crates/horizon-client/src/mobs/render.rs b/crates/horizon-client/src/mobs/render.rs index 077fd38a6..6f39686a3 100644 --- a/crates/horizon-client/src/mobs/render.rs +++ b/crates/horizon-client/src/mobs/render.rs @@ -177,8 +177,71 @@ pub fn body(m: &Mob) -> (String, String) { "frog#main".into(), format!("entity/frog/frog_{}", climate(m)), ), - S::Rabbit => ( - baby_or(m, "rabbit#main", "rabbit_baby#main"), + // RabbitRenderer.getTextureLocation: the Toast name tag, else the + // variant (the killer bunny's id is 99). + S::Rabbit => { + let name = if m.custom_name.as_deref() == Some("Toast") { + "toast" + } else { + match m.extra.variant { + 1 => "white", + 2 => "black", + 3 => "white_splotched", + 4 => "gold", + 5 => "salt", + 99 => "caerbannog", + _ => "brown", + } + }; + ( + baby_or(m, "rabbit#main", "rabbit_baby#main"), + b(&format!("entity/rabbit/rabbit_{name}")), + ) + } + // ParrotRenderer.getVariantTexture. + S::Parrot => { + let name = ["red_blue", "blue", "green", "yellow_blue", "grey"] + [usize::from(m.extra.variant).min(4)]; + (m.kind.model_key(), format!("entity/parrot/parrot_{name}")) + } + // AxolotlRenderer.getTextureLocation. + S::Axolotl => { + let name = + ["lucy", "wild", "gold", "cyan", "blue"][usize::from(m.extra.variant).min(4)]; + ( + baby_or(m, "axolotl#main", "axolotl_baby#main"), + b(&format!("entity/axolotl/axolotl_{name}")), + ) + } + // SalmonRenderer.submit: the model of the salmon's size. + S::Salmon => { + let model = match m.extra.variant { + 0 => "salmon_small#main", + 2 => "salmon_large#main", + _ => "salmon#main", + }; + (model.into(), "entity/fish/salmon".into()) + } + // The baby models and textures of AgeableMobRenderer's other + // subclasses. + S::Goat => ( + baby_or(m, "goat#main", "goat_baby#main"), + b(&m.kind.texture), + ), + S::PolarBear => ( + baby_or(m, "polar_bear#main", "polar_bear_baby#main"), + b(&m.kind.texture), + ), + S::Ocelot => ( + baby_or(m, "ocelot#main", "ocelot_baby#main"), + b(&m.kind.texture), + ), + S::Turtle => ( + baby_or(m, "turtle#main", "turtle_baby#main"), + b(&m.kind.texture), + ), + S::Armadillo => ( + baby_or(m, "armadillo#main", "armadillo_baby#main"), b(&m.kind.texture), ), S::Pig => { @@ -241,7 +304,7 @@ pub fn body(m: &Mob) -> (String, String) { ), S::ZombieVillager => ( baby_or(m, "zombie_villager#main", "zombie_villager_baby#main"), - "entity/zombie_villager/zombie_villager".into(), + b("entity/zombie_villager/zombie_villager"), ), S::Villager => ( baby_or(m, "villager#main", "villager_baby#main"), From 0d496fdc8fc820d5f299102c57c4c17c8bd5da1a Mon Sep 17 00:00:00 2001 From: Kacpherek Date: Mon, 26 Oct 2026 23:14:31 +0100 Subject: [PATCH 2/2] Save and load the variant of axolotls, rabbits, parrots, salmon and tropical fish --- crates/horizon-client/src/entity_nbt.rs | 67 ++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/crates/horizon-client/src/entity_nbt.rs b/crates/horizon-client/src/entity_nbt.rs index 161ca12e9..1b5b8311f 100644 --- a/crates/horizon-client/src/entity_nbt.rs +++ b/crates/horizon-client/src/entity_nbt.rs @@ -403,7 +403,36 @@ pub fn mob_to_nbt(m: &Mob, uuid: [i32; 4], raw: Option<&Tag>) -> Tag { crate::mobs::villager::write_nbt(m, map); } "minecraft:wandering_trader" => put_offers(map, m), - "minecraft:axolotl" => write_axolotl_memories(m, map), + "minecraft:axolotl" => { + write_axolotl_memories(m, map); + map.insert( + "Variant".into(), + Tag::Int(i32::from(m.extra.variant.min(4))), + ); + } + // Rabbit.addAdditionalSaveData (the legacy id; 99 is the killer bunny). + "minecraft:rabbit" => { + map.insert("RabbitType".into(), Tag::Int(i32::from(m.extra.variant))); + } + "minecraft:parrot" => { + map.insert( + "Variant".into(), + Tag::Int(i32::from(m.extra.variant.min(4))), + ); + } + "minecraft:salmon" => { + let size = SALMON_SIZES[usize::from(m.extra.variant).min(2)]; + map.insert("type".into(), string(size)); + } + // TropicalFish.addAdditionalSaveData: the packed variant. + "minecraft:tropical_fish" => { + let pattern = m.extra.variant.min(11); + let packed = i32::from(pattern >= 6) + | i32::from(pattern % 6) << 8 + | i32::from(m.extra.variant2 & 15) << 16 + | i32::from(m.extra.variant3 & 15) << 24; + map.insert("Variant".into(), Tag::Int(packed)); + } "minecraft:allay" => { // InventoryCarrier: the fetched stack; the liked player memory. let mut inv = Vec::new(); @@ -843,6 +872,7 @@ pub fn mob_from_nbt(tag: &Tag, types: &HashMap>, id: u32) - if m.species() == crate::mobs::Species::Axolotl { restore_axolotl_memories(&mut m, tag); } + read_variant(&mut m, tag); m.fire_ticks = number(tag.get("Fire")).unwrap_or(0) as i32; m.air = number(tag.get("Air")).unwrap_or(300) as i32; if m.species() == crate::mobs::Species::Allay { @@ -2024,6 +2054,41 @@ fn write_axolotl_memories(m: &Mob, map: &mut Compound) { } } } +/// The variant tags `Axolotl`, `Rabbit`, `Parrot`, `Salmon` and +/// `TropicalFish` read; a missing tag is the default variant (a medium +/// salmon, otherwise index 0). +fn read_variant(m: &mut Mob, tag: &Tag) { + use crate::mobs::Species as S; + let int = |key| number(tag.get(key)).unwrap_or(0) as i32; + match m.species() { + S::Axolotl | S::Parrot => m.extra.variant = int("Variant").clamp(0, 4) as u8, + // Rabbit.Variant.LEGACY_CODEC: an unknown id is brown. + S::Rabbit => { + m.extra.variant = match int("RabbitType") { + v @ (0..=5 | 99) => v as u8, + _ => 0, + } + } + S::Salmon => { + m.extra.variant = text(tag.get("type")) + .and_then(|t| SALMON_SIZES.iter().position(|v| *v == t)) + .unwrap_or(1) as u8; + } + S::TropicalFish => { + let packed = int("Variant"); + let (size, index) = (packed & 0xFF, (packed >> 8) & 0xFF); + // TropicalFish.Pattern.byId: an unknown id is a kob. + m.extra.variant = if size <= 1 && index < 6 { + (size * 6 + index) as u8 + } else { + 0 + }; + m.extra.variant2 = ((packed >> 16) & 15) as u8; + m.extra.variant3 = ((packed >> 24) & 15) as u8; + } + _ => {} + } +} fn restore_axolotl_memories(m: &mut Mob, tag: &Tag) { let memories = tag.get("Brain").and_then(|b| b.get("memories")); let ttl = memories