Pixel Dream StudiosWiki

Color layers

Color slots on the entity, layer textures, rendering, dyes, biome colors and override rules from code.

On the entity

AbstractSpellCastingPet implements LayerColorHolder:

int  getLayerColor(int slot);              // 0xRRGGBB, or NO_LAYER_COLOR (-1) for the original look
void setLayerColor(int slot, int rgb);     // NO_LAYER_COLOR resets the slot
boolean canBeDyed();                        // override: true for dyeable familiars
int getEyeDyeLayerSlot();                   // override: the slot shrunk dyes color (the eyes), or -1
int getSneakingEyeDyeLayerSlot();           // override: the slot shrunk dyes color while sneaking, or -1
int getSneakingDyeLayerSlot();              // override: the slot normal dyes color while sneaking, or -1
void initializeLayerColors(LevelReader level, @Nullable MobSpawnType reason); // spawn hook
ConstantValueMeaning
LayerColorHolder.NO_LAYER_COLOR-1No color: the original layer
AbstractSpellCastingPet.LAYER_COLOR_SLOTS5Slots 0 to 4
AbstractSpellCastingPet.DYE_LAYER_SLOT0The slot dyes change

Colors are synced to clients and saved as "LayerColors" in the familiar's NBT, which the roster, selection screen and familiar houses also store. Give your slots readable names on your entity:

public static final int HAT_LAYER = 0;
public static final int DOG_LAYER = 1;
public static final int CROSSBOW_LAYER = 2;

The spawn hook

initializeLayerColors runs once per familiar, on the server:

How the familiar appearsCalled?reason
Natural spawn, spawner, spawn egg, /summonYes, from finalizeSpawnThe MobSpawnType
Created by code and added with addFreshEntity (hatched from typed eggs, shards)Yes, from onAddedToLevelnull
Loaded from a save, resummoned, released from a houseNon/a
/summon with a LayerColors tagNo (the given colors are kept)n/a

Layer textures

Each colored part needs two textures with the same size and UV layout as the base texture:

FileContentUsed when
my_layer.pngOnly that part, in its original colors; everything else transparentThe slot has no color
my_layer_bw.pngThe same pixels in grayscaleThe slot has a color
  • Extract the layers from the base texture so the pixels match exactly, then cut those areas out of the base texture, so the layer is the only surface there.
  • Make the grayscale version bright: highlights close to white, since the tint can only darken.
  • Don't let two layers of one familiar share pixels: the later layer would draw over the earlier one.
  • Fully opaque pixels only: layers use cutout rendering.
  • Glowmasks: the base _glowmask is drawn after the color layers at full brightness, so remove the layer areas from it. If part of a layer should glow, give the layer its own my_layer_glowmask.png and my_layer_bw_glowmask.png, and use .glowing().

Rendering

Add one FamiliarColorLayer per slot, before AutoGlowingGeoLayer:

addRenderLayer(new FamiliarColorLayer<>(this, HunterPetEntity.HAT_LAYER,
        MyMod.id("textures/entity/layers/hunter_pet_hat_layer.png"),
        MyMod.id("textures/entity/layers/hunter_pet_hat_layer_bw.png")));

FamiliarColorLayer(renderer, slot, defaultTexture, tintTexture); defaultTexture can be null to draw nothing when the slot has no color. Add .glowing() for an emissive pass using the layers' glowmasks. The layer follows the body's visibility, flashes red when hurt, and uses culled render types to match the renderer above.

Anything else that draws the base texture (decoys, shadows...) needs the layer too, or the cut-out areas show as holes. For entities that aren't familiars, implement LayerColorHolder and return NO_LAYER_COLOR, or copy a familiar's colors into your own synced data.

Dyeable familiars

Override canBeDyed() to return true, and add a FamiliarColorLayer on AbstractSpellCastingPet.DYE_LAYER_SLOT. The lib handles the interaction.

For a random dye color on natural spawns:

@Override
protected void initializeLayerColors(LevelReader level, @Nullable MobSpawnType reason) {
    FamiliarColors.rollNaturalDye(this, reason);
}

Familiars hatched from typed eggs (reason == null) keep the original look. FamiliarColors.NATURAL_DEFAULT_LOOK_CHANCE (25%) is the chance of keeping it on a natural spawn.

Eyes (shrunk dyes)

Shrunk dyes color the slot returned by getEyeDyeLayerSlot(). For a familiar with a second part colored the same way (a companion's eyes, a lantern...), getSneakingEyeDyeLayerSlot() returns the slot colored while sneaking; familiars without one color their eyes whether sneaking or not.

public static final int EYES_LAYER = 3;
public static final int DOG_EYES_LAYER = 4;

@Override
public int getEyeDyeLayerSlot() {
    return EYES_LAYER;
}

@Override
public int getSneakingEyeDyeLayerSlot() {
    return DOG_EYES_LAYER;
}

This doesn't depend on canBeDyed(): a familiar can have dyeable eyes without dyeable clothes. Keep slot 0 for the main color, and add a FamiliarColorLayer for each eye slot. If the eyes glow, use .glowing() with the eye layers' glowmasks so dyed eyes glow in their color.

FamiliarDyes has the shrunk dye helpers: isShrunkDye, canBeShrunk and shrink.

Second dye part

For a familiar with two parts colored by normal dyes (clothes and wings, for example), return the second part's slot from getSneakingDyeLayerSlot(). Dyes color it while the owner sneaks, and slot 0 otherwise. Familiars without one color slot 0 whether sneaking or not.

public static final int WINGS_LAYER = 2;

@Override
public boolean canBeDyed() {
    return true;
}

@Override
public int getSneakingDyeLayerSlot() {
    return WINGS_LAYER;
}

Like the eyes, this doesn't depend on canBeDyed(), and the slot needs its own FamiliarColorLayer. Override rules for that dye still win over it.

Biome colors

public static final BiomeLayerColorData DRUID = new BiomeLayerColorData("familiar_colors/druid",
        new BiomeLayerColorData.Layer("wood", DruidPetEntity.WOOD_LAYER),
        new BiomeLayerColorData.Layer("leaf", DruidPetEntity.LEAF_LAYER));

Register it with event.addListener(...) in AddReloadListenerEvent, apply it in the spawn hook with DRUID.applyTo(this, level), and ship a default file in data/<modid>/familiar_colors/druid/. Two loaders can read the same folder: a loader reading the druid's folder with new Layer("wood", HunterPetEntity.CROSSBOW_LAYER) gives the hunter's crossbow the druid's wood colors.

Override rules from your mod

No code needed: add your rules to src/main/resources/data/familiarslib/familiar_color_overrides/overrides.json with "replace": false, or use your own file. To check rules from code, use FamiliarColorOverrides.INSTANCE.find(familiar, stack). Rules only exist on the server.

Changing colors from code

After changing colors or cosmetics of a tamed familiar from code, keep the roster in sync:

familiar.setLayerColor(MyFamiliar.HAT_LAYER, 0xA83232);
FamiliarManager.updateFamiliarData(familiar);
FamiliarSync.snapshot(serverPlayer, familiar.getUUID());

Identity Vessel

A filled vessel stores a familiarslib:familiar_identity component (FamiliarIdentity): the familiar's entity id and its 3 layer colors (-1 for a layer in its original look). FamiliarIdentityHandler handles taking and pouring, owner-only.

Mixing works like vanilla dyes on leather armor: the colors are averaged, then the average brightness is restored, so mixes don't get muddy. Mixed colors aren't in the compendium catalog, and FamiliarCompendium.discover only unlocks colors the catalog lists, so they're never tracked.

On this page