OptiFine CTM emissive tiles: blocks keep their overlays first; every tile pairs

Block overlays come before tiles under the shared cap, so a pack's many
tiles can't crowd out vanilla blocks'; any tile named by its file pairs,
not only those under ctm/; the doc comment is back on load_optifine.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
some-du6eandClaude Opus 5.5 committed 2026-11-01 18:00:28 -06:00
1 parent 6d89cb0195
commit 752049907d
2 files changed
+32 -24

No files matched your search

@@ -28,21 +28,19 @@ pub fn load(root: &Path, sprites: &mut Sprites) {
}
}
/// OptiFine's emissive overlays of the block sprites already loaded (what the terrain draws;
/// items draw from the HUD atlas): for each
/// `<ns>:block/<name>`, or connected-texture tile `assets/<ns>/optifine/ctm/.../<name>.png`,
/// whose `<name><suffix>.png` sits beside it, the overlay is loaded too and paired with it, first
/// by name, at most [`MAX`] - [`NATIVE`] (native mods keep their share of
/// the table). A pack without such files adds nothing.
/// A connected-texture tile's sprite name (its resource path, `Sprites::insert_file`'s) without
/// `.png`.
fn ctm_tile(name: &str) -> Option<&str> {
let stem = name.strip_suffix(".png")?;
let mut parts = stem.strip_prefix("assets/")?.splitn(3, '/');
let (_namespace, top, rest) = (parts.next()?, parts.next()?, parts.next()?);
(matches!(top, "optifine" | "mcpatcher") && rest.starts_with("ctm/")).then_some(stem)
/// A connected-texture tile's sprite name without `.png`: tiles outside `textures/` are named by
/// their resource path (`Sprites::insert_file`, which only they use), block sprites by an id.
fn tile_file(name: &str) -> Option<&str> {
name.strip_prefix("assets/")?;
name.strip_suffix(".png")
}
/// OptiFine's emissive overlays of the block sprites already loaded (what the terrain draws;
/// items draw from the HUD atlas): for each
/// `<ns>:block/<name>`, or connected-texture tile `assets/<ns>/.../<name>.png`, whose
/// `<name><suffix>.png` sits beside it, the overlay is loaded too and paired with it, blocks
/// first, then tiles, each by name, at most [`MAX`] - [`NATIVE`] (native mods keep their share of
/// the table). A pack without such files adds nothing.
pub fn load_optifine(root: &Path, sprites: &mut Sprites) -> Vec<(String, String)> {
let suffix = horizon_optifine::emissive::suffix(root);
// Blocks with identical pixels share a layer (and the shader pairs by layer): a shared
@@ -51,7 +49,7 @@ pub fn load_optifine(root: &Path, sprites: &mut Sprites) -> Vec<(String, String)
for layer in sprites.layers.values() {
*sharing.entry(*layer).or_default() += 1;
}
let bases: Vec<String> = sprites
let mut bases: Vec<String> = sprites
.layers
.iter()
.filter(|(name, layer)| {
@@ -59,14 +57,16 @@ pub fn load_optifine(root: &Path, sprites: &mut Sprites) -> Vec<(String, String)
&& (name
.split_once(':')
.is_some_and(|(_, p)| p.starts_with("block/") && !p.ends_with(&suffix))
|| ctm_tile(name).is_some_and(|stem| !stem.ends_with(&suffix)))
|| tile_file(name).is_some_and(|stem| !stem.ends_with(&suffix)))
})
.map(|(name, _)| name.clone())
.collect();
// Blocks before tiles: a pack's many tiles must not crowd vanilla blocks' overlays out.
bases.sort_by_key(|name| (tile_file(name).is_some(), name.clone()));
let mut pairs = Vec::new();
for base in bases {
// A tile is named by its file, a block sprite by its id.
let (overlay, file) = match ctm_tile(&base) {
let (overlay, file) = match tile_file(&base) {
Some(stem) => {
let overlay = format!("{stem}{suffix}.png");
(overlay.clone(), root.join(overlay))
@@ -223,25 +223,33 @@ mod tests {
png(&tiles.join("0.png"), 230);
png(&tiles.join("0_glow.png"), 240);
png(&tiles.join("1.png"), 250);
for tile in ["0", "1"] {
// A tile outside `ctm/` (`tiles=~/glass/2`) is a tile all the same.
png(&root.join("assets/minecraft/optifine/glass/2.png"), 245);
png(
&root.join("assets/minecraft/optifine/glass/2_glow.png"),
246,
);
for tile in ["ctm/lamp/0", "ctm/lamp/1", "glass/2"] {
sprites
.insert_file(
&root,
&format!("assets/minecraft/optifine/ctm/lamp/{tile}.png"),
)
.insert_file(&root, &format!("assets/minecraft/optifine/{tile}.png"))
.unwrap();
}
let pairs = load_optifine(&root, &mut sprites);
assert_eq!(
pairs,
// Blocks first, then tiles.
[
(
"minecraft:block/deepslate".into(),
"minecraft:block/deepslate_glow".into()
),
(
"assets/minecraft/optifine/ctm/lamp/0.png".into(),
"assets/minecraft/optifine/ctm/lamp/0_glow.png".into()
),
(
"minecraft:block/deepslate".into(),
"minecraft:block/deepslate_glow".into()
"assets/minecraft/optifine/glass/2.png".into(),
"assets/minecraft/optifine/glass/2_glow.png".into()
),
]
);
+1 -1
View File
@@ -40,7 +40,7 @@ harness named checks that loader against vanilla).
| Item definitions and item models (all model types, display transforms, tints) | `items/`, `models/item/` | works | `test_item_model_parity` |
| Custom entity models (OptiFine/EMF): a `.jem` reshapes a mob's model layer, with its animations and texture; Entity Model Features' files win over OptiFine's | `optifine/cem/`, `emf/cem/` | partly | `mobs::cem` and `mobs::cem_anim` unit tests; no per-bone textures, no random models, no textures under `optifine/` (vanilla's own entity models are code in 26.3, which `test_entity_model_parity` checks) |
| Random entity textures (OptiFine/ETF): numbered variants and their rules (biome, height, name, profession, colour, baby, health, moon, time, weather, size, blocks), chosen per entity from its UUID and rechecked once a second | `optifine/random/`, `optifine/mob/`, `mcpatcher/mob/`, `etf/random/`, `textures/entity/` with a `.properties` | partly | `horizon-optifine` `random` and `random_textures` tests, `mobs::random_textures` tests; no `nbt.*` or ETF's extra conditions (teams, items...), no block entities |
| Connected textures (OptiFine/Continuity): every method (`ctm`, `ctm_compact` with `ctm.N`, `horizontal`, `vertical`, `horizontal+vertical`, `vertical+horizontal`, `top`, `random`, `repeat`, `fixed`, `overlay`, `overlay_ctm`/`_random`/`_repeat`/`_fixed`, and Continuity's `overlay_horizontal`/`_vertical`/`_h+v`/`_v+h`), `matchTiles`/`matchBlocks` and the file-name forms, `tiles` (ranges, names, paths, `<skip>`, `<default>`), `weight`, `faces`, `biomes`, `heights` (and the old `minHeight`/`maxHeight`), `connect`, `connectTiles`/`connectBlocks`, `innerSeams`, `weights`, `randomLoops`, `symmetry`, `linked`, `width`/`height`, `tintIndex`/`tintBlock`, `layer`; Continuity's `orient`, `prioritize`, `optifineOnly` and its stacked-pane culling; tiles a rule picked matched again by another; panes drawn with OptiFine's unmirrored UVs | `optifine/ctm/**/*.properties` (and `mcpatcher/ctm/`) in any namespace, with their tiles | partly | `horizon-optifine` `ctm` and `ctm::connect` tests (the tile tables read off OptiFine's template images), `connected` and `mesher::quarter_tests` tests; `name=` rules are not loaded (the client meshing chunks never receives block entities' custom names, so they could never match); `random` picks from its own fixed hash, so random tiles land in other places than in OptiFine (same weights); tiles glow with their `_e` overlays as blocks do |
| Connected textures (OptiFine/Continuity): every method (`ctm`, `ctm_compact` with `ctm.N`, `horizontal`, `vertical`, `horizontal+vertical`, `vertical+horizontal`, `top`, `random`, `repeat`, `fixed`, `overlay`, `overlay_ctm`/`_random`/`_repeat`/`_fixed`, and Continuity's `overlay_horizontal`/`_vertical`/`_h+v`/`_v+h`), `matchTiles`/`matchBlocks` and the file-name forms, `tiles` (ranges, names, paths, `<skip>`, `<default>`), `weight`, `faces`, `biomes`, `heights` (and the old `minHeight`/`maxHeight`), `connect`, `connectTiles`/`connectBlocks`, `innerSeams`, `weights`, `randomLoops`, `symmetry`, `linked`, `width`/`height`, `tintIndex`/`tintBlock`, `layer`; Continuity's `orient`, `prioritize`, `optifineOnly` and its stacked-pane culling; tiles a rule picked matched again by another; panes drawn with OptiFine's unmirrored UVs | `optifine/ctm/**/*.properties` (and `mcpatcher/ctm/`) in any namespace, with their tiles | partly | `horizon-optifine` `ctm` and `ctm::connect` tests (the tile tables read off OptiFine's template images), `connected` and `mesher::quarter_tests` tests; `name=` rules are not loaded (the client meshing chunks never receives block entities' custom names, so they could never match); `random` picks from its own fixed hash, so random tiles land in other places than in OptiFine (same weights); tiles glow with their `_e` overlays, sharing the emissive row's table and limits (block overlays come first) |
| Emissive textures (OptiFine/ETF): `<texture>_e.png` (or the pack's `suffix.emissive`) drawn full bright over mobs, their layers and armour, armour stands, the elytra, the dragon, thrown tridents, block entities, placed blocks and connected-texture tiles | `<texture>_e.png` beside its texture (also random variants' and blink frames'); the suffix from `optifine/emissive.properties`, `etf/emissive.properties` or `textures/emissive.properties` | partly | `horizon-optifine` `emissive` tests, `mobs::render::emissive_tests`, `block_entity_render` and `native_emissive` tests; at most 96 block overlays (native mods keep 32 more), cutout; none on translucent faces, nor on blocks whose texture is pixel-identical to another's (they share a layer); not over items, armour trims, other projectiles, minecarts or end crystals |
| Blinking textures (ETF): `<texture>_blink.png` and `_blink2.png`, `blinkLength`/`blinkFrequency` | `textures/entity/`, random variants' folders | works | `horizon-optifine` `blink` tests, `mobs::render` blink test; sleeping villagers keep their eyes shut |
| Textures, any resolution (16x to 512x) | `textures/` | works | atlases stitched from the overlay; Faithful 32x, Default HD 128x and FPBR measured in [reuse-pack-overlays.md](reuse-pack-overlays.md) |