diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/Mixins.java b/src/main/java/com/gtnewhorizons/angelica/mixins/Mixins.java index 1147da6d1..3982795e7 100644 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/Mixins.java +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/Mixins.java @@ -196,7 +196,8 @@ public enum Mixins implements IMixins { .setPhase(Phase.EARLY) .setApplyIf(() -> AngelicaConfig.enableIris) .addClientMixins( - "shaders.MixinEntityRenderer" + "shaders.MixinEntityPickupFX" + , "shaders.MixinEntityRenderer" , "shaders.MixinGuiIngameForge" , "shaders.MixinFramebuffer" , "shaders.MixinItem" @@ -208,6 +209,7 @@ public enum Mixins implements IMixins { , "shaders.MixinRenderGlobal" , "shaders.MixinRenderHorse" , "shaders.MixinRenderItem" + , "shaders.MixinRenderManager" , "shaders.MixinRenderPlayerArmor" , "shaders.MixinTileEntityBeaconRenderer" ) diff --git a/src/main/java/net/coderbot/iris/shaderpack/ShaderPack.java b/src/main/java/net/coderbot/iris/shaderpack/ShaderPack.java index 6544e6b4f..5456c2c18 100644 --- a/src/main/java/net/coderbot/iris/shaderpack/ShaderPack.java +++ b/src/main/java/net/coderbot/iris/shaderpack/ShaderPack.java @@ -73,6 +73,7 @@ public class ShaderPack { private final Map dimensionMap; private final Map dimensionProgramSets; + private final Set foldersWithShaderFiles; private final Function sourceProvider; private final ShaderProperties shaderProperties; private boolean hasLoggedCacheLimitReached = false; @@ -114,6 +115,7 @@ public class ShaderPack { // Parse dimension.properties to get dimension name -> folder mappings this.dimensionMap = new HashMap<>(); + this.foldersWithShaderFiles = new HashSet<>(); Iris.logger.info("Dimension shader cache size: {} (3 vanilla pinned + {} modded), configurable via -Diris.dimensionCacheSize", MAX_DIMENSION_CACHE, Math.max(0, MAX_DIMENSION_CACHE - PINNED_DIMENSIONS.size())); @@ -135,7 +137,9 @@ public class ShaderPack { if (!dimensionFolders.isEmpty()) { for (String folderName : dimensionFolders) { boolean folderExists = checkAndAddDimensionFolder(starts, root, potentialFileNames, folderName); - if (!folderExists) { + if (folderExists) { + foldersWithShaderFiles.add(folderName); + } else { Iris.logger.warn("dimension.properties references folder '{}' but it doesn't exist or has no shader files", folderName); } } @@ -164,6 +168,7 @@ public class ShaderPack { boolean exists = checkAndAddDimensionFolder(starts, root, potentialFileNames, folderName); if (exists) { foundFolders.add(folderName); + foldersWithShaderFiles.add(folderName); } } catch (IOException e) { Iris.logger.warn("Failed to scan dimension folder: {}", folderName, e); @@ -173,8 +178,8 @@ public class ShaderPack { Iris.logger.warn("Failed to scan for dimension folders", e); } - // Set up default name mappings for vanilla dimensions only - // Other world{ID} folders are handled via dimension ID fallback at runtime + // Set up dimension mappings + // If world0 folder exists with shader files, use it for Overworld. Otherwise use base for all oher dims. if (foundFolders.contains("world0")) { dimensionMap.put("Overworld", "world0"); } @@ -572,15 +577,24 @@ public class ShaderPack { * The LinkedHashMap maintains access order, so oldest entries are at the front. */ private void evictOldDimensions() { - // If cache is disabled, evict everything except pinned + // Build set of pinned folders (folders that vanilla dimensions map to) + Set pinnedFolders = new HashSet<>(); + for (String pinnedDimName : PINNED_DIMENSIONS) { + String folder = dimensionMap.get(pinnedDimName); + if (folder != null) { + pinnedFolders.add(folder); + } + } + + // If cache is disabled, evict everything except pinned folders if (MAX_DIMENSION_CACHE == 0) { - dimensionProgramSets.entrySet().removeIf(e -> !PINNED_DIMENSIONS.contains(e.getKey())); + dimensionProgramSets.entrySet().removeIf(e -> !pinnedFolders.contains(e.getKey())); return; } // Count non-pinned entries long nonPinnedCount = dimensionProgramSets.entrySet().stream() - .filter(e -> !PINNED_DIMENSIONS.contains(e.getKey())) + .filter(e -> !pinnedFolders.contains(e.getKey())) .count(); int maxNonPinned = Math.max(0, MAX_DIMENSION_CACHE - PINNED_DIMENSIONS.size()); @@ -597,7 +611,7 @@ public class ShaderPack { // Find the first (oldest/least recently used) non-pinned entry String toEvict = null; for (Map.Entry entry : dimensionProgramSets.entrySet()) { - if (!PINNED_DIMENSIONS.contains(entry.getKey())) { + if (!pinnedFolders.contains(entry.getKey())) { toEvict = entry.getKey(); break; } @@ -643,16 +657,20 @@ public class ShaderPack { } // If still no match, try world{ID} folder as fallback for backward compatibility + // But only if that folder actually has shader files! if (folderName == null) { - folderName = "world" + dimensionId; + String worldFolder = "world" + dimensionId; + if (foldersWithShaderFiles.contains(worldFolder)) { + folderName = worldFolder; + } } - // If we have a folder name, try to get or create its ProgramSet - if (folderName != null) { + // If we have a folder name that contains shader files, try to get or create its ProgramSet + if (folderName != null && foldersWithShaderFiles.contains(folderName)) { ProgramSet programSet = dimensionProgramSets.get(folderName); if (programSet == null) { - // Create ProgramSet on-demand for any dimension folder + // Create ProgramSet on-demand for dimension folder try { programSet = new ProgramSet( AbsolutePackPath.fromAbsolutePath("/" + folderName), @@ -660,23 +678,14 @@ public class ShaderPack { shaderProperties, this ); - } catch (Exception e) { - // If the folder doesn't exist, just fall back to base silently. - // Expected since I don't think literally anyone has ever done custom dimension shaders - String message = e.getMessage(); - if (message != null && (message.contains("does not contain") || message.contains("No sources found"))) { - Iris.logger.debug("Dimension folder '" + folderName + "' not found, using base shaders"); - } else { - Iris.logger.error("Failed to create ProgramSet for dimension folder '{}', falling back to base", folderName, e); - } - programSet = null; - } - - if (programSet != null) { dimensionProgramSets.put(folderName, programSet); - // Check cache size limit and evict LRU dimensions if needed + // Check cache size limit and evict LRU dimensions if needed evictOldDimensions(); + } catch (Exception e) { + // This shouldn't happen but just in case. + Iris.logger.error("Failed to create ProgramSet for dimension folder '{}', falling back to base", folderName, e); + programSet = null; } } diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinEntityPickupFX.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinEntityPickupFX.java new file mode 100644 index 000000000..7ee6f63fc --- /dev/null +++ b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinEntityPickupFX.java @@ -0,0 +1,33 @@ +package com.gtnewhorizons.angelica.mixins.early.shaders; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import net.coderbot.iris.layer.GbufferPrograms; +import net.minecraft.client.particle.EntityPickupFX; +import net.minecraft.client.renderer.entity.RenderManager; +import net.minecraft.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +/** + * Mixin to handle EntityPickupFX rendering items during pickup animation. + * EntityPickupFX renders particles during the PARTICLES phase, but it renders + * the actual EntityItem using RenderManager.renderEntityWithPosYaw. We need + * to switch to ENTITIES phase during this render to apply proper materials. + */ +@Mixin(EntityPickupFX.class) +public class MixinEntityPickupFX { + + @WrapOperation( + method = "renderParticle", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/entity/RenderManager;func_147940_a(Lnet/minecraft/entity/Entity;DDDFF)Z") + ) + private boolean iris$wrapPickupRender(RenderManager renderManager, Entity entity, double x, double y, double z, float yaw, float partialTicks, Operation original) { + GbufferPrograms.beginEntities(); + try { + return original.call(renderManager, entity, x, y, z, yaw, partialTicks); + } finally { + GbufferPrograms.endEntities(); + } + } +} diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinItemRenderer.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinItemRenderer.java index aef367566..aa7377e41 100644 --- a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinItemRenderer.java +++ b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinItemRenderer.java @@ -40,7 +40,7 @@ public class MixinItemRenderer { ItemStack itemStack = mc.thePlayer.getCurrentEquippedItem(); if (itemStack == null || itemStack.getItem() == null) { - CapturedRenderingState.INSTANCE.setCurrentRenderedItem(-1); + CapturedRenderingState.INSTANCE.setCurrentRenderedItem(0); return; } @@ -83,6 +83,6 @@ public class MixinItemRenderer { at = @At("RETURN") ) private void iris$resetHeldItemId(float partialTicks, CallbackInfo ci) { - CapturedRenderingState.INSTANCE.setCurrentRenderedItem(-1); + CapturedRenderingState.INSTANCE.setCurrentRenderedItem(0); } } diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderBiped.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderBiped.java index 6ad981470..67acc8546 100644 --- a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderBiped.java +++ b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderBiped.java @@ -32,7 +32,7 @@ public class MixinRenderBiped { ItemStack itemStack = entity.func_130225_q(3 - armorSlot); if (itemStack == null || itemStack.getItem() == null) { - CapturedRenderingState.INSTANCE.setCurrentRenderedItem(-1); + CapturedRenderingState.INSTANCE.setCurrentRenderedItem(0); original.call(instance, model); return; } @@ -54,7 +54,7 @@ public class MixinRenderBiped { at = @At("HEAD") ) private void iris$resetAtStart(EntityLiving entity, double x, double y, double z, float entityYaw, float partialTicks, org.spongepowered.asm.mixin.injection.callback.CallbackInfo ci) { - CapturedRenderingState.INSTANCE.setCurrentRenderedItem(-1); + CapturedRenderingState.INSTANCE.setCurrentRenderedItem(0); } /** @@ -66,7 +66,7 @@ public class MixinRenderBiped { at = @At("HEAD") ) private void iris$resetInEquippedItems(EntityLiving entity, float partialTicks, org.spongepowered.asm.mixin.injection.callback.CallbackInfo ci) { - CapturedRenderingState.INSTANCE.setCurrentRenderedItem(-1); + CapturedRenderingState.INSTANCE.setCurrentRenderedItem(0); } /** @@ -90,6 +90,6 @@ public class MixinRenderBiped { original.call(itemRenderer, entity, itemStack, renderPass); // Reset after rendering the held item - CapturedRenderingState.INSTANCE.setCurrentRenderedItem(-1); + CapturedRenderingState.INSTANCE.setCurrentRenderedItem(0); } } diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderGlobal.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderGlobal.java index 9c4ca00d2..c76485ad9 100644 --- a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderGlobal.java +++ b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderGlobal.java @@ -14,8 +14,10 @@ import net.coderbot.iris.pipeline.WorldRenderingPipeline; import net.coderbot.iris.shaderpack.materialmap.NamespacedId; import net.coderbot.iris.uniforms.CapturedRenderingState; import net.minecraft.client.renderer.RenderGlobal; +import net.minecraft.client.renderer.culling.ICamera; import net.minecraft.client.renderer.entity.RenderManager; import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.EntityList; import net.minecraft.entity.effect.EntityLightningBolt; import net.minecraft.entity.player.EntityPlayer; @@ -97,18 +99,43 @@ public class MixinRenderGlobal { angelica$cachedEntityIdMap = entityIdMap; } - if (entity instanceof EntityLightningBolt) { - entityId = entityIdMap.applyAsInt(LIGHTNING_BOLT_ID); - } else { - String entityType = EntityList.getEntityString(entity); - if (entityType != null) { - int cached = angelica$entityIdCache.getInt(entityType); - if (cached == Integer.MIN_VALUE) { - cached = entityIdMap.applyAsInt(new NamespacedId(entityType)); - angelica$entityIdCache.put(entityType, cached); - } - entityId = cached; + String entityType = EntityList.getEntityString(entity); + String cacheKey; + + if (entityType != null) { + // Use registered entity name + cacheKey = entityType; + int cached = angelica$entityIdCache.getInt(cacheKey); + if (cached == Integer.MIN_VALUE) { + cached = entityIdMap.applyAsInt(new NamespacedId(entityType)); + angelica$entityIdCache.put(cacheKey, cached); } + entityId = cached; + } else { + // Unregistered entity check + Class entityClass = entity.getClass(); + String className = entityClass.getName(); + cacheKey = className; + + int cached = angelica$entityIdCache.getInt(cacheKey); + if (cached == Integer.MIN_VALUE) { + // Try simple class name first (e.g., "EntityLightningBolt") + String simpleClassName = entityClass.getSimpleName(); + cached = entityIdMap.applyAsInt(new NamespacedId(simpleClassName)); + + // If not found, try FQN (e.g., "net.minecraft.entity.effect.EntityLightningBolt") + if (cached == -1) { + cached = entityIdMap.applyAsInt(new NamespacedId(className)); + } + + // Special case: Shaderpacks have "minecraft:lightning_bolt" so we should continue to provide this + if (cached == -1 && entity instanceof EntityLightningBolt) { + cached = entityIdMap.applyAsInt(LIGHTNING_BOLT_ID); + } + + angelica$entityIdCache.put(cacheKey, cached); + } + entityId = cached; } } diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderHorse.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderHorse.java index eb6ab62fe..091dfe129 100644 --- a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderHorse.java +++ b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderHorse.java @@ -78,7 +78,7 @@ public abstract class MixinRenderHorse { // Reset only if we set it if (hasArmor) { - CapturedRenderingState.INSTANCE.setCurrentRenderedItem(-1); + CapturedRenderingState.INSTANCE.setCurrentRenderedItem(0); } } } diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderItem.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderItem.java index 95f5a7779..9f483a881 100644 --- a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderItem.java +++ b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderItem.java @@ -68,6 +68,6 @@ public class MixinRenderItem { at = @At("RETURN") ) private void iris$resetItemIdAfterRender(net.minecraft.entity.item.EntityItem entity, double x, double y, double z, float entityYaw, float partialTicks, CallbackInfo ci) { - CapturedRenderingState.INSTANCE.setCurrentRenderedItem(-1); + CapturedRenderingState.INSTANCE.setCurrentRenderedItem(0); } } diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderManager.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderManager.java new file mode 100644 index 000000000..03eaee0c2 --- /dev/null +++ b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderManager.java @@ -0,0 +1,114 @@ +package com.gtnewhorizons.angelica.mixins.early.shaders; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import it.unimi.dsi.fastutil.objects.Object2IntFunction; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import net.coderbot.iris.block_rendering.BlockRenderingSettings; +import net.coderbot.iris.shaderpack.materialmap.NamespacedId; +import net.coderbot.iris.uniforms.CapturedRenderingState; +import net.coderbot.iris.uniforms.ItemMaterialHelper; +import net.minecraft.client.renderer.entity.RenderManager; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityList; +import net.minecraft.entity.item.EntityItem; +import net.minecraft.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; + +/** + * Mixin to handle material ID setting for entities rendered through renderEntityWithPosYaw. + * This is needed for EntityPickupFX which renders entities during pickup animation + * Entity ID mapping is handled by MixinRenderGlobal for the normal rendering path. + */ + +// TODO: Find a way to de-duplicate this code. +@Mixin(RenderManager.class) +public class MixinRenderManager { + @Unique private static final Object2IntOpenHashMap angelica$entityIdCache = new Object2IntOpenHashMap<>(); + @Unique private static Object2IntFunction angelica$cachedEntityIdMap; + + static { + angelica$entityIdCache.defaultReturnValue(Integer.MIN_VALUE); + } + + @WrapOperation( + method = "func_147939_a", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/entity/Render;doRender(Lnet/minecraft/entity/Entity;DDDFF)V") + ) + private void iris$wrapDoRender(net.minecraft.client.renderer.entity.Render render, Entity entity, double x, double y, double z, float entityYaw, float partialTicks, Operation original) { + // Set appropriate material ID when entities are rendered through renderEntityWithPosYaw + // (used by EntityPickupFX during pickup animation) + // Note: Entity ID mapping is handled by MixinRenderGlobal for the normal path + boolean setItemId = false; + boolean setEntityId = false; + + if (entity instanceof EntityItem) { + EntityItem entityItem = (EntityItem) entity; + ItemStack itemStack = entityItem.getEntityItem(); + int itemId = ItemMaterialHelper.getMaterialId(itemStack); + CapturedRenderingState.INSTANCE.setCurrentRenderedItem(itemId); + setItemId = true; + } else { + Object2IntFunction entityIdMap = BlockRenderingSettings.INSTANCE.getEntityIds(); + if (entityIdMap != null) { + // Invalidate cache if the map changed (shader reload) + if (entityIdMap != angelica$cachedEntityIdMap) { + angelica$entityIdCache.clear(); + angelica$cachedEntityIdMap = entityIdMap; + } + + int entityId = entity.getEntityId(); + + String entityType = EntityList.getEntityString(entity); + String cacheKey; + + if (entityType != null) { + // Use registered entity name + cacheKey = entityType; + int cached = angelica$entityIdCache.getInt(cacheKey); + if (cached == Integer.MIN_VALUE) { + cached = entityIdMap.applyAsInt(new NamespacedId(entityType)); + angelica$entityIdCache.put(cacheKey, cached); + } + entityId = cached; + } else { + // Unregistered entity - try class names + Class entityClass = entity.getClass(); + String className = entityClass.getName(); + cacheKey = className; + + int cached = angelica$entityIdCache.getInt(cacheKey); + if (cached == Integer.MIN_VALUE) { + // Try simple class name first (e.g., "EntityLightningBolt") + String simpleClassName = entityClass.getSimpleName(); + cached = entityIdMap.applyAsInt(new NamespacedId(simpleClassName)); + + // If not found, try FQN (e.g., "net.minecraft.entity.effect.EntityLightningBolt") + if (cached == -1) { + cached = entityIdMap.applyAsInt(new NamespacedId(className)); + } + + angelica$entityIdCache.put(cacheKey, cached); + } + entityId = cached; + } + + CapturedRenderingState.INSTANCE.setCurrentEntity(entityId); + setEntityId = true; + } + } + + try { + original.call(render, entity, x, y, z, entityYaw, partialTicks); + } finally { + if (setItemId) { + CapturedRenderingState.INSTANCE.setCurrentRenderedItem(0); + } + if (setEntityId) { + CapturedRenderingState.INSTANCE.setCurrentEntity(-1); + } + } + } +} diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderPlayerArmor.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderPlayerArmor.java index ecef7cd08..d507e7aa9 100644 --- a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderPlayerArmor.java +++ b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderPlayerArmor.java @@ -32,7 +32,7 @@ public class MixinRenderPlayerArmor { ItemStack itemStack = player.getCurrentArmor(3 - armorSlot); if (itemStack == null || itemStack.getItem() == null) { - CapturedRenderingState.INSTANCE.setCurrentRenderedItem(-1); + CapturedRenderingState.INSTANCE.setCurrentRenderedItem(0); original.call(instance, model); return; } @@ -54,7 +54,7 @@ public class MixinRenderPlayerArmor { at = @At("HEAD") ) private void iris$resetAtStart(AbstractClientPlayer player, double x, double y, double z, float entityYaw, float partialTicks, org.spongepowered.asm.mixin.injection.callback.CallbackInfo ci) { - CapturedRenderingState.INSTANCE.setCurrentRenderedItem(-1); + CapturedRenderingState.INSTANCE.setCurrentRenderedItem(0); } /** @@ -66,7 +66,7 @@ public class MixinRenderPlayerArmor { at = @At("HEAD") ) private void iris$resetInEquippedItems(AbstractClientPlayer player, float partialTicks, org.spongepowered.asm.mixin.injection.callback.CallbackInfo ci) { - CapturedRenderingState.INSTANCE.setCurrentRenderedItem(-1); + CapturedRenderingState.INSTANCE.setCurrentRenderedItem(0); } /** @@ -90,6 +90,6 @@ public class MixinRenderPlayerArmor { original.call(itemRenderer, entity, itemStack, renderPass); // Reset after rendering the held item - CapturedRenderingState.INSTANCE.setCurrentRenderedItem(-1); + CapturedRenderingState.INSTANCE.setCurrentRenderedItem(0); } }