package net.minecraft.client.resources.model.sprite; import com.google.common.collect.Lists; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.mojang.logging.LogUtils; import com.mojang.serialization.JsonOps; import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap; import it.unimi.dsi.fastutil.objects.Object2ObjectMap; import it.unimi.dsi.fastutil.objects.Object2ObjectMaps; import it.unimi.dsi.fastutil.objects.ObjectIterator; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.stream.Collectors; import net.minecraft.client.resources.model.ModelDebugName; import net.minecraft.util.GsonHelper; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; public class TextureSlots { public static final TextureSlots EMPTY = new TextureSlots(Map.of()); private static final char REFERENCE_CHAR = '#'; private final Map resolvedValues; private TextureSlots(final Map resolvedValues) { this.resolvedValues = resolvedValues; } @Nullable public Material getMaterial(String reference) { if (isTextureReference(reference)) { reference = reference.substring(1); } return (Material)this.resolvedValues.get(reference); } private static boolean isTextureReference(final String texture) { return texture.charAt(0) == '#'; } public static TextureSlots.Data parseTextureMap(final JsonObject texturesObject) { TextureSlots.Data.Builder builder = new TextureSlots.Data.Builder(); for (Entry entry : texturesObject.entrySet()) { parseEntry((String)entry.getKey(), (JsonElement)entry.getValue(), builder); } return builder.build(); } private static void parseEntry(final String slot, final JsonElement value, final TextureSlots.Data.Builder output) { if (GsonHelper.isStringValue(value) && isTextureReference(value.getAsString())) { output.addReference(slot, value.getAsString().substring(1)); } else { output.addTexture(slot, Material.CODEC.parse(JsonOps.INSTANCE, value).getOrThrow(JsonParseException::new)); } } public record Data(Map values) { public static final TextureSlots.Data EMPTY = new TextureSlots.Data(Map.of()); public static class Builder { private final Map textureMap = new HashMap(); public TextureSlots.Data.Builder addReference(final String slot, final String reference) { this.textureMap.put(slot, new TextureSlots.Reference(reference)); return this; } public TextureSlots.Data.Builder addTexture(final String slot, final Material material) { this.textureMap.put(slot, new TextureSlots.Value(material)); return this; } public TextureSlots.Data build() { return this.textureMap.isEmpty() ? TextureSlots.Data.EMPTY : new TextureSlots.Data(Map.copyOf(this.textureMap)); } } } private record Reference(String target) implements TextureSlots.SlotContents { } public static class Resolver { private static final Logger LOGGER = LogUtils.getLogger(); private final List entries = new ArrayList(); public TextureSlots.Resolver addLast(final TextureSlots.Data data) { this.entries.addLast(data); return this; } public TextureSlots.Resolver addFirst(final TextureSlots.Data data) { this.entries.addFirst(data); return this; } public TextureSlots resolve(final ModelDebugName debugNameProvider) { if (this.entries.isEmpty()) { return TextureSlots.EMPTY; } else { Object2ObjectMap resolved = new Object2ObjectArrayMap<>(); Object2ObjectMap unresolved = new Object2ObjectArrayMap<>(); for (TextureSlots.Data data : Lists.reverse(this.entries)) { data.values.forEach((slot, contents) -> { switch (contents) { case TextureSlots.Value value: unresolved.remove(slot); resolved.put(slot, value.material()); break; case TextureSlots.Reference reference: resolved.remove(slot); unresolved.put(slot, reference); break; default: throw new MatchException(null, null); } }); } if (unresolved.isEmpty()) { return new TextureSlots(resolved); } else { boolean hasChanges = true; while (hasChanges) { hasChanges = false; ObjectIterator> iterator = Object2ObjectMaps.fastIterator(unresolved); while (iterator.hasNext()) { it.unimi.dsi.fastutil.objects.Object2ObjectMap.Entry entry = (it.unimi.dsi.fastutil.objects.Object2ObjectMap.Entry)iterator.next(); Material maybeResolved = resolved.get(((TextureSlots.Reference)entry.getValue()).target); if (maybeResolved != null) { resolved.put((String)entry.getKey(), maybeResolved); iterator.remove(); hasChanges = true; } } } if (!unresolved.isEmpty()) { LOGGER.warn( "Unresolved texture references in {}:\n{}", debugNameProvider.debugName(), unresolved.entrySet() .stream() .map(e -> "\t#" + (String)e.getKey() + "-> #" + ((TextureSlots.Reference)e.getValue()).target + "\n") .collect(Collectors.joining()) ); } return new TextureSlots(resolved); } } } } public sealed interface SlotContents permits TextureSlots.Value, TextureSlots.Reference { } private record Value(Material material) implements TextureSlots.SlotContents { } }